Skip to main content

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.

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.

note

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.
tip

When running the integration tests in a continuous integration environment, you must make sure the docker daemon is available:

# 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