testcontainers_modules/clickhouse/
mod.rs

1use std::{borrow::Cow, collections::BTreeMap};
2
3use testcontainers::{
4    core::{wait::HttpWaitStrategy, ContainerPort, WaitFor},
5    Image,
6};
7
8const DEFAULT_IMAGE_NAME: &str = "clickhouse/clickhouse-server";
9const DEFAULT_IMAGE_TAG: &str = "23.3.8.21-alpine";
10
11/// Port that the [`ClickHouse`] container has internally
12/// Can be rebound externally via [`testcontainers::core::ImageExt::with_mapped_port`]
13///
14/// [`ClickHouse`]: https://clickhouse.com/
15pub const CLICKHOUSE_PORT: ContainerPort = ContainerPort::Tcp(8123);
16
17/// Module to work with [`ClickHouse`] inside of tests.
18///
19/// This module is based on the official [`ClickHouse docker image`].
20///
21/// # Example
22/// ```
23/// use testcontainers_modules::{clickhouse, testcontainers::runners::SyncRunner};
24///
25/// let clickhouse = clickhouse::ClickHouse::default().start().unwrap();
26/// let http_port = clickhouse.get_host_port_ipv4(8123).unwrap();
27///
28/// // do something with the started clickhouse instance..
29/// ```
30///
31/// [`ClickHouse`]: https://clickhouse.com/
32/// [`Clickhouse docker image`]: https://hub.docker.com/r/clickhouse/clickhouse-server
33#[derive(Debug, Default, Clone)]
34pub struct ClickHouse {
35    env_vars: BTreeMap<String, String>,
36}
37
38impl Image for ClickHouse {
39    fn name(&self) -> &str {
40        DEFAULT_IMAGE_NAME
41    }
42
43    fn tag(&self) -> &str {
44        DEFAULT_IMAGE_TAG
45    }
46
47    fn ready_conditions(&self) -> Vec<WaitFor> {
48        vec![WaitFor::http(
49            HttpWaitStrategy::new("/").with_expected_status_code(200_u16),
50        )]
51    }
52
53    fn env_vars(
54        &self,
55    ) -> impl IntoIterator<Item = (impl Into<Cow<'_, str>>, impl Into<Cow<'_, str>>)> {
56        &self.env_vars
57    }
58
59    fn expose_ports(&self) -> &[ContainerPort] {
60        &[CLICKHOUSE_PORT]
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use clickhouse::Row;
67    use reqwest::Client;
68    use serde::Deserialize;
69
70    use crate::{clickhouse::ClickHouse as ClickhouseImage, testcontainers::runners::AsyncRunner};
71
72    #[tokio::test]
73    async fn clickhouse_db() -> Result<(), Box<dyn std::error::Error + 'static>> {
74        let clickhouse = ClickhouseImage::default();
75        let node = clickhouse.start().await?;
76
77        let host = node.get_host().await?;
78        let port = node.get_host_port_ipv4(8123).await?;
79        let url = format!("http://{}:{}", host, port);
80
81        // testing http endpoint
82        // curl http://localhost:8123/ping and check if the response is "Ok."
83        let response = Client::new().get(format!("{}/ping", url)).send().await?;
84        assert_eq!(response.status(), 200);
85
86        // create table
87        let query = "CREATE TABLE t (a UInt8) ENGINE = Memory";
88        let response = Client::new().post(url.clone()).body(query).send().await?;
89        assert_eq!(response.status(), 200);
90
91        // insert data
92        let query = "INSERT INTO t VALUES (1),(2),(3)";
93        let response = Client::new().post(url.clone()).body(query).send().await?;
94        assert_eq!(response.status(), 200);
95
96        // query data
97        let query = "SELECT * FROM t";
98        let response = Client::new().post(url.clone()).body(query).send().await?;
99        assert_eq!(response.status(), 200);
100
101        let client = clickhouse::Client::default().with_url(format!("http://{host}:{port}"));
102        #[derive(Row, Deserialize)]
103        struct MyRow {
104            #[serde(rename = "a")] // we don't read the field, so it's a dead-code in tests
105            _a: u8,
106        }
107        let rows = client.query("SELECT * FROM t").fetch_all::<MyRow>().await?;
108        assert_eq!(rows.len(), 3);
109
110        Ok(())
111    }
112}