Using Redwoodjs with testcontainers

Summary

I hacked RedwoodJS API tests to work with PostgreSQL in a disposable container via testcontainers. I just got this working, so there are likely error conditions I don’t handle, and the code could be neater. For instance, my approach to the database URL environment variables could be made more precise.

However, since I wasn’t able to find any existing docs on how to do this, I figured what I have now is a step forward and worth sharing.

Background

I wanted to make sure I always had a clean database when running tests. RedwoodJS tries to clean up after itself, but sometimes that doesn’t work, and when you’re doing more in your database than just Prisma models, you have to figure out your own cleanup strategy. That can be difficult to get right, so I figured I’d just throw away the database entirely by using testcontainers. Testcontainers launches a dependency in a Docker container.

Implementation

api/$ yarn add  testcontainers @testcontainers/postgresql

api/src/lib/test/testcontainers.ts

import { PostgreSqlContainer, StartedPostgreSqlContainer } from '@testcontainers/postgresql';

let postgresContainer: StartedPostgreSqlContainer | undefined = undefined

export const initializeContainer = async () => {
  postgresContainer = await new PostgreSqlContainer().start();
  return postgresContainer
};

export const stopContainer = async () => {
  if (postgresContainer) {
    await postgresContainer.stop();
  }
}

process.on('exit', stopContainer)

In api/jest.config.js add:

  globalSetup: '<rootDir>/api/src/lib/test/setup/jestGlobalSetup.ts',
  globalTeardown: '<rootDir>/api/src/lib/test/setup/jestGlobalTeardown.ts',

This was a little hairy because it messed up the Redwoodjs presets. I addressed that in the below:

api/src/lib/test/setup/jestGlobalSetup.ts:

import { initializeContainer } from "../testcontainers";

const rwJestPreset = require('@redwoodjs/testing/config/jest/api/jest-preset')

module.exports = async () => {

  const postgresContainer = await initializeContainer()

  const dbEnvVars = [
    'DIRECT_URL',
    'DATABASE_URL',
    'TEST_DATABASE_URL',
    'TEST_DIRECT_URL',
  ]

  for (const envVar of dbEnvVars) {
    process.env[envVar] = postgresContainer.getConnectionUri()
  }

  if (rwJestPreset.globalSetup) {
    try {
      const presetGlobalSetup = require(rwJestPreset.globalSetup)
      return presetGlobalSetup()
    } catch (e) {
      //  do nothing?
    }
  }
}

Then api/src/lib/test/setup/jestGlobalTeardown.ts

import { stopContainer } from "../testcontainers";

const rwJestPreset = require('@redwoodjs/testing/config/jest/api/jest-preset')

module.exports = async () => {
  if (rwJestPreset.globalTeardown) {
    try {
      const presetGlobalTeardown = require(rwJestPreset.globalTeardown)
      return presetGlobalTeardown()
    } catch (e) {
      //  do nothing?
    }
  }
  await stopContainer()
}

I hope this is useful for someone. Perhaps RedwoodJS can one day support testcontainers out of the box.

2 Likes

Thanks for the writeup @ketan!

We’re not doing test containers, but I haven’t had any problems with our setup, might be, that we just don’t do enough with the database.

We use gitlab and gitlab ci has the concept of services, which we’re using:

.before_script_template:
  before_script:
  - yarn config set enableGlobalCache false
  # abort with an error code if the lock file got edited
  - yarn install --immutable
  environment:
    name: test
  interruptible: true

api-test:
  extends: .before_script_template
  stage: test
  services:
  - postgres:16
  variables:
    POSTGRES_DB: projectname_test
    POSTGRES_USER: projectname_user
    POSTGRES_HOST_AUTH_METHOD: trust
  script:
  - yarn rw test api --watch=false --collect-coverage --ci --reporters=default --reporters=jest-junit --showSeed
  coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
  artifacts:
    reports:
      junit:
      - junit.xml