In this article

Jest can be used for more than just unit testing your UI. It can be used on your whole tech stack. If you want to expand your tests at the API layer to include integration tests involving your database, let's examine what that might look like. For this build out we are using Node 14, Typescript, TypeORM and MySQL.

Schema creation

There are a few approaches that I want to discuss for your test framework setup. You probably want to stand up a unique database schema for your integration tests at the beginning of a test run. Likewise, we don't want extra schemas hanging around after, so we will need to clean that up.

We hooked our npm test script to execute a node process that runs a setup javascript file. For our integration tests, we need to make sure each run has a unique schema name. This can be accomplished by appending the date to our integration schema name.

process.env.MYSQL_DATABASE_INTEGRATION =

  process.env.MYSQL_DATABASE_INTEGRATION + Date.now();

...

jest.run(argv);

We will take advantage of the jest.config.js file at the root level of your project to configure this. There are two noteworthy configurations. The globalSetup option runs first, before your tests. Likewise, globalTeardown runs after all tests have complete.

globalSetup: "./src/integrationTests/databaseSetup.ts",

globalTeardown: "./src/integrationTests/databaseTeardown.ts"

In the setup file, we need to create a new schema. Don't forget to grant access to that schema to your database user.

import mysql from "mysql";



module.exports = async () => {

  const {

    MYSQL_DATABASE_INTEGRATION,

    MYSQL_HOST,

    MYSQL_ROOT_PASSWORD,

    MYSQL_USER

  } = process.env;



  await new Promise((resolve, reject) => {

    try {

      var con = mysql.createConnection({

        host: MYSQL_HOST,

        user: "root",

        password: MYSQL_ROOT_PASSWORD

      });

      con.connect(async function(err: any) {

        if (err) throw err;

        con.query(

          "create database " + MYSQL_DATABASE_INTEGRATION,

          async function(err: any) {

            if (err) throw err;



            con.query(

              "GRANT ALL PRIVILEGES ON " +

                MYSQL_DATABASE_INTEGRATION +

                ".* TO '" +

                MYSQL_USER +

                "'@'%'",

              function(err: any) {

                if (err) throw err;

              }

            );

            con.end();

            resolve(true);

          }

        );

      });

    } catch (err) {

      reject(err);

    }

  });

};

Our teardown file is simple. We just need to drop the database schema.

import mysql from "mysql";



const {

  MYSQL_DATABASE_INTEGRATION,

  MYSQL_HOST,

  MYSQL_ROOT_PASSWORD

} = process.env;



module.exports = async () => {

  return new Promise((resolve, reject) => {

    var con = mysql.createConnection({

      host: MYSQL_HOST,

      user: "root",

      password: MYSQL_ROOT_PASSWORD

    });

    con.connect(function(err) {

      if (err) reject(err);

      con.query("drop database " + MYSQL_DATABASE_INTEGRATION, function(err) {

        if (err) reject(err);

      });

      con.end();

      resolve();

    });

  });

};

Note that in these files TypeORM was not used, but we did use the MySql package. TypeORM did not support some of the functionality required to execute DDL queries with MySql. For simplicity, I decided to be explicit in these files.

Connecting to your integration schema in tests

Now running the tests is where things get tricky. Let's start with a single test file. Jest runs the individual tests in a file sequentially. There is no conflict. Let's review our database connection file which helps our test files use our database instance. Ours uses three methods: connect, close and clear.

const connection = {

  async connect() {

    const connectionOptions = await getConnectionOptions(MYSQL_CONNECTION_NAME);

    await createConnection({

      ...connectionOptions,

      name: "test",

      entities: [path.join(__dirname, "../entity/*.ts")],

      migrations: [path.join(__dirname, "../migration/*.ts")],

      subscribers: [path.join(__dirname, "../subscriber/*.ts")],

      migrationsRun: true

    });

  },



  async close() {

    await getConnection(MYSQL_CONNECTION_NAME).close();

  },



  async clear() {

    const connection = getConnection(MYSQL_CONNECTION_NAME);

    const entities = connection.entityMetadatas;



    const entitiesToTruncate = [...truncateOrder];

    for (let entity of entities) {

      if (

        entitiesToTruncate.indexOf(entity.name) === -1 &&

        skipTruncating.indexOf(entity.name) === -1

      ) {

        entitiesToTruncate.push(entity.tableName);

      }

    }



    for (let entity of entitiesToTruncate) {

      const repository = connection.getRepository(entity);

      await repository.query(`DELETE FROM ${entity} WHERE 1=1`);

    }

  }

};

The clear function was a little tricky. Again, TypeORM has some nice functionality for clearing a schema that was not supported by MySql yet. Instead, tables are cleared individually. But the order is important due to foreign key dependencies. 

MySql also throws errors when truncating a table with foreign keys (hence the '1=1'). Jest has a nasty habit of swallowing error messages that occur in asynchronous methods run in the before or after hooks. Be careful if you are seeing some unusual behavior that you can't explain.

A single test then needs to create a connection for TypeORM to use and close that connection at the end.

beforeAll(() => {

    return connection.connect();

  });



  afterAll(() => {

    return connection.close();

  });

Using test-specific data

You can add test-specific data in your beforeEach hook. Make sure to call clear in the afterEach.

afterEach(() => {

    return new Promise(async (resolve, reject) => {

      try {

        await connection.clear();

        jest.clearAllMocks();

        resolve();

      } catch (e) {

        reject(e);

      }

    });

  });

Each of these hooks returns the asynchronous promise. This tells the Jest framework to wait until the tasks complete instead of closing out early.

Running test files in parallel

What happens when you add multiple test files? Jest runs multiple files in parallel by default. If they all connect to the same schema and clear your schema (delete all data), this setup becomes problematic. This now comes to your preference as to what works best for your setup. Here are a few options you can implement:

  1. Move the logic to create a database schema with a unique name in the beforeAll of your test setup. This creates a schema per test file. This may be a convenient option but be careful of the time spent standing up new database schemas for each file.
  2. Only clear specific test data in your afterEach. Make sure to use unique identifiers to only delete data relevant to your test. Be careful that your tests are not checking the database contents with a select statement that could grab something other tests are adding.
  3. Run the tests sequentially using the Jest command line option --runInBand. This can be used in your pipeline which runs unit tests and integration tests separately. Run just the integration tests sequentially.

File structure

My final note is how Jest really is a ready-to-go framework. Name your tests in the following manner:

  • TestComponent.unit.test.ts
  • TestComponent.int.test.ts

You can now easily run your unit tests and integration tests separately by executing npm run test unit or npm run test int. Jest uses the final option as a regex for your file names, running only the matching test files.

I hope this makes it easy to set up all your tests using Jest. I've found Jest to be a convenient tool with easy-to-read output.

Learn more about our development expertise.
Explore