It’s Friday evening. Today’s development has grown a bit larger than I initially planned. Along the way, I found something that I’ll be happy about someday, but today, it only increased the number of hours spent in front of the computer.

Builders! Builders everywhere! Link to heading

Let’s see what the future holds for all these hundreds of lines of code I developed today, integrated into previous modules. I have a TenantsModule structure and its currently somewhat cumbersome initialization.

 1pub struct TenantsModule {
 2    pub pool_manager: Arc<dyn PgPoolManagerTrait>,
 3    pub config: Arc<AppConfig>,
 4    pub repo_factory: Box<dyn Fn() -> Box<dyn TenantsRepository + Send + Sync> + Send + Sync>,
 5    pub migrator_factory: Box<dyn Fn() -> Box<dyn DatabaseMigrator + Send + Sync> + Send + Sync>,
 6    pub connection_tester_factory:
 7        Box<dyn Fn() -> Box<dyn ConnectionTester + Send + Sync> + Send + Sync>,
 8}
 9
10#[test]
11fn some_important_test() {
12    // ...
13    let mut pool_manager_mock = Arc::new(MockPgPoolManagerTrait::new());
14    // some long pool_manager_mock init...
15
16    let mut migrator_factory = MockDatabaseMigrator::new();
17    // some long migrator_factory init...
18
19    let mut connection_tester_factory = MockConnectionTester::new();
20    // some long connection_tester_factory init...
21
22    let config = Arc::new(AppConfigBuilder::default().build().unwrap());
23    // let's say I want to use the default test config
24
25    let repo_factory = Box::new(|| {
26        let mut repo = MockTenantsRepository::new();
27        repo.expect_setup_self_hosted()
28            .times(1)
29            .withf(|name, _, _| name == "test")
30            .returning(|_, _, _| {
31                Ok(Tenant {
32                    id: Uuid::new_v4(),
33                    name: "test".to_string(),
34                    db_host: "example.com".to_string(),
35                    db_port: 5432,
36                    db_name: "tenant_1234567890".to_string(),
37                    db_user: "tenant_1234567890".to_string(),
38                    db_password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".to_string(),
39                    db_max_pool_size: 5,
40                    db_ssl_mode: "verify-full".to_string(),
41                    created_at: Local::now(),
42                    updated_at: Local::now(),
43                    deleted_at: None,
44                })
45            });
46        Box::new(repo) as Box<dyn TenantsRepository + Send + Sync>
47    });
48    let tenants_module = TenantsModule {
49        pool_manager: pool_manager_mock.clone(),
50        config: config.clone(),
51        repo_factory,
52        migrator_factory,
53    };
54    // ...
55}

The problem with this is that at the moment I create this structure, I need to have all the values I want to include already set, but this requirement often creates quite inconvenient situations.

The simplest solution for this, which is also used in many other languages, is the builder pattern. Its core idea is that in the Builder structure, the values can exist without the entire structure being fully initialized. When all values are in place, I can then create the TenantsModule structure at the end using a build function.

What does this builder look like in practice?

 1pub struct TenantsModuleBuilder {
 2    pub pool_manager: Option<Arc<dyn PgPoolManagerTrait>>, // Option!
 3    pub config: Option<Arc<AppConfig>>, // Option!
 4    pub repo_factory:
 5        Option<Box<dyn Fn() -> Box<dyn TenantsRepository + Send + Sync> + Send + Sync>>, // Option!
 6    pub migrator_factory:
 7        Option<Box<dyn Fn() -> Box<dyn DatabaseMigrator + Send + Sync> + Send + Sync>>, // Option!
 8    pub connection_tester_factory:
 9        Option<Box<dyn Fn() -> Box<dyn ConnectionTester + Send + Sync> + Send + Sync>>, // Option!
10}
11
12impl TenantsModuleBuilder {
13    pub fn new() -> Self {
14        Self {
15            pool_manager: None,
16            config: None,
17            repo_factory: None,
18            migrator_factory: None,
19            connection_tester_factory: None,
20        }
21    }
22    pub fn pool_manager(mut self, pool_manager: Arc<dyn PgPoolManagerTrait>) -> Self {
23        self.pool_manager = Some(pool_manager);
24        self
25    }
26    pub fn config(mut self, config: Arc<AppConfig>) -> Self {
27        self.config = Some(config);
28        self
29    }
30    pub fn repo_factory(
31        mut self,
32        repo_factory: Box<dyn Fn() -> Box<dyn TenantsRepository + Send + Sync> + Send + Sync>,
33    ) -> Self {
34        self.repo_factory = Some(repo_factory);
35        self
36    }
37    pub fn migrator_factory(
38        mut self,
39        migrator_factory: Box<dyn Fn() -> Box<dyn DatabaseMigrator + Send + Sync> + Send + Sync>,
40    ) -> Self {
41        self.migrator_factory = Some(migrator_factory);
42        self
43    }
44    pub fn connection_tester_factory(
45        mut self,
46        connection_tester_factory: Box<
47            dyn Fn() -> Box<dyn ConnectionTester + Send + Sync> + Send + Sync,
48        >,
49    ) -> Self {
50        self.connection_tester_factory = Some(connection_tester_factory);
51        self
52    }
53
54    pub fn build(self) -> Result<TenantsModule, String> {
55        Ok(TenantsModule {
56            pool_manager: self
57                .pool_manager
58                .ok_or("pool_manager is required".to_string())?,
59            config: self.config.ok_or("config is required".to_string())?,
60            repo_factory: self
61                .repo_factory
62                .ok_or("repo_factory is required".to_string())?,
63            migrator_factory: self
64                .migrator_factory
65                .ok_or("migrator_factory is required".to_string())?,
66            connection_tester_factory: self
67                .connection_tester_factory
68                .ok_or("connection_tester is required".to_string())?,
69        })
70    }
71}

And what is this good for? Link to heading

Let’s assume I need to write many tests where the pool_manager, config, migrator_factory, and connection_tester_factory are always the same, but I need to change the repo_factory from test to test. Using a builder, I can do this much more flexibly, even with structures that contain private fields. (The TenantsModule is not such a structure at the moment)

The final outcome with a builder would look something like this:

 1#[cfg(test)]
 2pub(crate) mod tests {
 3    use super::*;
 4    use crate::app::config::AppConfigBuilder;
 5
 6    use crate::app::database::{
 7        MockConnectionTester, MockDatabaseMigrator, MockPgPoolManagerTrait,
 8    };
 9    use crate::tenants::repository::MockTenantsRepository;
10
11    fn default_tenants_module_for_self_hosted_tests() -> TenantsModuleBuilder {
12        let mut pool_manager_mock = Arc::new(MockPgPoolManagerTrait::new());
13        // some long pool_manager_mock init...
14
15        let mut migrator_factory = MockDatabaseMigrator::new();
16        // some long migrator_factory init...
17
18        let mut connection_tester_factory = MockConnectionTester::new();
19        // some long connection_tester_factory init...
20
21        let config = Arc::new(AppConfigBuilder::default().build().unwrap());
22        // let's say I want to use the default test config
23
24        // notice: I expect the caller to finish the repo_factory
25        TenantsModuleBuilder::default()
26            .pool_manager(pool_manager_mock.clone())
27            .config(config)
28            .migrator_factory(migrator_factory)
29            .connection_tester_factory(connection_tester_factory)
30    }
31
32    #[test]
33    fn some_important_test1() {
34        let repo_factory = Box::new(|| {
35            let mut repo = MockTenantsRepository::new();
36            repo.expect_setup_self_hosted()
37                .times(1)
38                .withf(|name, _, _| name == "test")
39                .returning(|_, _, _| {
40                    Ok(Tenant {
41                        id: Uuid::new_v4(),
42                        name: "test".to_string(),
43                        db_host: "example.com".to_string(),
44                        db_port: 5432,
45                        db_name: "tenant_1234567890".to_string(),
46                        db_user: "tenant_1234567890".to_string(),
47                        db_password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".to_string(),
48                        db_max_pool_size: 5,
49                        db_ssl_mode: "verify-full".to_string(),
50                        created_at: Local::now(),
51                        updated_at: Local::now(),
52                        deleted_at: None,
53                    })
54                });
55            Box::new(repo) as Box<dyn TenantsRepository + Send + Sync>
56        });
57
58        let tenants_module = default_tenants_module_for_self_hosted_tests()
59            .repo_factory(repo_factory)
60            .build()
61            .unwrap();
62    }
63
64    #[test]
65    fn some_important_test1() {
66        let repo_factory = Box::new(|| {
67            let mut repo = MockTenantsRepository::new();
68            repo.expect_setup_managed()
69                .times(0)
70                .withf(|_, name, _, _, _| name == "test")
71                .returning(|uuid: Uuid, _, _, _, _| {
72                    Ok(Tenant {
73                        id: uuid,
74                        name: "test".to_string(),
75                        db_host: "localhost".to_string(),
76                        db_port: 5432,
77                        db_name: "database".to_string(),
78                        db_user: "user".to_string(),
79                        db_password: "password".to_string(),
80                        db_max_pool_size: 5,
81                        db_ssl_mode: "disable".to_string(),
82                        created_at: Local::now(),
83                        updated_at: Local::now(),
84                        deleted_at: None,
85                    })
86                });
87            Box::new(repo) as Box<dyn TenantsRepository + Send + Sync>
88        });
89
90        let tenants_module = default_tenants_module_for_self_hosted_tests()
91            .repo_factory(repo_factory)
92            .build()
93            .unwrap();
94    }
95}

With this solution, I can avoid unnecessarily reinitializing the parts of the tenants_module that need to remain the same across all tests. If I need to fix something in the default parts, I only have to do it in one place, and the change will be reflected in all tests.

I was planning to publish this post yesterday, but it was already too late. :)

If you find any errors, feel free to send an email!