Getting started with Testcontainers for Node.js
Testcontainers is a testing library that provides easy and lightweight APIs for bootstrapping integration tests with real services wrapped in Docker containers. Using Testcontainers, you can write tests that talk to the same type of services you use in production, without mocks or in-memory services.
If you are new to Testcontainers, please read What is Testcontainers, and why should you use it? to learn more about it. |
Let us look at how we can use Testcontainers to test a Node.js application using a PostgreSQL database.
Create a new project
Initialize a new Node.js project using the npm init
command:
npm init -y
Add pg
, jest
and @testcontainers/postgresql
as dependencies:
npm install pg --save
npm install jest @testcontainers/postgresql --save-dev
Write the tests and solution
Let us create a simple application that stores and retrieves customers from a PostgreSQL database.
Test driven development (TDD) is a great way to develop software, and coupled with Testcontainers we can quickly iterate to get working solutions with confidence. Let’s write our first test which saves and returns customers from PostgreSQL:
const { Client } = require("pg");
const { PostgreSqlContainer } = require("@testcontainers/postgresql");
const { createCustomerTable, createCustomer, getCustomers } = require("./customer-repository");
describe("Customer Repository", () => {
jest.setTimeout(60000);
let postgresContainer;
let postgresClient;
beforeAll(async () => {
postgresContainer = await new PostgreSqlContainer().start();
postgresClient = new Client({ connectionString: postgresContainer.getConnectionUri() });
await postgresClient.connect();
await createCustomerTable(postgresClient)
});
afterAll(async () => {
await postgresClient.end();
await postgresContainer.stop();
});
it("should create and return multiple customers", async () => {
const customer1 = { id: 1, name: "John Doe" };
const customer2 = { id: 2, name: "Jane Doe" };
await createCustomer(postgresClient, customer1);
await createCustomer(postgresClient, customer2);
const customers = await getCustomers(postgresClient);
expect(customers).toEqual([customer1, customer2]);
});
});
The beforeAll
block sets up a actual PostgreSQL container. We then initialize a client from the pg
library, and connect it to the PostgreSQL instance running in the container. As part of the test setup, we create the customer
table.
Now, let’s implement a solution:
async function createCustomerTable(client) {
const sql = "CREATE TABLE IF NOT EXISTS customers (id INT NOT NULL, name VARCHAR NOT NULL, PRIMARY KEY (id))";
await client.query(sql);
}
async function createCustomer(client, customer) {
const sql = "INSERT INTO customers (id, name) VALUES($1, $2)";
await client.query(sql, [customer.id, customer.name]);
}
async function getCustomers(client) {
const sql = "SELECT * FROM customers";
const result = await client.query(sql);
return result.rows;
}
module.exports = { createCustomerTable, createCustomer, getCustomers }
Looks good, but does it work?
$ npm test
> [email protected] test
PASS src/customer-repository.test.js
Customer Repository
✓ should create and return multiple customers (5 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.11 s, estimated 3 s
Ran all test suites.
Voila! You have your first Testcontainers based test running.
Are you seeing the benefit of testing with a real PostgreSQL instance? If not, try changing the id
type of the customer
table from INT
to BIGINT
and re-run the test, what would you expect to happen? If you had a unit test, or used a mock instead of using Testcontainers, you would not have caught this bug.
Sometimes it’s useful to see what Testcontainers is doing under the hood. Did we really spin up a container? What version did we use? What’s PostgreSQL doing? You can see all this and more by setting the DEBUG
environment variable. Let’s re-run the test with DEBUG=testcontainers*
and find out:
$ DEBUG=testcontainers* npm test
> [email protected] test
testcontainers [DEBUG] Loading ".testcontainers.properties" file... +0ms
testcontainers [DEBUG] Loaded Docker client configuration, tcHost: "tcp://127.0.0.1:42317", dockerHost: "tcp://127.0.0.1:42317" +3ms
testcontainers [DEBUG] Found Docker client strategy "UnixSocketStrategy" +0ms
testcontainers [DEBUG] Testing Docker client strategy "unix:///var/run/docker.sock"... +0ms
testcontainers [DEBUG] Fetching system info... +3ms
testcontainers [DEBUG] Node version: v18.15.0, Platform: linux, Arch: x64, OS: Ubuntu 23.04, Version: 24.0.1, Arch: x86_64, CPUs: 32, Memory: 16746291200, Compose installed: true, Compose version: 1.29.2 +216ms
testcontainers [INFO] Using Docker client strategy "UnixSocketStrategy", Docker host "localhost" (127.0.0.1) +1ms
testcontainers [DEBUG] Not pulling image "postgres:13.3-alpine" as it already exists +2ms
testcontainers [DEBUG] Creating new Reaper for session "2d8457b13f3d" with socket path "/var/run/docker.sock"... +0ms
testcontainers [DEBUG] Not pulling image "testcontainers/ryuk:0.4.0" as it already exists +1ms
testcontainers [INFO] Creating container for image "testcontainers/ryuk:0.4.0"... +0ms
testcontainers [INFO] [a72827588430] Created container for image "testcontainers/ryuk:0.4.0" +52ms
testcontainers [INFO] [a72827588430] Starting container for image "testcontainers/ryuk:0.4.0"... +0ms
testcontainers [INFO] [a72827588430] Started container for image "testcontainers/ryuk:0.4.0" +249ms
testcontainers [DEBUG] [a72827588430] Waiting for container to be ready... +2ms
testcontainers [DEBUG] [a72827588430] Waiting for log message "/.+ Started!/"... +1ms
testcontainers:containers [a72827588430] 2023/06/07 09:06:32 Pinging Docker... +0ms
testcontainers:containers [a72827588430] 2023/06/07 09:06:32 Docker daemon is available! +0ms
testcontainers:containers [a72827588430] 2023/06/07 09:06:32 Starting on port 8080... +0ms
testcontainers:containers [a72827588430] 2023/06/07 09:06:32 Started! +0ms
testcontainers [INFO] [a72827588430] Container is ready +2ms
testcontainers [DEBUG] [a72827588430] Connecting to Reaper (attempt 1) on "localhost:32783"... +0ms
testcontainers [DEBUG] [a72827588430] Connected to Reaper +1ms
testcontainers [INFO] Creating container for image "postgres:13.3-alpine"... +0ms
testcontainers:containers [a72827588430] 2023/06/07 09:06:32 New client connected: 172.17.0.1:46694 +3ms
testcontainers:containers [a72827588430] 2023/06/07 09:06:32 Adding {"label":{"org.testcontainers.session-id=2d8457b13f3d":true}} +0ms
testcontainers [INFO] [9d3296dd6c2a] Created container for image "postgres:13.3-alpine" +36ms
testcontainers [INFO] [9d3296dd6c2a] Starting container for image "postgres:13.3-alpine"... +0ms
testcontainers [INFO] [9d3296dd6c2a] Started container for image "postgres:13.3-alpine" +248ms
testcontainers [DEBUG] [9d3296dd6c2a] Waiting for container to be ready... +1ms
testcontainers [DEBUG] [9d3296dd6c2a] Waiting for log message "/.*database system is ready to accept connections.*/"... +0ms
testcontainers:containers [9d3296dd6c2a] The files belonging to this database system will be owned by user "postgres". +285ms
testcontainers:containers [9d3296dd6c2a] This user must also own the server process. +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] The database cluster will be initialized with locale "en_US.utf8". +0ms
testcontainers:containers [9d3296dd6c2a] The default database encoding has accordingly been set to "UTF8". +0ms
testcontainers:containers [9d3296dd6c2a] The default text search configuration will be set to "english". +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] Data page checksums are disabled. +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] fixing permissions on existing directory /var/lib/postgresql/data ... ok +0ms
testcontainers:containers [9d3296dd6c2a] creating subdirectories ... ok +0ms
testcontainers:containers [9d3296dd6c2a] selecting dynamic shared memory implementation ... posix +0ms
testcontainers:containers [9d3296dd6c2a] selecting default max_connections ... 100 +0ms
testcontainers:containers [9d3296dd6c2a] selecting default shared_buffers ... 128MB +2ms
testcontainers:containers [9d3296dd6c2a] selecting default time zone ... UTC +27ms
testcontainers:containers [9d3296dd6c2a] creating configuration files ... ok +1ms
testcontainers:containers [9d3296dd6c2a] running bootstrap script ... ok +47ms
testcontainers:containers [9d3296dd6c2a] sh: locale: not found +92ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:32.636 UTC [30] WARNING: no usable system locales were found +0ms
testcontainers:containers [9d3296dd6c2a] performing post-bootstrap initialization ... ok +202ms
testcontainers:containers [9d3296dd6c2a] syncing data to disk ... ok +46ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] Success. You can now start the database server using: +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] initdb: warning: enabling "trust" authentication for local connections +0ms
testcontainers:containers [9d3296dd6c2a] You can change this by editing pg_hba.conf or using the option -A, or +0ms
testcontainers:containers [9d3296dd6c2a] --auth-local and --auth-host, the next time you run initdb. +0ms
testcontainers:containers [9d3296dd6c2a] pg_ctl -D /var/lib/postgresql/data -l logfile start +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] waiting for server to start....2023-06-07 09:06:32.903 UTC [35] LOG: starting PostgreSQL 13.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.3.1_git20210424) 10.3.1 20210424, 64-bit +20ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:32.910 UTC [35] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" +7ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:32.919 UTC [36] LOG: database system was shut down at 2023-06-07 09:06:32 UTC +9ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:32.922 UTC [35] LOG: database system is ready to accept connections +2ms
testcontainers:containers [9d3296dd6c2a] done +65ms
testcontainers:containers [9d3296dd6c2a] server started +0ms
testcontainers:containers [9d3296dd6c2a] CREATE DATABASE +81ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/* +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] waiting for server to shut down....2023-06-07 09:06:33.068 UTC [35] LOG: received fast shutdown request +1ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.071 UTC [35] LOG: aborting any active transactions +2ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.071 UTC [35] LOG: background worker "logical replication launcher" (PID 42) exited with exit code 1 +0ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.071 UTC [37] LOG: shutting down +0ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.092 UTC [35] LOG: database system is shut down +22ms
testcontainers:containers [9d3296dd6c2a] done +76ms
testcontainers:containers [9d3296dd6c2a] server stopped +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] PostgreSQL init process complete; ready for start up. +0ms
testcontainers:containers [9d3296dd6c2a] +0ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.180 UTC [1] LOG: starting PostgreSQL 13.3 on x86_64-pc-linux-musl, compiled by gcc (Alpine 10.3.1_git20210424) 10.3.1 20210424, 64-bit +12ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.180 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 +0ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.180 UTC [1] LOG: listening on IPv6 address "::", port 5432 +0ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.186 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" +5ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.193 UTC [49] LOG: database system was shut down at 2023-06-07 09:06:33 UTC +7ms
testcontainers [INFO] [9d3296dd6c2a] Container is ready +731ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.197 UTC [1] LOG: database system is ready to accept connections +7ms
testcontainers [INFO] [9d3296dd6c2a] Stopping container... +27ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.243 UTC [1] LOG: received fast shutdown request +43ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.249 UTC [1] LOG: aborting any active transactions +6ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.249 UTC [1] LOG: background worker "logical replication launcher" (PID 55) exited with exit code 1 +1ms
testcontainers:containers [9d3296dd6c2a] 2023-06-07 09:06:33.249 UTC [50] LOG: shutting down +0ms
testcontainers [INFO] [9d3296dd6c2a] Stopped container +351ms
PASS src/customer-repository.test.js
Customer Repository
✓ should create and return multiple customers (4 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 2.138 s, estimated 3 s
Ran all test suites
Conclusion
We have explored how to use Testcontainers for Node.js to test an application using a PostgreSQL database.
We have seen how writing an integration test using Testcontainers is very similar to writing a unit test that you can run from your IDE. Also, any of your teammates can clone the project and run tests without installing Postgres on their computers.
In addition to PostgreSQL, Testcontainers provides dedicated modules for many commonly used SQL databases, NoSQL databases, messaging queues, etc. Besides the modules you can use Testcontainers to run any containerized dependency for your tests!
You can explore more about Testcontainers at https://testcontainers.com/.