Running integration tests using Rell and TypeScript
Integration testing is a vital part of the test pyramid, mainly focusing on ensuring the frontend works with the actual backend. In this guide, we will set up a simple test harness in TypeScript for integration tests that communicate with our rell code. We will start a Rell test node and perform a query and transaction towards it.
Setup
We start by setting up a new project as follows:
# Create a new project and navigate to it
chr create-rell-dapp rell-it
cd rell-it
# Set up a TypeScript project
npm init --yes
npm install postchain-client
npm install typescript jest @types/jest ts-jest testcontainers @testcontainers/postgresql --save-dev
npx tsc --init
npx ts-jest config:init
We will use jest test framework and testcontainers for the test harness and the postchain-client for communicating with rell.
You will now have a source folder with main.rell
containing a query and an operation, which we will use in our integration test.
Integration test using Jest and testcontainers
Now let's create our first integration test, src/main.test.ts
.
import { cwd } from "process";
import { IClient, createClient } from "postchain-client";
import {
GenericContainer,
Network,
StartedNetwork,
StartedTestContainer,
Wait,
} from "testcontainers";
import {
PostgreSqlContainer,
StartedPostgreSqlContainer,
} from "@testcontainers/postgresql";
// Define the test suite
describe("Rell Integration Tests", () => {
let network: StartedNetwork;
let postgres: StartedPostgreSqlContainer;
let container: StartedTestContainer;
let client: IClient;
// Set up PostgreSQL container and Chromia node container
beforeAll(async () => {
// Start a new network for containers
network = await new Network().start();
// Start a PostgreSQL container
postgres = await new PostgreSqlContainer("postgres:14.9-alpine3.18")
.withNetwork(network)
.withExposedPorts(5432)
.withDatabase("postchain")
.withPassword("postchain")
.withUsername("postchain")
.withNetworkAliases("postgres")
.start();
// Start a Chromia node container
container = await new GenericContainer(
"registry.gitlab.com/chromaway/core-tools/chromia-cli/chr:latest"
)
.withNetwork(network)
.withCopyDirectoriesToContainer([{ source: cwd(), target: "/usr/app" }])
.withExposedPorts(7740)
.withEnvironment({
CHR_DB_URL: "jdbc:postgresql://postgres/postchain",
})
.withCommand(["chr", "node", "start", "--wipe"])
.withWaitStrategy(Wait.forLogMessage("Blockchain has been started"))
.withStartupTimeout(60000)
.start();
// Create a Postchain client for communication
client = await createClient({
blockchainIid: 0,
nodeUrlPool: "http://localhost:" + container.getMappedPort(7740),
});
}, 30000);
// Stop containers after all tests are complete
afterAll(async () => {
await container.stop();
await postgres.stop();
await network.stop();
});
// Integration tests
it("Can update and query dapp", async () => {
// Verify initial query result
expect(await client.query("hello_world")).toBe("Hello World!");
// Send a transaction to update the dapp
expect(
(await client.sendTransaction({ name: "set_name", args: ["Joe"] }))
.statusCode
).toBe(200);
// Verify the updated query result
expect(await client.query("hello_world")).toBe("Hello Joe!");
});
});
The test file uses a beforeAll
section to start up two containers in the same docker network. The first is a Postgres container where we configure database and user names to the default values of chromia-cli.
We then start a chromia container where we copy the project's source files to the /usr/app
folder of the container. We can copy the entire working directory, but your folder structure might benefit from being more explicit here. We export the rest API port 7740 and configure the CHR_DB_URL
environment variable to override whatever is configured in chromia.yml
. Finally, we update the container's command to chr node start --wipe
to start the test node.
It is not always best to initialize the containers in beforeAll
. In some cases, it might be better to restart the Chromia node before each test case, in which case beforeEach
should be used. The PostgreSQL container can typically persist between test cases, so here, beforeAll
is correct.
We finalize the test startup with
client = await createClient({
blockchainIid: 0,
nodeUrlPool: "http://localhost:" + container.getMappedPort(7740),
});
configuring the nodeUrlPool
to point to port 7740, which is mapped internally to some other port and ensured by the getMappedPort
call.
We can now add the actual tests as follows:
// Perform integration tests
it("Can update and query dapp", async () => {
// Verify initial query result
expect(await client.query("hello_world")).toBe("Hello World!");
// Send a transaction to update the dapp
expect(
(await client.sendTransaction({ name: "set_name", args: ["Joe"] }))
.statusCode
).toBe(200);
// Verify the updated query result
expect(await client.query("hello_world")).toBe("Hello Joe!");
});
This test does a query and a transaction towards the node and verifies the results.
We can now run the tests using npx jest
:
$ npx jest
PASS src/main.test.ts (11.855 s)
Rell integration tests
✓ Can update and query dapp (1659 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.906 s
Ran all test suites.
When running the integration tests in a continuous integration environment, you must make sure the docker daemon is available:
- GitLab CI
- Bitbucket Pipelines
# DinD service is required for Testcontainers
services:
- name: docker:dind
# explicitly disable tls to avoid docker startup interruption
command: ["--tls=false"]
variables:
# Instruct Testcontainers to use the daemon of DinD. Use port 2375 for non-tls connections.
DOCKER_HOST: "tcp://docker:2375"
# Instruct Docker not to start over TLS.
DOCKER_TLS_CERTDIR: ""
# Improve performance with overlayfs.
DOCKER_DRIVER: overlay2
image: node
pipelines:
default:
- step:
script:
- export TESTCONTAINERS_RYUK_DISABLED=true
- npx jest
services:
- docker
definitions:
services:
docker:
memory: 2048