Minél többet dolgozol egy kódbázissal, annál inkább kirajzolódnak mintázatok. Néhány ilyen mintázat egészen zord képet fest arról, hogyan fog kinézni a kód, ha például további 50 modullal bővíted az idő során. Ilyenkor érdemes megállni egy pillanatra átgondolni az eddig kóddizájn döntéseidet.

A következő kódrészletben már az előző iterációban is éreztem, hogy probléma lesz ennek a skálázásával, ezért gyorsan rádobtam egy builder pattern-t a biztonság kedvéért, de aztán folytattam tovább az éppen aktuális gondolatmenetet.

 1fn test_create_managed_success() {
 2    // ...
 3    let app_state = Arc::new(
 4        AppStateBuilder::new()
 5            .users_module(Arc::new(UsersModule {}))
 6            .config_module(config.clone())
 7            .tenants_module(Arc::new(TenantsModule {
 8                pool_manager: pool_manager_mock.clone(),
 9                config: config.clone(),
10                repo_factory,
11                migrator_factory,
12            }))
13            .auth_module(Arc::new(AuthModule {
14                pool_manager: pool_manager_mock.clone(),
15                password_hasher: Arc::new(Argon2Hasher),
16                config: config.clone(),
17            }))
18            .build()
19            .unwrap(),
20    );
21    // ...
22}

Aztán ma, ahogy elkezdtem az auth modul tesztjeit okosítani rájöttem, hogy builder ide vagy oda nem lesz nekem annyira jó hosszú távon, ha tesztenként újra kell inicializálgatom az egész AppStatet defaultokkal meg mock-okkal főleg annak fényében, hogy egy-egy végpont a modulok csak egy kis halmazát használja. Mielőtt teljesen elharapódzik ez a minta a kódban kellett találni valami értelmesebb megoldást.

Default, a megmentő Link to heading

Kezdtem azzal, hogy megírtam a Default trait implementációját az összes modulra. Szépen megkapták a #[cfg(test)] taget, mivel csak a tesztekhez szeretném őket használni. Én írtam a kódot, az IDE importálta, amit importálnia kellett. Minden egész simán ment, amíg le nem akartam fordíttatni a programot. A compiler szólt, hogy jó lenne, ha az új importjaimra is ráírnám, hogy #[cfg(test)], mert azok is csak akkor fordulnak le, ha #[cfg(test)]. Legyen hát. Egy darabig csináltam így. Aztán rájöttem, hogy ez így macerás is és nem is szép.

Van jobb megoldás Link to heading

Sokkal szebb, ha a tests modulban implementálom a Default traitet. Így nem keverem össze a teszt-hez szükséges importokat a tényleges buildhez szükséges importokkal és még csak nem is kell darabonként mindenre ráírnom, hogy #[cfg(test)]

És a végeredmény?

 1#[cfg(test)]
 2pub(crate) mod tests {
 3    use super::*;
 4    use crate::app::database::MockPgPoolManagerTrait;
 5    use crate::auth::repository::MockAuthRepository;
 6
 7    impl Default for AuthModule {
 8        fn default() -> Self {
 9            AuthModule {
10                pool_manager: Arc::new(MockPgPoolManagerTrait::new()),
11                config: Arc::new(AppConfig::default()),
12                repo_factory: Box::new(|| Box::new(MockAuthRepository::new())),
13            }
14        }
15    }
16}
 1fn test_create_managed_success() {
 2    // ...
 3    let app_state = AppStateBuilder::default()
 4        .tenants_module(Arc::new(TenantsModule {
 5            pool_manager: pool_manager_mock.clone(),
 6            config: config.clone(),
 7            repo_factory,
 8            migrator_factory,
 9        }))
10        .build()
11        .unwrap();
12    // ...
13}