Long day! You’ve done a thousand things and your head is a bit sore. As the folk wisdom says: Go to sleep! Calm music, a one-hour timer, you lay your head down and at that very moment, your brain shifts into overdrive and you realize that you might be able to solve the problem that has been bothering you for a while in your code. But well, there’s always tomorrow, isn’t there? But what if I forget? Here we go again!
Let’s see, if I write down in an outline what I don’t want to forget, will I succeed in falling asleep?
The problem Link to heading
I would like to run the tests in such a way that the program does not perform actual database operations during testing. There are several reasons for this. First, it would significantly slow down the tests. Second, the test results wouldn’t always be consistent depending on what I forget to clean up from the test database. Third, I wouldn’t be able to run such tests properly from Jenkins pipelines if they perform real database operations. In short: there are plenty of reasons.
1pub async fn create_inner<F, M>(
2 claims: Claims,
3 organizational_units_module: Arc<OrganizationalUnitsModule>,
4 payload: Result<Json<CreateRequestHelper>, JsonRejection>,
5 repo_factory: F,
6 migrator_factory: M,
7) -> Response
8where
9 F: Fn() -> Box<dyn OrganizationalUnitsRepository + Send + Sync>,
10 M: Fn() -> Box<dyn DatabaseMigrator + Send + Sync>,
11{
12 match payload {
13 Ok(Json(payload)) => match CreateRequest::try_from(payload) {
14 Ok(user_input) => {
15 let mut repo = repo_factory();
16 let migrator = migrator_factory();
17 match try_create(
18 &mut *repo,
19 &*migrator,
20 claims,
21 user_input,
22 organizational_units_module,
23 )
24 .await
25 {
26 Ok(resp) => (StatusCode::CREATED, Json(resp)).into_response(),
27 Err(e) => e.into_response(),
28 }
29 }
30 Err(e) => e.into_response(),
31 },
32 Err(_) => FriendlyError::UserFacing(
33 StatusCode::BAD_REQUEST,
34 "ORGANIZATIONAL_UNITS/HANDLER/CREATE".to_string(),
35 "Invalid JSON".to_string(),
36 )
37 .trace(Level::DEBUG)
38 .into_response(),
39 }
40}
41
42#[debug_handler]
43pub async fn create(
44 AuthenticatedUser(claims): AuthenticatedUser,
45 State(organizational_unit_module): State<Arc<OrganizationalUnitsModule>>,
46 payload: Result<Json<CreateRequestHelper>, JsonRejection>,
47) -> Response {
48 create_inner(
49 claims,
50 organizational_unit_module.clone(),
51 payload,
52 || {
53 Box::new(PoolWrapper::new(
54 organizational_unit_module
55 .pool_manager
56 .get_default_tenant_pool(),
57 ))
58 },
59 || Box::new(PgDatabaseMigrator),
60 )
61 .await
62}
The other problem Link to heading
I don’t call the database directly, I need to mock it, which isn’t such an easy task in Rust. The types, the borrow checker, and everything else make it complicated, but it’s roughly figured out. The only difference is that I don’t call the Axum handler directly; instead, I created an internal function from it that I can test. However, this causes the tests to bypass middleware, so they generously assume that authorization and authentication are working.
And what occurred to me? Link to heading
What would happen if the initialization of the two closure didn’t happen in the handler, but rather
during the initialization of the OrganizationalUnitsModule
, because from this context, the repository_factory
doesn’t really need anything, and the PgDatabaseMigrator
will receive the parameter it works with at a later stage
anyway.
1#[debug_handler]
2pub async fn create(
3 AuthenticatedUser(claims): AuthenticatedUser,
4 State(organizational_unit_module): State<Arc<OrganizationalUnitsModule>>,
5 payload: Result<Json<CreateRequestHelper>, JsonRejection>,
6) -> Response {
7 create_inner(
8 claims,
9 organizational_unit_module.clone(),
10 payload,
11 || { // << I'm looking at you!
12 Box::new(PoolWrapper::new(
13 organizational_unit_module
14 .pool_manager
15 .get_default_tenant_pool(),
16 ))
17 },
18 || Box::new(PgDatabaseMigrator), // << And you!
19 )
20 .await
21}
So, the OrganizationalUnitsModule
could be something like this:
1pub struct OrganizationalUnitsModule {
2 pub pool_manager: Arc<dyn PgPoolManagerTrait>,
3 pub config: Arc<AppConfig>,
4 pub repo_factory: Arc<dyn Fn() -> Box<dyn OrganizationalUnitsRepository + Send + Sync> + Send + Sync>,
5 pub migrator_factory: Arc<dyn Fn() -> Box<dyn DatabaseMigrator + Send + Sync> + Send + Sync>,
6}
Whether this will work is a problem for tomorrow!
Good night!
Of course, you should know how to call the handler from the tests! Link to heading
I have already found a solution for this earlier: there is a oneshot()
function in tower::ServiceExt
!
Good night, really! :))