Skip to content

Commit

Permalink
Add Support for Building DockerCompose Environments (#28)
Browse files Browse the repository at this point in the history
* Add Support for Building DockerCompose Environments

* Add To Docs

* Remove unused Import

* Add Docker Compose to Examples Folder

* Add Docker Compose example Test NPM Script

* Add Example and Fixup Port Mapping

* Add Docker Compose Example to Travis

* Update README with Docker Compose example
  • Loading branch information
jpulec authored Mar 29, 2021
1 parent 9707bed commit e923c24
Show file tree
Hide file tree
Showing 13 changed files with 348 additions and 11 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ script:
- npm run build
- npm run example:redis
- npm run example:redis-typescript
- npm run example:docker-compose
22 changes: 18 additions & 4 deletions DOC.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,29 @@

## Container Configuration
```js
// you can have multiple containers that gets started with your test
export interface JestTestcontainersConfig {
// You can either build containers using docker compose OR set up multiple images
// using SingleContainerConfig, but not both
export type JestTestcontainersConfig =
| DockerComposeContainersConfig
| MultipleContainerConfig;

export type DockerComposeConfig = {
// The directory where the compose file is located
composeFilePath: string;
// The actual docker compose file to be built
composeFile: string;
startupTimeout?: number;
}

// you can have multiple containers that get started with your test
type MultipleContainerConfig = {
// each container needs to have a unique key
// the IP and PORTS for this container will be registered with this container key
// for example, with a *containerKey* of redis, you would end up with
// global.__TESTCONTAINERS_REDIS_IP__ and global.__TESTCONTAINERS_REDIS_PORT_XXXX__
// variables
[containerKey: string]: SingleContainerConfig;
}
[key: string]: SingleContainerConfig;
};

export interface SingleContainerConfig {
image: string;
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ Detailed documentation of the `jest-testcontainers-config.js` can be found at [D
## Examples
- [examples/01-basic-redis](examples/01-basic-redis) showcases writing integration tests against Redis. You can use `npm run build && npm run example:redis` command to run this example locally.
- [examples/02-typescript-redis](examples/02-typescript-redis) same test cases as in example #1. However using Typescript instead of JavaScript. You can use `npm run build && npm run example:redis-typescript` to run this example locally.
- [examples/03-docker-compose](examples/03-docker-compose) same test cases as examples #1 and #2, however using Docker Compose to build the container. You can use `npm run build && npm run example:docker-compose` to run this example locally.
- [Yengas/nodejs-postgresql-testcontainers](https://github.com/Yengas/nodejs-postgresql-testcontainers) showcases writing integration tests against PostgreSQL with schema migration and Typescript. You can check out the project page for more details.

## Watch mode support
Expand Down
8 changes: 8 additions & 0 deletions examples/03-docker-compose/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: "3.7"

services:
redis:
image: redis:6
container_name: redis
ports:
- "6379"
41 changes: 41 additions & 0 deletions examples/03-docker-compose/example.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
const redis = require('redis');
const { promisify } = require('util');

describe('docker compose example suite', () => {
let redisClient;

beforeAll(() => {
const connectionUri = `redis://${global.__TESTCONTAINERS_REDIS_IP__}:${global.__TESTCONTAINERS_REDIS_PORT_6379__}`;
redisClient = redis.createClient(connectionUri);
});

afterAll(() => {
redisClient.quit();
});

it("should set a container name", () => {
expect(global.__TESTCONTAINERS_REDIS_NAME__).toBeDefined();
});

it('should write correctly', async () => {
// Arrange
const setAsync = promisify(redisClient.set).bind(redisClient);

// Act
const setResult = await setAsync('test', 73);

// Assert
expect(setResult).toEqual('OK');
});

it('should read the written value correctly', async () => {
// Arrange
const getAsync = promisify(redisClient.get).bind(redisClient);

// Act
const getResult = await getAsync('test');

// Assert
expect(getResult).toEqual('73');
});
});
7 changes: 7 additions & 0 deletions examples/03-docker-compose/jest-testcontainers-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
dockerCompose: {
composeFilePath: ".",
composeFile: "docker-compose.yml",
startupTimeout: 10000,
}
};
5 changes: 5 additions & 0 deletions examples/03-docker-compose/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
// preset: "@trendyol/jest-testcontainers", // should be like this in your project
preset: "../../jest-preset.js"
// make sure you are not setting *testEnvironment* to something (e.g. node). it should not be present.
};
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ Example usages of the library.
Simple usage of the library with plain Javascript. Starts `redis:latest` and runs set/get queries on it.
## 02-typescript-redis
Simple usage of the library with Typescript. Starts `redis:5.0.5` and runs set/get queries on it.
## 03-docker-compose
Simple usage of the library and the built in ability to build Docker containers using Docker Compose.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"style:fix": "npm run lint && npm run prettier",
"prepublish": "npm run build",
"example:redis": "cd ./examples/01-basic-redis && jest",
"example:redis-typescript": "cd ./examples/02-typescript-redis && jest"
"example:redis-typescript": "cd ./examples/02-typescript-redis && jest",
"example:docker-compose": "cd ./examples/03-docker-compose && jest"
},
"lint-staged": {
"*.ts": [
Expand Down
48 changes: 48 additions & 0 deletions src/config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,54 @@ describe("config", () => {
expect(actualConfig).toEqual(expectedConfig);
});

it("should parse to docker compose options correctly", () => {
// Arrange
const objInput: any = {
dockerCompose: {
composeFilePath: ".",
composeFile: "docker-compose.yml",
startupTimeout: 1000
}
};
const expectedConfig: JestTestcontainersConfig = {
dockerCompose: {
composeFilePath: ".",
composeFile: "docker-compose.yml",
startupTimeout: 1000
}
};

// Act
const actualConfig = parseConfig(objInput);

// Assert
expect(actualConfig).toEqual(expectedConfig);
});

it("should throw when trying to combine dockerCompose with other options", () => {
// Arrange
const objInput: any = {
dockerCompose: {
composeFilePath: ".",
composeFile: "docker-compose.yml",
startupTimeout: 1000
},
first: {
image: "first",
wait: {
text: "hello",
type: "text"
}
}
};

// Act
const expectResult = expect(() => parseConfig(objInput));

// Assert
expectResult.toThrow();
});

it("empty config should throw", () => {
// Arrange
const objInput: any = {};
Expand Down
27 changes: 25 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,23 @@ class JestTestcontainersConfigError extends Error {
export type EnvironmentVariableMap = { [key: string]: string };
export type WaitConfig = PortsWaitConfig | TextWaitConfig;

export interface JestTestcontainersConfig {
export type DockerComposeConfig = {
composeFilePath: string;
composeFile: string;
startupTimeout?: number;
};

type DockerComposeContainersConfig = {
dockerCompose?: DockerComposeConfig;
};

type MultipleContainerConfig = {
[key: string]: SingleContainerConfig;
}
};

export type JestTestcontainersConfig =
| DockerComposeContainersConfig
| MultipleContainerConfig;

export interface SingleContainerConfig {
image: string;
Expand Down Expand Up @@ -186,6 +200,15 @@ export function parseConfig(containerConfigs: any) {
);
}

if ("dockerCompose" in containerConfigs) {
if (Object.keys(containerConfigs).length !== 1) {
throw new JestTestcontainersConfigError(
"testcontainers config cannot contain other images when using 'dockerCompose' option"
);
}
return containerConfigs;
}

return Object.keys(containerConfigs).reduce(
(acc, key) => ({
...acc,
Expand Down
138 changes: 136 additions & 2 deletions src/containers.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { Duration, TemporalUnit } from "node-duration";
import { Wait } from "testcontainers";
import { DockerComposeEnvironment, Wait } from "testcontainers";
import {
StartedTestContainer,
TestContainer
} from "testcontainers/dist/test-container";
import { JestTestcontainersConfig, SingleContainerConfig } from "./config";
import {
DockerComposeConfig,
JestTestcontainersConfig,
SingleContainerConfig
} from "./config";
import {
AllStartedContainersAndMetaInfo,
buildDockerComposeEnvironment,
buildTestcontainer,
getMetaInfo,
startAllContainers,
startContainer,
startDockerComposeContainers,
StartedContainerAndMetaInfo
} from "./containers";

Expand Down Expand Up @@ -225,6 +231,47 @@ describe("containers", () => {
});
});

describe("buildDockerComposeEnvironment", () => {
it("should create simple docker compose environment", () => {
// Arrange
const dockerComposeConfig: DockerComposeConfig = {
composeFilePath: ".",
composeFile: "docker-compose.yml"
};
const nameRegex = new RegExp(/testcontainers-[0-9A-F]{32}/i);

// Act
const actualEnvironment: any = buildDockerComposeEnvironment(
dockerComposeConfig
);

// Assert
expect(actualEnvironment.projectName).toEqual(
expect.stringMatching(nameRegex)
);
});

it("should set startup timeout correctly", () => {
// Arrange
const dockerComposeConfig: DockerComposeConfig = {
composeFilePath: ".",
composeFile: "docker-compose.yml",
startupTimeout: 60000
};
const nameRegex = new RegExp(/testcontainers-[0-9A-F]{32}/i);

// Act
const actualEnvironment: any = buildDockerComposeEnvironment(
dockerComposeConfig
);

// Assert
expect(actualEnvironment.startupTimeout).toEqual(
new Duration(60000, TemporalUnit.MILLISECONDS)
);
});
});

describe("getMetaInfo", () => {
it("should work with no ports", () => {
// Arrange
Expand Down Expand Up @@ -340,6 +387,58 @@ describe("containers", () => {
});
});

describe("startDockerComposeContainers", () => {
it("should call builder and getter functions", async () => {
// Arrange
const ports = [1];
const boundPorts = new Map<number, number>([[1, 2]]);
const startedContainer = ({
containerName: "container-name",
boundPorts: {
ports: boundPorts
}
} as unknown) as StartedTestContainer;
const containerMeta: StartedContainerAndMetaInfo = {
container: startedContainer,
ip: "localhost",
name: "container-name",
portMappings: boundPorts
};
const environment: DockerComposeEnvironment = ({
up: jest.fn(() =>
Promise.resolve({
startedGenericContainers: {
"container-name": startedContainer
}
})
)
} as unknown) as DockerComposeEnvironment;
const dockerComposeBuilderFn: any = jest.fn(() => environment);
const expectedMetaResult: AllStartedContainersAndMetaInfo = {
"container-name": containerMeta
};
const getMetaInfoFn: any = jest.fn(() => containerMeta);
const dockerComposeConfig: DockerComposeConfig = {
composeFilePath: ".",
composeFile: "docker-compose.yml",
startupTimeout: 1000
};

// Act
const actualMetaResult = await startDockerComposeContainers(
dockerComposeConfig,
dockerComposeBuilderFn,
getMetaInfoFn
);

// Assert
expect(actualMetaResult).toEqual(expectedMetaResult);
expect(getMetaInfoFn).toHaveBeenCalledWith(startedContainer, ports);
expect(environment.up).toHaveBeenCalledWith();
expect(dockerComposeBuilderFn).toHaveBeenCalledWith(dockerComposeConfig);
});
});

describe("startAllContainers", () => {
it("should call starter function", async () => {
// Arrange
Expand Down Expand Up @@ -379,5 +478,40 @@ describe("containers", () => {
expect(startContainerFn).toHaveBeenCalledWith(config.rabbit);
expect(startContainerFn).toHaveBeenCalledWith(config.redis);
});

it("should call docker compose starter function", async () => {
// Arrange
const config: JestTestcontainersConfig = {
dockerCompose: {
composeFilePath: ".",
composeFile: "docker-compose.yml"
}
};
const container = (null as unknown) as StartedTestContainer;
const redisPortMappings = new Map<number, number>([[1, 2]]);
const infos: AllStartedContainersAndMetaInfo = {
redis: {
name: "redis",
container,
ip: "localhost",
portMappings: redisPortMappings
}
};
const startContainerFn: any = jest.fn();
const startDockerComposeContainersFn: any = jest.fn(() => infos);

// Act
const allStartedContainerAndMetaInfo = await startAllContainers(
config,
startContainerFn,
startDockerComposeContainersFn
);

// Assert
expect(allStartedContainerAndMetaInfo).toEqual(infos);
expect(startDockerComposeContainersFn).toHaveBeenCalledWith(
config.dockerCompose
);
});
});
});
Loading

0 comments on commit e923c24

Please sign in to comment.