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}