dockertest_server/test.rs
1/// Contains types for creating tests
2use dockertest::{Composition, DockerOperations, DockerTest};
3use futures::Future;
4use type_map::concurrent::TypeMap;
5
6use crate::server::{Config, Server};
7
8/// A single test which brings up one or more [Servers][Server].
9///
10/// A [Test][crate::test::Test] receives [Configs][Config] via `register` which
11/// determines the servers that will be brought up when the test is executed.
12/// The `run` method is used to bring up the [Servers][Server] and then run the
13/// given test body.
14///
15/// The body of a test is determined by an async closure passed to the `run`
16/// method. The closure will receive a [TestInstance] which can be used to
17/// access the [Servers][Server] that were created when the test was setup.
18pub struct Test {
19 pub configs: TypeMap,
20 pub compositions: Vec<Composition>,
21}
22
23impl Test {
24 /// Creates an empty [Test].
25 pub fn new() -> Self {
26 Test {
27 configs: TypeMap::new(),
28 compositions: Vec::new(),
29 }
30 }
31
32 /// Registers a [Config] with this test.
33 ///
34 /// A [Test] can be configured with any number of [Configs][Config] for
35 /// determining which [Servers][Server] are brought up in a test. Each
36 /// [Config] passed will have it's respective [Server] created before the
37 /// test body is ran.
38 pub fn register(&mut self, config: impl Config + 'static) {
39 self.configs.insert(config.clone());
40 self.compositions.push(config.into_composition());
41 }
42
43 /// Brings up the [Servers][Server] registered with this test and then
44 /// runs the given test body.
45 ///
46 /// The test body receives a [TestInstance] which can be used for accessing
47 /// any configured [Servers][Server] through the `server` method.
48 /// The test body is guaranteed to not execute until all configured
49 /// [Servers][Server] are verified to be running and available. The scope of
50 /// the test body determines the life of the [Servers][Server]: they are
51 /// created before the closure is run and destroyed after the closure exits.
52 pub fn run<T, F>(self, fun: T)
53 where
54 T: FnOnce(TestInstance) -> F + Send + 'static,
55 F: Future<Output = ()> + Send + 'static,
56 {
57 let mut test = DockerTest::new();
58 for comp in self.compositions {
59 test.add_composition(comp)
60 }
61
62 let configs = self.configs;
63 test.run(|ops| async move {
64 let instance = TestInstance::new(configs, ops);
65 (fun)(instance).await;
66 });
67 }
68}
69
70impl Default for Test {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76/// Represents a running instance of a [Test].
77///
78/// Internally, this type keeps track of all [Configs][Config] that were
79/// registered with a [Test] using a [TypeMap]. A [Server] can be fetched by
80/// calling the `server` method with the desired type that implements [Server].
81/// See the associated method documentation for more details.
82pub struct TestInstance {
83 pub configs: TypeMap,
84 pub op: DockerOperations,
85}
86
87impl TestInstance {
88 /// Returns a new [TestInstance].
89 pub fn new(configs: TypeMap, op: DockerOperations) -> Self {
90 TestInstance { configs, op }
91 }
92
93 /// Returns an instance of the requested type that implements [Server].
94 ///
95 /// Internally a [TestInstance] has a [TypeMap] which contains all of the
96 /// [Configs][Config] that were registered with a given [Test]. Since each
97 /// [Config] has a one-to-one relationship with a [Server], an instance of a
98 /// [Server] can be created by simply having a copy of it's associated
99 /// [Config]. This method performs that mapping for you, looking for a
100 /// [Config] of the type specified by [Server::Config] in the type map and
101 /// then generating and returning a new instance of the [Server] using the
102 /// [Config] and runtime data generated when the underlying container was
103 /// brought up.
104 ///
105 /// Note that it then follows this method will fail if the passed [Server]
106 /// type did not have it's associated [Config] registered when the [Test]
107 /// was created.
108 pub fn server<S: Server>(&self) -> S {
109 let config = self.configs.get::<S::Config>().unwrap();
110 let container = self.op.handle(config.handle());
111 S::new(config, container)
112 }
113}