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();
}
- Create container
- wait_until_ready
- 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!
- Full-time Rust?
- Open-source?
- Research?
- 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