Pragmatic Testing in AdonisJS with Japa #5.0

Database Test Runner Hooks

In This Lesson

We'll learn how to set up a dedicated test database to ensure clean, isolated testing. We'll configure a separate SQLite database using a test environment variable and set up Japa runner hooks to automatically migrate and seed our database

Created by
@tomgobich
Published

When it comes to tests using a database, we're going to want to test on a database separate from our actual application. We want to test in a clean setup so that if we have a test that creates a user with the email test@test.com that test doesn't get hung up because:

  • We created that user in development

  • That user is still dangling around from a previous test run

  • Anything else in between

To resolve this, first, we're going to want to set our test environment to use a completely different database from our dev server. If you're not using SQLite, then you'll want to make a database for testing. I typically do the normal database name, suffixed with _test, like adocasts_test.

However, we are using SQLite in this project, so we want to instead set this up to use a different database between our dev and test environments.

Within our .env file, we'll add:

DB_DATABASE=db.sqlite3

Great, now we want our test environment to use a different database, so within our .env.test lets add:

DB_DATABASE=test.sqlite3

Next, we'll add this into our start/env.ts definitions to ensure it's defined before our app can boot.

export default await Env.create(new URL("../", import.meta.url), {
  // ...

  DB_DATABASE: Env.schema.string(),
});
Copied!
  • start
  • env.ts

Next, we'll dig into our config/database.ts file to switch this from a hard-coded string to instead use our DB_DATABASE environment variable so it switches appropriately depending on our environment.

const dbConfig = defineConfig({
  connection: "sqlite",
  connections: {
    sqlite: {
      client: "better-sqlite3",
      connection: {
        filename: app.tmpPath(env.get("DB_DATABASE")), // <-
      },
      useNullAsDefault: true,
      migrations: {
        naturalSort: true,
        paths: ["database/migrations"],
      },
    },
  },
});
Copied!
  • config
  • database.ts

Perfect! Now db.sqlite3 will get used on our dev server and test.sqlite3 will get used for our test runs. Next, we want to reset our test database before every run. This will ensure it's always up-to-date with our schema and there's no lingering data that may falsely break tests.

Within our bootstrap.ts file, we can use a runner hook for this using testUtils, which has some db() options.

/**
 * Configure lifecycle function to run before and after all the
 * tests.
 *
 * The setup functions are executed before all the tests
 * The teardown functions are executed after all the tests
 */
export const runnerHooks: Required<Pick<Config, "setup" | "teardown">> = {
  setup: [
    // () => testUtils.db().truncate(), // <- great if you have a ton of migrations
    () => testUtils.db().migrate(),
    () => testUtils.db().seed(),
  ],
  teardown: [],
};
Copied!

The options we have here are:

  • migrate which will run our migrations before any tests are run, then rollback those migrations after our tests have completed.

  • truncate which truncates or empties every table at the end of each test run cycle. It'll leave the tables in place, meaning it can be a quicker option than migrate if you have a ton of migrations.

  • seed which will run any seeders before our tests run.

An important note here is that we're talking about test runs and not individual tests. So, we aren't migrating or seeding between each test, just the overall test run.

Great, so if all went according to plan, we should be able to run our tests without error, and we should also see the SQLite test database created within our projects tmp folder.

Join the Discussion 0 comments

Create a free account to join in on the discussion
robot comment bubble

Be the first to comment!