It's time to finally dig-in into testing our actix services. So far I've asked you to overlook the tests, and focus on the actix features. Now we're going to make sure that the things we've been using actually work!
One of the reasons I delayed going further into test explanations is that we've been putting them
in the routes.rs
files of our projects, plus they also contained a fair bit of duplication.
Doing integration tests on binary crates is a bit less clean than on library crates.
That's why the integration project has been divided into 2 crates:
- integration contains a single (and very short) main.rs file, while;
- integration-lib has the bulk of our application, basically
everything is now done in
integration-lib
, plus tests;
Doing integration tests in a lib crate is easy, you just create a tests folder that lives next to src, and cargo will do its magic. If you want to understand a bit more, check out the Rust book chapter on this.
If you understand how the tests are working here, going back to earlier projects you'll see that they're pretty similar (a bunch will be exactly the same).
We won't be looking at each test individually, this would become repetitive pretty fast, I'll be showing you only the interesting parts.
We'll start with /users
tests, as we're dealing with authorization and testing some /tasks
services will require User
setup.
7.2 Testing users
First off, ignore the macro at the start, we'll come back to it later!
The first test we'll be looking at is:
#[actix_rt::test]
async fn test_user_insert_valid_user()
As I've told you before, actix_rt::test
is the async runtime for our test. We must set up a
App
, but instead of using users::user_service
as the configuration for App::configure
, we'll
be setting only the services we're interested in testing, in this case users::insert
(here aliased
to users::user_insert
).
test::init_service(app)
starts our server from our App
builder, it must be mutable to comply
with the test::call_service
function.
We then create our test::TestRequest
with POST
to the URI /users/register
, remember that this
route is not protected (not wrapped in HttpAuthentication
middleware), so we just need to set the
request body, and no headers.
test::call_service(&mut app, request)
is aptly named, it'll call our service with the request
we've just created, and returns as ServiceResponse
(not a HttpResponse
!). And finally, we just
assert the response as successful.
The code pattern of this test is pretty consistent with what other tests want, so I'll be using a couple of macros to keep things from being repeated.
#[actix_rt::test]
pub async fn test_user_update_valid_user()
This is our first test to take advantage of our pair of macros: setup_app!
and pre_insert_user
.
So let's take a small detour to explain each, before we come back to the users
tests.
Let's start with the simpler of the two macros:
macro_rules! pre_insert_user {
($app: expr) => {{
// ...
user
}}
If you look at it, this is almost a copy of our test_user_insert_valid_user
test function, except
that: it doesn't start a server, it just inserts a user and returns it. We're not using any mocking
library, and we're not pre-inserting into our test database either, so this macro is a handy way of
inserting a user that we'll call before any test that requires an existing User
.
Instead of creating the server, we take it as the macro parameter $app: expr
.
There is one new thing in there:
let user: User = test::read_body_json(insert_user_response).await;
test::read_body_json
is a neat actix helper that takes a ServiceResponse
and extracts the json body into our User
type. It takes a bit of fiddling with a ServiceResponse
to do the same without this function and
its friend
test::read_body
.
The second macro you'll see is the setup_app!
call, in
mod.rs, and it's a tiny bit bigger than this one.
#[macro_export]
macro_rules! setup_app {
($configure: expr) => {{
// ...
(app, bearer_token, cookies)
}}
Well, the reason for this macro existence comes mainly from the fact that we must deal with a bunch of protected routes. Which means that for many tests we would have to do the following:
- Set up
App
withusers::insert
andusers::login
services, so that our requests may go through authentication; - Do the work of inserting a user and logging in with it;
- Extract the authentication token from the login response, to use it on requests that require it;
Doing this for each test gets old really fast, thus a macro to rescue us.
The $configure: expr
parameter is used to pass the tests' relevant services to the App
builder.
Tests that use this macro will start with a closure |cfg: &mut ServiceConfig|
, much like our
users::user_service
and tasks::task_service
functions.
This macro also sets the necessary middlewares to handle login: IdentityService
and
CookieSession
.
The bulk of it is the call to register a user, then use it to login. We retrieve the auth-cookie
,
and the session-cookie
from the login ServiceResponse
, and return the initialized App
, the
bearer token, and the cookies we extracted.
These 3 pieces are all we'll be needing to go on with the tests.
7.4 Back to the users
Let's jump to the /users
update test:
#[actix_rt::test]
pub async fn test_user_update_valid_user()
We start by creating a configuration closure with the routes we're interested in testing. Even
though the user_insert
service is already part of the setup_app!
macro, I left it there to make
it clear that we depend on this service for the test.
The macro invocation of setup_app!
returns the running server (app
), the bearer_token
and
cookies
, both of which will be inserted in the test's request headers.
The next invocation of pre_insert_user!
is our shorthand for inserting a User
into the database,
this is the User
that we will be updating.
After all this setup, we're finally ready to create the TestRequest
we're interested in, with the
help of
TestRequest::insert_header
,
and
TestRequest::cookie
.
Finally, we just assert if the ServiceResponse::status()
was successful. Some tests will compare
the StatusCode
directly against what we expect from the service, instead of if they were just successful, this is
to cover some services that respond with
StatusCode::FOUND
, or
StatusCode::NOT_MODIFIED
.
Most of the tests will look like this, except the ones that don't require authentication. I've left
the test_user_logout
as an "expanded" test case, so it doesn't make use of macros.
7.5 Testing tasks
These tests are structured in much the same way as the
test_user_routes are. It comes with its own macro
pre_insert_task
that inserts a Task
into the database.
test_task_insert_valid_task
was left as the expanded version, much like test_user_logout
was,
and it covers the whole process of setting up App
, a LoggedUser
, and finally using the /tasks
insert service.
I don't feel that there is much to be gained by going over every test here, as they're using the same features you already saw in test_user_routes. If you feel that I should explain something here, please open up an issue!
The main issue I've run into when writing these tests was getting a 404
because I kept forgetting
to add a service to ServiceConfig
, so if you get a 404
, check you've added the services you're
using (in the case of these projects, also check the macros), and check that the TestRequest::uri
s
are correct.
We're not using any mocking library, we use a test database instead, this means that these tests may
not play well with concurrency, plus are limited with by the
PoolOptions::max_connections
.
I've been running these tests in single-threaded mode with:
# Runs every test in a single thread
cargo test -- --test-threads=1
This chapter covered a big portion of testing with actix-web. On the next project tls, we'll set up TLS and have our server running on HTTPS.