Dockest is an integration testing tool aimed at alleviating the process of evaluating unit tests whilst running multi-container Docker applications.
The original motivation for Dockest, along with real world examples, can be read in this blog article.
Dockest was born out of frustration and with a vision to make developers’ lives slightly less miserable.
Dockest provides an abstraction for your Docker services’ lifecycles during integration testing, freeing developers from convoluted and flaky shell scripts. Adopting Dockest is super easy regardless if you’ve got existing tests or not and doesn’t necessarily require additional CI pipeline steps.
The value that Dockest provides over e.g. plain docker-compose is that it figures out the connectivity and responsiveness status of each individual service (either synchronously or asynchronously) and once all services are ready the tests run.
Dockest can be used in a variety of use cases and situations, some of which can be found under
packages/examples.
What is AWS CodeBuild?
Cool, can I run it locally?
You can now locally test and debug your AWS CodeBuild builds using the new CodeBuild local agent.
Dockest can also build and run application services as part of your integration tests.
In order to run Dockest, there's a few system requirements:
yarn add --dev dockest
# npm install --save-dev dockest
// cache.ts
export const cacheKey = 'arbitraryNumberKey';
export const setCache = (redisClient: Redis, arbitraryNumber: number) => {
redisClient.set(cacheKey, arbitraryNumber);
};
// cache.spec.ts
import Redis from 'ioredis'; // ... or client of choice
import { cacheKey, setCache } from './cache';
const redisClient = new Redis({
host: 'localhost',
port: 6379, // Match with configuration in docker-compose.yml
});
it('should cache an arbitrary number', async () => {
const arbitraryNumber = 5;
await setCache(redisClient, arbitraryNumber);
const cachedValue = await redisClient.get(cacheKey);
expect(cachedValue).toEqual(arbitraryNumber);
});
Transform unit test into an integration test by creating a docker-compose.yml and dockest.ts file.
Important note for the Compose file
Dockest expects services' ports to be defined using long format and works best with high versions of docker-compose (i.e. 3.7 or higher)
# docker-compose.yml
version: '3.8'
services:
myRedis:
image: redis:5.0.3-alpine
ports:
- published: 6379
target: 6379
// dockest.ts
import { Dockest } from 'dockest';
const dockest = new Dockest();
// Specify the services from the Compose file that should be included in the integration test
const dockestServices = [
{
serviceName: 'myRedis', // Must match a service in the Compose file
},
];
dockest.run(dockestServices);
Configure package.json to run dockest.ts. ts-node is recommended for
TypeScript projects.
{
"scripts": {
"test": "ts-node ./dockest"
},
"devDependencies": {
"dockest": "...",
"ts-node": "..."
}
}
Finally, run the tests:
yarn test
v1.2.3-alpha.0 or -beta.0 to create a tagged releasegit checkout -b "release-v1.2.3"yarn lerna version --no-push --no-git-tag-versionmain branchSetup and testing
This is a monorepo using lerna, meaning all scripts can be run from root.
yarn prep will executes the necessary scripts to install dependencies for all packages (including root) as well as
build whatever needs building.
yarn dev:link will link the library source to each example, making developing a smoother experience.
import { Dockest } from 'dockest';
const { run } = new Dockest(opts);
DockestOpts is optional, i.e. the dockest constructor can be called without arguments.
DockestOpts structure:
| property | type | default |
|---|---|---|
| composeFile | string |
docker-compose.yml |
| composeOpts | object |
see paragraph on composeOpts |
| debug | boolean |
false |
| dumpErrors | boolean |
false |
| exitHandler | function |
null |
| jestLib | object |
require('jest') |
| jestOpts | object |
{} |
| logLevel | object |
logLevel.INFO, i.e. 3 |
| runInBand | boolean |
true |
DockestOpts.composeFileCompose file(s) with services to use while running tests
DockestOpts.composeOptscomposeOpts structure:
| property | desription | type | default |
|---|---|---|---|
| alwaysRecreateDeps | Recreate dependent containers. Incompatible with --no-recreate |
boolean |
false |
| build | Build images before starting containers | boolean |
false |
| forceRecreate | Recreate containers even if their configuration and image haven't changed | boolean |
false |
| noBuild | Don't build an image, even if it's missing | boolean |
false |
| noColor | Produce monochrome output | boolean |
false |
| noDeps | Don't start linked services | boolean |
false |
| noRecreate | If containers already exist, don't recreate them. Incompatible with --force-recreate and -V |
boolean |
false |
| quietPull | Pull without printing progress information | boolean |
false |
Forwards options to docker-compose up, Docker's docs.
DockestOpts.debugPauses Dockest just before executing Jest. Useful for more rapid development using Jest manually
DockestOpts.dumpErrorsSerializes errors and dumps them in dockest-error.json. Useful for debugging.
DockestOpts.exitHandlerCallback that will run before exit. Received one argument of type { type: string, code?: number, signal?: any, error?: Error, reason?: any, p?: any }
DockestOpts.jestLibThe Jest library itself, typically passed as { lib: require('jest') }. If omitted, Dockest will attempt to require
Jest from your application's dependencies. If absent, Dockest will use it's own version.
DockestOpts.jestOptsJest's CLI options, an exhaustive list of CLI-options can be found in Jest's documentation
DockestOpts.logLevelDecides how much logging will occur. Each level represents a number ranging from 0-4
DockestOpts.runInBand [boolean]Initializes and runs the Runners in sequence. Disabling this could increase performance
Note: Jest runs tests in parallel per default, which is why Dockest defaults runInBand to true. This will cause
jest to run sequentially in order to avoid race conditions for I/O operations. This may lead to longer runtimes.
import { Dockest } from 'dockest';
const { run } = new Dockest();
const dockestServices = [
{
serviceName: 'service1',
commands: ['echo "Hello name1 🌊"'],
dependents: [
{
serviceName: 'service2',
},
],
readinessCheck: () => Promise.resolve(),
},
];
run(dockestServices);
Dockest services are meant to map to services declared in the Compose file(s)
DockestService structure:
| property | type | default |
|---|---|---|
| name | string |
property is required |
| commands | (string | function)[] => string[] |
[] |
| dependents | DockestService[] |
[] |
| readinessCheck | function |
() => Promise.resolve() |
DockestService.nameService name that matches the corresponding service in your Compose file
DockestService.commandsBash scripts that will run once the service is ready. E.g. database migrations.
Can either be a string, or a function that generates a string. The function is fed the container id of the service.
DockestService.dependentsdependents are Dockest services that are are dependent on the parent service.
For example, the following code
const dockestServices = [
{
serviceName: 'service1',
dependents: [
{
serviceName: 'service2',
},
],
},
];
will ensure that service1 starts up and is fully responsive before even attempting to start service2.
Why not rely on the Docker File service configuration options
depends_on?
Docker's docs explains this very neatly:
version: '3.8'
services:
web:
build: .
depends_on:
- db
- redis
redis:
image: redis
db:
image: postgres
depends_ondoes not wait fordbandredisto be “ready” before startingweb- only until they have been started.
DockestService.readinessCheckThe Dockest Service's readinessCheck function helps determining a service's readiness (or "responsiveness") by, for
example, querying a database using select 1. The readinessCheck function receive the corresponding Compose service
configuration from the Compose file as first argument and the containerId as the second.
The readinessCheck takes a single argument in form of an object.
```ts const dockestServices
$ claude mcp add dockest \
-- python -m otcore.mcp_server <graph>