Realworld-axum-sqlx refactoring and unit testing for an even more real world

Is anyone else interested in using axum in production projects webapps? I am. I'm also learning Rust, so go easy:)

I've forked realworld-axum-sqlx, many thanks to those who got it as far as it is, and I made some changes. My goal is to separate data access from route handlers, enable mocking, and make testing of route handlers simple. So I moved data access logic to a models folder. I also moved logic out of mod.rs files. But it's all pretty much the same code in new places.

While implementing unit tests for users and profiles routes I made a Store struct that sits in ApiContext and has factory methods for the different model controllers. The user and profile model controllers have traits and an Arc implementation.

It all works, but it seems like a rats nest. I have to create a MockStore with a nested MockNNNCtrlTrait return.

I have a few questions:

  1. Why can't I make a set of tests in my profiles routes that only load a single route and set the state to a single DynProfileCtrl for instance?

  2. Why do we have to use trait objects? All types are known at compile time.

Here's the repo. If anybody is interested enough to read to this point and wants to look at it. I do think that actual realistic examples of web apps is of interest to others:

For 1, I'm not sure I understand what you mean by "load a single route". The reason the state has to have the whole store is because that's how the app was designed. If each controller was put in state directly you could only set the ones you needed for each test. The downside is it would be more work to extract multiple if you need more than one for a route. There are other ways you could go about it too, but it doesn't look like creating a mock store is that much work so I'm not sure there would be significant benefit to doing anything more complicated.

For 2, the trait objects are being used to allow "switching" between the real controllers and the mock controllers. In theory you could just switch which types are used in the Store with a cfg if you only have a single mock controller and a single real controller per trait. It would be relatively easy to accidentally introduce breakage between normal builds and test builds with a setup like that though.

Thank you for looking at this.

By or loading single route I meant something like this in the http/profiles.rs file:

        // snip: in some profiles.rs test function
        let mock_profile_ctrl = Arc::new(MockProfileCtrlTrait::new()) as DynProfileCtrl;

        let app = Router::new()
            .route("/api/profies/:username", get(get_user_profile))
            .with_state(mock_profile_ctrl);
        // snip

I would think, by looking at the If I try that, I get this:

error[E0277]: the trait bound `fn() {http::profiles::tests::get_user_profile}: Handler<_, _, _>` is not satisfied
   --> src/http/profiles.rs:174:50
    |
174 |             .route("/api/profies/:username", get(get_user_profile))
    |                                              --- ^^^^^^^^^^^^^^^^ the trait `Handler<_, _, _>` is not implemented for fn item `fn() {http::profiles::tests::get_user_profile}`
    |                                              |
    |                                              required by a bound introduced by this call
    |
    = help: the trait `Handler<T, S, B2>` is implemented for `Layered<L, H, T, S, B, B2>`
note: required by a bound in `axum::routing::get`
   --> /Users/brannan/.cargo/registry/src/index.crates.io-6f17d22bba15001f/axum-0.6.0/src/routing/method_routing.rs:403:1
    |
403 | top_level_handler_fn!(get, GET);
    | ^^^^^^^^^^^^^^^^^^^^^^---^^^^^^
    | |                     |
    | |                     required by a bound in this function
    | required by this bound in `get`
    = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)

I clearly don't understand something correctly.

Regarding the second question, generally speaking trait objects are necessary when you cannot know the type of an object at run time or when you need a collection to contain different object types with different storage requirements. When running tests we definitely know the type at run time so I'm not sure why we can't use generics instead of trait objects to achieve what we want for testing.

Again, there's obviously something I'm missing.

Thanks

Well having to re-specify routes in your tests is likely to increase the amount of breakage you have in tests, so that's one reason not to do things that way.

Ahh apologies, I missed that you were actually retrieving the controller trait objects with the State extractor. I see what your problem is now. Your MaybeAuthUser extractor has a bound on it's impl ApiContext: FromRef<S>. Because you don't have any way to get an ApiContext from DynProfileCtrl you get that error.

You can shuffle things around to make it work
First instead of using ApiContext directly you can have MaybeAuthUser only extract an Arc<Config>, and implement FromRef<ApiContext> for Arc<Config>

impl FromRef<ApiContext> for Arc<Config> {
    fn from_ref(input: &ApiContext) -> Self {
        input.config.clone()
    }
}

#[async_trait]
impl<S> FromRequestParts<S> for MaybeAuthUser
where
    S: Send + Sync,
    Arc<Config>: FromRef<S>,

Then you can create a new state type that wraps a single controller and also provides a config

#[derive(Clone)]
pub struct ControllerAndConfig<T> {
    pub config: Arc<Config>,
    pub controller: T,
}

impl FromRef<ControllerAndConfig<DynProfileCtrl>> for DynProfileCtrl {
    fn from_ref(input: &ControllerAndConfig<DynProfileCtrl>) -> Self {
        input.controller.clone()
    }
}

impl<T> FromRef<ControllerAndConfig<T>> for Arc<Config> {
    fn from_ref(input: &ControllerAndConfig<T>) -> Self {
        input.config.clone()
    }
}

Unfortunately you can't blanket impl FromRef for T for coherence reasons, though you could if you were willing to wrap the controller types in another wrapper type everywhere they appeared in state. Just manually implementing it for all the controller types isn't the end of the world (and would be easy to do with a macro)

Then you could set up a profile route test like this

let mock_profile_ctrl = Arc::new(MockProfileCtrlTrait::new()) as DynProfileCtrl;

let app: Router = Router::new()
    .route("/api/profies/:username", get(super::get_user_profile))
    .with_state(ControllerAndConfig {
        config: todo!(),
        controller: mock_profile_ctrl,
    });

You can wire things up with generics instead. You'll need one type parameter onApiContext for each different controller trait, and a separate wrapper type for each controller trait to implement FromRef with, since you don't have a single concrete type you can name per trait anymore.

use std::sync::Arc;

use axum::{
    extract::{FromRef, State},
    response::IntoResponse,
    routing::get,
    Router,
};

#[tokio::main]
async fn main() {
    // build our application with a single route
    let app = Router::new()
        // I'm honestly a little surprised I didn't have to specify the type of `get_profile`'s type parameter here
        .route("/profile", get(get_profile))
        .with_state(ApiContext {
            profile: Arc::new(MockProfileCtrl),
            user: Arc::new(()),
        });

    // run it with hyper on localhost:3000
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

trait ProfileCtrl {
    fn get_name(&self) -> String;
}

struct MockProfileCtrl;

impl ProfileCtrl for MockProfileCtrl {
    fn get_name(&self) -> String {
        "Name!".into()
    }
}

pub struct ApiContext<Profile, User> {
    profile: Arc<Profile>,
    user: Arc<User>,
}

impl<Profile, User> Clone for ApiContext<Profile, User> {
    fn clone(&self) -> Self {
        Self {
            profile: self.profile.clone(),
            user: self.user.clone(),
        }
    }
}

impl<Profile, User> FromRef<ApiContext<Profile, User>> for ProfileController<Profile> {
    fn from_ref(input: &ApiContext<Profile, User>) -> Self {
        Self(input.profile.clone())
    }
}

/// You need a wrapper type now to specify which controller to extract from `ApiContext` since you no longer have a concrete type you can name to extract.
pub struct ProfileController<T>(pub Arc<T>);

async fn get_profile<T: ProfileCtrl>(
    State(ProfileController(profile)): State<ProfileController<T>>,
) -> impl IntoResponse {
    (axum::http::StatusCode::OK, profile.get_name())
}

Thank you very much. This clears up a lot.

Your first point on re specifying the routes in the test is probably correct.

The benefit of using generics vs trait objects is also questionable in this case.

But I wanted to understand.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.