Hi! 👋

Thomas Eizinger

CoBloX Research Lab

Difference between:

  • Mock
  • Stub
  • Fake
  • Spy
  • Dummy

Isolating Dependencies

Boundaries?

  • Databases?
  • Blockchain nodes?
  • ...

Docker to the rescue!

github.com/testcontainers/

testcontainers-rs

Why?

  • COMIT protocol @ coblox.tech
  • need to test smart contracts

How it started.

let endpoint = env!("BITCOIN_RPC_ENDPOINT");
let username = env!("BITCOIN_RPC_USERNAME");
let password = env!("BITCOIN_RPC_PASSWORD");
  • very brittle
  • manual starting/stopping
  • concurrency-issues

Good tests

  • are independent
  • are short
  • test a single thing

Showcase!

Show us the code!

pub trait Docker
where
    Self: Sized,
{
    fn run<I: Image>(&self, image: I) -> Container<Self, I>;
    fn logs(&self, id: &str) -> Logs;
    fn ports(&self, id: &str) -> Ports;
    fn rm(&self, id: &str);
    fn stop(&self, id: &str);
}
pub trait Image
{
    type Args;
    type EnvVars;

    fn descriptor(&self) -> String;
    fn wait_until_ready<D: Docker>(&self, container: &Container<D, Self>);
    fn args(&self) -> Self::Args;
    fn env_vars(&self) -> Self::EnvVars;
    fn with_args(self, arguments: Self::Args) -> Self;
}
#[derive(Debug)]
pub struct Container<'d, D, I>
where
    D: 'd,
    D: Docker,
    I: Image,
{
    id: String,
    docker_client: &'d D,
    image: I,
}

Key features

Waiting until a container is ready...

impl Image for ParityEthereum {
    type Args = ParityEthereumArgs;
    type EnvVars = HashMap<String, String>;

    fn wait_until_ready<D: Docker>(&self, container: &Container<D, Self>) {
        container
            .logs()
            .stderr
            .wait_for_message("Public node URL:")
            .unwrap();
    }


    fn ...
}
#[test]
fn parity_parity_listaccounts() {
    let _ = pretty_env_logger::try_init();
    let docker = clients::Cli::default();
    let node = docker.run(images::parity_parity::ParityEthereum::default());

    let (_event_loop, web3) = {
        let host_port = node.get_host_port(8545).unwrap();

        let url = format!("http://localhost:{}", host_port);

        let (_event_loop, transport) = Http::new(&url).unwrap();
        let web3 = Web3::new(transport);

        (_event_loop, web3)
    };

    let accounts = web3.eth().accounts().wait();

    assert_that(&accounts).is_ok();
}
  1. Create container
  2. wait_until_ready
  3. return

Remove container after use

impl<'d, D, I> Drop for Container<'d, D, I>
where
    D: Docker,
    I: Image,
{
    fn drop(&mut self) {
        self.rm()
    }
}
#[test]
fn parity_parity_listaccounts() {
    let _ = pretty_env_logger::try_init();
    let docker = clients::Cli::default();
    let node = docker.run(images::parity_parity::ParityEthereum::default());

    let (_event_loop, web3) = {
        let host_port = node.get_host_port(8545).unwrap();

        let url = format!("http://localhost:{}", host_port);

        let (_event_loop, transport) = Http::new(&url).unwrap();
        let web3 = Web3::new(transport);

        (_event_loop, web3)
    };

    let accounts = web3.eth().accounts().wait();

    assert_that(&accounts).is_ok();
}

Extendability

  • More images
  • Different wait strategies
  • Different docker clients (CLI, HTTP)

Learnings

Keep it simple!

/// Implementation of the Docker client API using the docker cli.
///
/// This (fairly naive) implementation of the Docker client API 
/// simply creates `Command`s to the `docker` CLI.
/// It thereby assumes that the `docker` CLI is installed and 
/// that it is in the PATH of the current execution environment.
#[derive(Debug, Default)]
pub struct Cli {
    ...
}
impl Docker for Cli {
    fn run<I: Image>(&self, image: I) -> Container<Cli, I> {
        let mut command = Command::new("docker");

        let command = {
            command.arg("run");

            for (key, value) in image.env_vars() {
                command.arg("-e").arg(format!("{}={}", key, value));
            }

            command
                .arg("-d") // Always run detached
                .arg("-P") // Always expose all ports
                .arg(image.descriptor())
                .args(image.args())
                .stdout(Stdio::piped())
        };

        debug!("Executing command: {:?}", command);

        let child = command.spawn().expect("Failed to execute docker command");

        let stdout = child.stdout.unwrap();
        let reader = BufReader::new(stdout);

        let container_id = reader.lines().next().unwrap().unwrap();
        
        Container::new(container_id, self, image)
    }

    ...
}

Planned features

  • Utilize Rust's lifetimes
  • HTTP client

Utilize Lifetimes

#[test]
fn parity_parity_listaccounts() {
    let _ = pretty_env_logger::try_init();
    let docker = clients::Cli::default();
    let node = docker.run(images::parity_parity::ParityEthereum::default());

    let (_event_loop, web3) = {
        let host_port = node.get_host_port(8545).unwrap();

        let url = format!("http://localhost:{}", host_port);

        let (_event_loop, transport) = Http::new(&url).unwrap();
        let web3 = Web3::new(transport);

        (_event_loop, web3)
    };

    let accounts = web3.eth().accounts().wait();

    assert_that(&accounts).is_ok();
}

What if we move this guy to another thread?

`node.drop()`

We are hiring!

  1. Full-time Rust?
  2. Open-source?
  3. Research?
  4. Crypto/Blockchain?

team@coblox.tech

slides.com/thomas_eizinger

We are around!

Thanks for listening!

Presenting testcontainers-rs

By Thomas Eizinger

Presenting testcontainers-rs

Presenting testcontainers-rs at the Rust Sydney Meetup - 13th March 2019

  • 781