ClickHouse module for Testcontainers that enables developers to programmatically create, configure, and manage ClickHouse Docker containers for testing
ClickHouse module for Testcontainers that enables developers to programmatically create, configure, and manage ClickHouse Docker containers for testing purposes. The module offers a fluent API for container configuration, exposes both native and HTTP interfaces, and integrates seamlessly with the official @clickhouse/client library.
Required Dependencies:
@testcontainers/clickhouse (this package)@testcontainers/core is required (provided transitively)testcontainers package is required (provided transitively)@clickhouse/client is recommended for connecting to the container (not required by testcontainers itself)Default Behaviors:
Threading Model:
start() method is async and must be awaitedstop() method is async and must be awaited@clickhouse/client are independent of container lifecycleLifecycle:
await container.start() before usestart() resolvesawait container.stop() for cleanupExceptions:
ContainerStartException - Container failed to start (timeout, image pull failure, etc.)PortBindingException - Port binding conflictsDockerNotAvailableException - Docker daemon not accessibleImagePullException - Failed to pull Docker image@clickhouse/client - Connection failures, query errorsTimeoutError - Health check timeout (container started but not ready)Edge Cases:
withStartupTimeout())withReuse(true) to reuse containers across test runs (requires testcontainers configuration)withNetworkMode() for multi-container testsnpm install --save-dev @testcontainers/clickhouseFor production dependencies (if needed):
npm install @testcontainers/clickhouseimport { ClickHouseContainer, StartedClickHouseContainer } from "@testcontainers/clickhouse";For CommonJS:
const { ClickHouseContainer, StartedClickHouseContainer } = require("@testcontainers/clickhouse");Note: StartedClickHouseContainer is typically obtained by calling start() on a ClickHouseContainer instance, but the type is exported for type annotations.
import { ClickHouseContainer } from "@testcontainers/clickhouse";
import { createClient } from "@clickhouse/client";
// Start a ClickHouse container
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
// Connect using the built-in client options
const client = createClient(container.getClientOptions());
// Execute queries
const result = await client.query({
query: "SELECT 1 AS value",
format: "JSON",
});
// Clean up
await client.close();
await container.stop();import { ClickHouseContainer } from "@testcontainers/clickhouse";
import { createClient } from "@clickhouse/client";
let container: StartedClickHouseContainer | null = null;
let client: any = null;
try {
container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
client = createClient(container.getClientOptions());
const result = await client.query({
query: "SELECT 1 AS value",
format: "JSON",
});
console.log(result.json());
} catch (error) {
console.error("Test failed:", error);
throw error;
} finally {
if (client) {
await client.close();
}
if (container) {
await container.stop();
}
}import { ClickHouseContainer } from "@testcontainers/clickhouse";
import { createClient } from "@clickhouse/client";
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:23.8")
.withUsername("myuser")
.withPassword("mypassword")
.withDatabase("mydb")
.start();
const client = createClient(container.getClientOptions());
// Use the container...
await client.close();
await container.stop();The module provides two main classes:
GenericContainer to provide ClickHouse-specific configuration methods.The module automatically:
Configure and start ClickHouse containers with custom credentials and database settings.
/**
* Main container class for creating and configuring ClickHouse test containers.
* Extends GenericContainer from the testcontainers package.
*/
class ClickHouseContainer extends GenericContainer {
/**
* Creates a new ClickHouse container instance
* @param image - Docker image name (e.g., "clickhouse/clickhouse-server:latest")
*/
constructor(image: string);
/**
* Sets the database name to create and use
* @param database - Database name (default: "test")
* @returns This container instance for method chaining
*/
withDatabase(database: string): this;
/**
* Sets the username for authentication
* @param username - Username (default: "test")
* @returns This container instance for method chaining
*/
withUsername(username: string): this;
/**
* Sets the password for authentication
* @param password - Password (default: "test")
* @returns This container instance for method chaining
*/
withPassword(password: string): this;
/**
* Starts the container and returns a started instance
* @returns Promise resolving to a StartedClickHouseContainer
* @throws ContainerStartException if container fails to start
* @throws TimeoutError if health check times out
*/
start(): Promise<StartedClickHouseContainer>;
}Usage Examples:
import { ClickHouseContainer } from "@testcontainers/clickhouse";
// Basic usage with defaults (username: "test", password: "test", database: "test")
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
// Custom configuration
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:23.8")
.withUsername("myuser")
.withPassword("mypassword")
.withDatabase("mydb")
.start();
// With specific image version
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:23.8.23.12.1")
.withDatabase("production_test")
.start();Retrieve connection details and configuration objects for connecting to the running ClickHouse container.
/**
* Represents a running ClickHouse container with methods to retrieve connection details.
* Extends AbstractStartedContainer from the testcontainers package.
*/
class StartedClickHouseContainer extends AbstractStartedContainer {
/**
* Returns the mapped host port for the native ClickHouse protocol
* @returns Port number for native protocol (internal port 9000)
*/
getPort(): number;
/**
* Returns the mapped host port for the HTTP interface
* @returns Port number for HTTP interface (internal port 8123)
*/
getHttpPort(): number;
/**
* Returns the configured username
* @returns Username string
*/
getUsername(): string;
/**
* Returns the configured password
* @returns Password string
*/
getPassword(): string;
/**
* Returns the configured database name
* @returns Database name string
*/
getDatabase(): string;
/**
* Gets the base HTTP URL (protocol, host and mapped port) for the ClickHouse container's HTTP interface
* @returns HTTP URL string (e.g., "http://localhost:32768")
*/
getHttpUrl(): string;
/**
* Gets configuration options suitable for passing directly to createClient() from @clickhouse/client
* Uses the HTTP interface. This is the recommended method for connecting.
* @returns Configuration object with url, username, password, and database
*/
getClientOptions(): ClientOptions;
/**
* Gets a ClickHouse connection URL for the HTTP interface with embedded credentials
* Format: http://username:password@hostname:port/database
* @returns Connection URL string
*/
getConnectionUrl(): string;
/**
* Stops the container and cleans up resources
* @returns Promise that resolves when container is stopped
*/
stop(): Promise<void>;
/**
* Restarts the container
* @returns Promise that resolves when container is restarted
*/
restart(): Promise<void>;
/**
* Gets container logs
* @returns Promise resolving to log stream or string
*/
logs(): Promise<string>;
/**
* Executes a command in the running container
* @param command - Command to execute
* @returns Promise resolving to command execution result
*/
exec(command: string[]): Promise<ExecResult>;
}Usage Examples:
import { ClickHouseContainer } from "@testcontainers/clickhouse";
import { createClient } from "@clickhouse/client";
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withUsername("customuser")
.withPassword("custompass")
.withDatabase("testdb")
.start();
// Method 1: Using getClientOptions() (recommended)
const client1 = createClient(container.getClientOptions());
// Method 2: Using getConnectionUrl()
const client2 = createClient({
url: container.getConnectionUrl(),
});
// Method 3: Using individual getters
const client3 = createClient({
url: container.getHttpUrl(),
username: container.getUsername(),
password: container.getPassword(),
database: container.getDatabase(),
});
// Get port information
console.log(`Native protocol port: ${container.getPort()}`);
console.log(`HTTP interface port: ${container.getHttpPort()}`);
// Execute queries
const result = await client1.query({
query: "SELECT version()",
format: "JSON",
});
await client1.close();
await container.stop();The container extends GenericContainer, providing access to all Testcontainers configuration methods.
Environment Variables:
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withEnvironment("CLICKHOUSE_DB", "customdb")
.withEnvironment("CLICKHOUSE_USER", "customuser")
.withEnvironment("CLICKHOUSE_PASSWORD", "custompass")
.withEnvironment("CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT", "1")
.start();Custom Ports:
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withExposedPorts(9000, 8123, 9009) // Native, HTTP, Inter-Server
.start();
// Get specific port mappings
const nativePort = container.getMappedPort(9000);
const httpPort = container.getMappedPort(8123);
const interServerPort = container.getMappedPort(9009);Startup Timeout:
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withStartupTimeout(Duration.ofSeconds(300)) // 5 minutes for slow environments
.start();Container Reuse:
// Enable container reuse (requires testcontainers configuration)
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withReuse(true)
.start();File System Operations:
// Copy files into container
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withCopyFilesToContainer([
{
source: "./config.xml",
target: "/etc/clickhouse-server/config.d/custom-config.xml",
},
])
.start();
// Bind mount host directory
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withBindMounts([
{
source: "./data",
target: "/var/lib/clickhouse/data",
},
])
.start();Network Configuration:
// Use host network mode
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withNetworkMode("host")
.start();
// Create custom network for multi-container tests
import { Network } from "testcontainers";
const network = await Network.newNetwork().start();
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withNetwork(network)
.start();Resource Limits:
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withMemory(2 * 1024 * 1024 * 1024) // 2GB
.withCpuCount(2)
.start();Proper Cleanup Pattern:
import { ClickHouseContainer } from "@testcontainers/clickhouse";
let container: StartedClickHouseContainer | null = null;
try {
container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
// Use container...
} finally {
if (container) {
await container.stop();
}
}With Test Framework Integration (Jest):
import { ClickHouseContainer } from "@testcontainers/clickhouse";
describe("ClickHouse Tests", () => {
let container: StartedClickHouseContainer;
beforeAll(async () => {
container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
});
afterAll(async () => {
await container.stop();
});
it("should execute queries", async () => {
const client = createClient(container.getClientOptions());
// Test code...
await client.close();
});
});With Test Framework Integration (Mocha):
import { ClickHouseContainer } from "@testcontainers/clickhouse";
describe("ClickHouse Tests", function () {
let container: StartedClickHouseContainer;
before(async function () {
container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
});
after(async function () {
await container.stop();
});
it("should execute queries", async function () {
const client = createClient(container.getClientOptions());
// Test code...
await client.close();
});
});Container Restart:
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
// Use container...
await container.restart(); // Restart the container
// Container is ready again after restartStartup Failures:
import { ClickHouseContainer } from "@testcontainers/clickhouse";
try {
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
} catch (error) {
if (error instanceof ContainerStartException) {
console.error("Container failed to start:", error.message);
// Check Docker daemon, image availability, port conflicts
} else if (error instanceof TimeoutError) {
console.error("Container health check timed out");
// Increase startup timeout or check container logs
} else {
console.error("Unexpected error:", error);
}
throw error;
}Connection Failures:
import { createClient } from "@clickhouse/client";
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
try {
const client = createClient(container.getClientOptions());
// Wait for container to be fully ready
await new Promise((resolve) => setTimeout(resolve, 1000));
const result = await client.query({
query: "SELECT 1",
format: "JSON",
});
} catch (error) {
if (error.code === "ECONNREFUSED") {
console.error("Connection refused - container may not be ready");
} else if (error.code === "ETIMEDOUT") {
console.error("Connection timeout");
} else {
console.error("Query error:", error);
}
throw error;
}Query Errors:
try {
const result = await client.query({
query: "SELECT * FROM non_existent_table",
format: "JSON",
});
} catch (error) {
if (error.code === 60) {
// ClickHouse error code 60: UNKNOWN_TABLE
console.error("Table does not exist");
} else {
console.error("Query failed:", error.message);
}
}Parallel Test Execution:
import { ClickHouseContainer } from "@testcontainers/clickhouse";
// Each test gets its own container
const test1 = async () => {
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
// Test code...
await container.stop();
};
const test2 = async () => {
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
// Test code...
await container.stop();
};
// Run in parallel - each gets unique ports
await Promise.all([test1(), test2()]);Multi-Container Scenarios:
import { ClickHouseContainer } from "@testcontainers/clickhouse";
import { Network } from "testcontainers";
// Create shared network
const network = await Network.newNetwork().start();
try {
// Create multiple containers on same network
const container1 = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withNetwork(network)
.withNetworkAliases("clickhouse1")
.start();
const container2 = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withNetwork(network)
.withNetworkAliases("clickhouse2")
.start();
// Containers can communicate using network aliases
// Use container1.getHost() and container2.getHost() for connections
} finally {
await container1?.stop();
await container2?.stop();
await network.stop();
}Custom Health Check:
import { ClickHouseContainer } from "@testcontainers/clickhouse";
import { Wait } from "testcontainers";
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withWaitStrategy(
Wait.forHttp("/ping", 8123)
.forStatusCode(200)
.withStartupTimeout(Duration.ofSeconds(300))
)
.start();Verifying Container Readiness:
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
// Container is ready when start() resolves
// Additional verification if needed:
const client = createClient(container.getClientOptions());
// Simple readiness check
try {
await client.ping();
console.log("Container is ready");
} catch (error) {
console.error("Container not ready:", error);
}Connection Pooling:
import { createClient } from "@clickhouse/client";
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest").start();
// Create client with connection pooling
const client = createClient({
...container.getClientOptions(),
max_open_connections: 10,
request_timeout: 30000,
});
// Reuse client across multiple queries
for (let i = 0; i < 100; i++) {
await client.query({
query: `INSERT INTO test_table VALUES (${i})`,
});
}
await client.close();
await container.stop();Batch Operations:
const client = createClient(container.getClientOptions());
// Batch insert
const values = Array.from({ length: 1000 }, (_, i) => `(${i}, 'value${i}')`).join(",");
await client.exec({
query: `INSERT INTO test_table VALUES ${values}`,
});
// Or use insert method for structured data
await client.insert({
table: "test_table",
values: Array.from({ length: 1000 }, (_, i) => ({ id: i, value: `value${i}` })),
format: "JSONEachRow",
});Resource Management:
// Use ulimits for high-load tests (already set by default, but can be customized)
const container = await new ClickHouseContainer("clickhouse/clickhouse-server:latest")
.withCreateContainerCmdModifier((cmd) => {
cmd.getHostConfig().withUlimits([
{
name: "nofile",
soft: 262144,
hard: 262144,
},
]);
})
.start();/**
* Configuration options returned by getClientOptions() for use with @clickhouse/client
*/
interface ClientOptions {
/** Base HTTP URL for the ClickHouse container */
url?: string;
/** Username for authentication */
username: string;
/** Password for authentication */
password: string;
/** Database name to use */
database: string;
}Both ClickHouseContainer and StartedClickHouseContainer inherit from Testcontainers base classes, providing additional functionality:
From GenericContainer (available on ClickHouseContainer):
withEnvironment(key, value): Set environment variableswithExposedPorts(...ports): Expose additional portswithCopyFilesToContainer(files): Copy files into the containerwithBindMounts(mounts): Mount host directorieswithNetworkMode(mode): Configure network modewithNetwork(network): Attach to a networkwithNetworkAliases(...aliases): Set network aliaseswithStartupTimeout(duration): Configure startup timeoutwithWaitStrategy(strategy): Configure wait strategywithReuse(reuse): Enable container reusewithMemory(bytes): Set memory limitwithCpuCount(count): Set CPU countwithCreateContainerCmdModifier(modifier): Customize container creationFrom AbstractStartedContainer (available on StartedClickHouseContainer):
stop(): Stop the containerrestart(): Restart the containerlogs(): Get container logsexec(command): Execute commands in the containergetHost(): Get the container hostgetMappedPort(internalPort): Get mapped port for any exposed portgetContainerId(): Get Docker container IDThe module applies the following default configuration:
These defaults can be overridden using the withDatabase(), withUsername(), and withPassword() methods, or by using inherited Testcontainers methods for more advanced configuration.
Container Won't Start:
docker psawait container.logs()Connection Refused:
await container.start() completed successfullycontainer.getHttpUrl() and container.getPort()Health Check Timeout:
.withStartupTimeout(Duration.ofSeconds(300))Port Conflicts in CI/CD:
Resource Exhaustion:
docker ps -adocker container pruneImage Pull Failures:
docker pull clickhouse/clickhouse-server:latestInstall with Tessl CLI
npx tessl i tessl/npm-testcontainers--clickhouse