Hello.
This is a question more related to architecture and rust.
I have been learning and using rust kit a lot to build micro services and am facing some challenges in testing I'd like to ask for recomendations.
The architecture I am using is as follows:
main function - starts the servers (one or more kinds of servers) and set routes with handlers.
Handlers layer: responsible for defining the handlers configured for the servers
- Validate data with respect to the required format.
- Convert request data from the request specific formats to the required parameters for core function.
- Calls core function,
- Gets core function response and converts back to the expected format response.
core layer:
- Exposes and implements a set of functions to the handlers layer. These functions are the core module public API and represents the business logic.
- Create submodules for internal use, such as repositories and others.
- creates a public sub module where input and output parameters to work with the public functions are made available.
This creates separation between layers and allows me to have, for example, handlers from graphql, grpc and AMQP protocols all calling the same core functions, and the core layer is not even aware of the existance of all these handlers and such.
This also separates the internal layers of core functionality from the outside world, so that handlers have no database access and changes in the requests / responses do not affect the core (core could be even extracted to a new crate but, because core functionality is kit specific for each microservice it is being maintained as an internal module with pub fns exposed to the outside).
See, I am not saying that this is the best approach, I am only trying to explain my way of thinking. Now, here is what I am trying to figure out:
For unit testing the core functionality, I need to mock somehow things that the core functions use, the internal modules such as repositories and alike. I have no idea aon how to perform such mocking, because the public api does not receive these dependencies as parameters. The premise is that the handlers layer don't have to know nothing about the internal implementations of core functions. If I coded the public api to receive, say, the repository they would use, then the handler layer would have to inject this, and it is not the handlers layer's responsibility to know what repository the core functions should use as part of its work.
In javascript, I would patch the module loader to load a mocked repository module, so that when the core function requires the repository module it would get instead a mocked module, all of that without compromising the way the handler calls the core function.
In java, probably the dependency ingector would create an object with the dependencies in place, but I have been trying to not rely that much on object oriented paradigms when using rust.
Question is: how to solve this in rust?
A typical core module is as follows:
core/accounts/mod.rs
mod repository;
pub mod model;
use repository::*;
use model::*;
pub fn create_account(agency: i32, account_nunber: i32, context: Context) -> Result<String> {
let conn = context.get_connection();
let new_account = repository::create_account(agency, account_nunber, &conn);
new_account.public_id
}
core/accounts/repository.rs *** database access
core/accounts/model.rs *** module where the Account struct, returned by repository::create_account function returns.
The handler function would then:
handler.rs
use crate::core::accounts::create_account;
pub fn handler(req: request, shared_state: SharedStat) -< Response {
let (agency, account_nunber) = request.getparams();
let context = Context {
connection: shared_stat.get_connection(),
}
let public_id = create_account(agency, account_number, context);
Response::ok(public_id)
}
I realize that even trying to not "polute" the handlers layer with something it shouldn't take care of, I still have the context creation. However, the context creation is generic, in the sense that it does require some parameters, but those are the same for every core function. If these were repositories, the handlers would need to know which repository to inject for each function call. Now, all they know is that they have to provide a context containing a database connection pool.
I think I was able to show how things currently are set and what my problem is. I thank you for any advices on this topic.