ClickHouse module for Testcontainers that enables developers to programmatically create, configure, and manage ClickHouse Docker containers for testing
npx @tessl/cli install tessl/npm-testcontainers--clickhouse@11.9.0ClickHouse 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:latest