Skip to content

Commit

Permalink
mini guide for ava+testcontainers
Browse files Browse the repository at this point in the history
  • Loading branch information
sombriks committed Jun 25, 2024
1 parent f5e8935 commit 68c3b67
Show file tree
Hide file tree
Showing 9 changed files with 3,697 additions and 1 deletion.
115 changes: 114 additions & 1 deletion docs/recipes/ava-and-testcontainers.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,121 @@
# Using AVA with testcontainers

In order to extend test boundaries, one can use [testcontainers][testcontainers]
to provision services locally and evolve unit tests into integration tests while avoiding the use of [mocks][sinon].
to provision services locally and evolve unit tests into integration tests while
avoiding the use of too much [mocks][sinon].

## Dependencies

- [ava 6][ava]
- [test containers postgresql plugin 10][tc-postgres]
- [knex 3][knex]
- [pg 8][pg]
- [dotenv-flow 4][dotenv]
- [docker 25 or newer properly configured][docker]

## Setup

```bash
mkdir sample-testcontainers
cd sample-testcontainers
npm init -y
npm i knex pg dotenv-flow
npm i -D ava @testcontainers/postgresql
touch example.js example.spec.js init-db.sql .env README.md .gitignore
```

The whole point of provision a database for testing is to know the db state, so
there is a init script:

```sql
-- init-db.sql script for our todo list

-- our table
create table if not exists todos(
id serial primary key,
description text not null,
is_done boolean not null default false,
created_at timestamp not null default current_timestamp,
updated_at timestamp not null default current_timestamp
);

-- some test values

insert into todos(description, is_done)
values ('Do the dishes', true),
('Walk the dogs', false),
('Buy Groceries', false),
('Wash the cars', false),
('Pay due bills', true);
```

Then you can prepare a database instance for the test suite using `before` and
`after` [test hooks][test-setup-hooks]:

```javascript
// some imports

test.before(async t => {
// provision postgres
t.context.postgres = await new PostgreSqlContainer('postgres:16.3-alpine3.20')
.withDatabase(process.env.PG_DATABASE)
.withUsername(process.env.PG_USERNAME)
.withPassword(process.env.PG_PASSWORD)
.withBindMounts([{
source: resolve('./init-db.sql'),
target: '/docker-entrypoint-initdb.d/init.sql',
}])
.start();

// set query builder instance in test suite context
t.context.db = provisionDb(t.context.postgres.getConnectionUri())
})

test.after.always(async t => {
// proper cleanup after all test cases
await t.context.db.destroy();
await t.context.postgres.stop({ timeout: 500 });
})

// some test cases

test("should list using 'ilike'", async t => {
const todos = await t.context.db('todos').whereILike("description", "%do%")
t.truthy(todos)
t.is(2, todos.length)
t.truthy(todos.find(todo => todo.description == "Do the dishes"))
})

```

## Sample run

```bash
$ npm run test

> [email protected] test
> ava --node-arguments '-r dotenv-flow/config' example.spec.js

⚠ Using configuration from /home/sombriks/git/ava/ava.config.js

✔ should list todos
✔ should list using 'ilike'

2 tests passed
```

## Further reading

- [Example project][example]

[testcontainers]: https://testcontainers.com/
[sinon]: https://sinonjs.org/
[ava]: https://avajs.dev/
[tc-postgres]: https://node.testcontainers.org/modules/postgresql/
[knex]: https://knexjs.org/
[pg]: https://node-postgres.com/
[dotenv]: https://github.com/kerimdzhanov/dotenv-flow
[docker]: https://docs.docker.com/engine/install/
[example]: ../../examples/sample-tescontainers/README.md
[test-setup-hooks]: ../../docs/01-writing-tests.md
6 changes: 6 additions & 0 deletions examples/sample-tescontainers/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# PG_CONNECTION_URL= postgresql://username:password@localhost:5432/todolist
PG_DATABASE=todolist
PG_USERNAME=username
PG_PASSWORD=password
PG_HOSTNAME=localhost
PG_PORT=5432
1 change: 1 addition & 0 deletions examples/sample-tescontainers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
34 changes: 34 additions & 0 deletions examples/sample-tescontainers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# sample testcontainers with ava

Extending test boundaries by including the database

## Dependencies

- ava
- testcontainers postgresql module
- knex
- pg
- dotenv-flow

See [package.json][package.json] for details

## Running

Download dependencies:

```bash
npm i
```

Run the test suite:

```bash
npm run test
```

## Noteworthy

See [recipes][recipes]

[recipes]: ../../docs/recipes/ava-and-testcontainers.md
[package.json]: ./package.json
12 changes: 12 additions & 0 deletions examples/sample-tescontainers/example.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Knex from "knex"

Check failure on line 1 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

Strings must use singlequote.

Check failure on line 1 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

Missing semicolon.

export const provisionDb = (connection) => {

Check failure on line 3 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

Unexpected parentheses around single function argument.
if (!connection) {

Check failure on line 4 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

Expected indentation of 1 tab but found 2 spaces.
const { PG_DATABASE, PG_HOSTNAME, PG_PORT, PG_USERNAME, PG_PASSWORD } = process.env

Check failure on line 5 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

Expected indentation of 2 tabs but found 4 spaces.

Check failure on line 5 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

There should be no space after '{'.

Check failure on line 5 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

There should be no space before '}'.

Check failure on line 5 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

Unexpected use of the global variable 'process'. Use 'require("process")' instead.

Check failure on line 5 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

Missing semicolon.
connection = `postgresql://${PG_USERNAME}:${PG_PASSWORD}@${PG_HOSTNAME}:${PG_PORT}/${PG_DATABASE}$`

Check failure on line 6 in examples/sample-tescontainers/example.js

View workflow job for this annotation

GitHub Actions / Lint source files

Expected indentation of 2 tabs but found 4 spaces.
}
return Knex({
connection,
client: 'pg'
})
}
39 changes: 39 additions & 0 deletions examples/sample-tescontainers/example.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { resolve } from "path"
import test from 'ava'
import { PostgreSqlContainer } from '@testcontainers/postgresql';

import { provisionDb } from './example.js'

test.before(async t => {
// provision postgres
t.context.postgres = await new PostgreSqlContainer('postgres:16.3-alpine3.20')
.withDatabase(process.env.PG_DATABASE)
.withUsername(process.env.PG_USERNAME)
.withPassword(process.env.PG_PASSWORD)
.withBindMounts([{
source: resolve('./init-db.sql'),
target: '/docker-entrypoint-initdb.d/init.sql',
}])
.start();

// set query builder instance in test suite context
t.context.db = provisionDb(t.context.postgres.getConnectionUri())
})

test.after.always(async t => {
await t.context.db.destroy();
await t.context.postgres.stop({ timeout: 500 });
})

test("should list todos", async t => {
const todos = await t.context.db('todos')
t.truthy(todos)
t.is(5, todos.length)
})

test("should list using 'ilike'", async t => {
const todos = await t.context.db('todos').whereILike("description", "%do%")
t.truthy(todos)
t.is(2, todos.length)
t.truthy(todos.find(todo => todo.description == "Do the dishes"))
})
16 changes: 16 additions & 0 deletions examples/sample-tescontainers/init-db.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- init-db.sql script for our todo list
-- our table
create table if not exists todos(
id serial primary key,
description text not null,
is_done boolean not null default false,
created_at timestamp not null default current_timestamp,
updated_at timestamp not null default current_timestamp
);
-- some test values
insert into todos(description, is_done)
values ('Do the dishes', true),
('Walk the dogs', false),
('Buy Groceries', false),
('Wash the cars', false),
('Pay due bills', true);
Loading

0 comments on commit 68c3b67

Please sign in to comment.