Skip to main content

Application State

We'll add a simple visitor counter so that we can keep track of how many visitors access the application's endpoints.

Extending the Application State

The application state is defined in web/src/state.rs. That's where we'll add our new counter property to the AppState struct:

use my_app_config::Config;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

/// The application's state that is available in [`crate::controllers`] and [`crate::middlewares`].
-pub struct AppState {}
+pub struct AppState {
+ counter: AtomicUsize,
+}
+
+impl AppState {
+ pub fn get_visit_count(&self) -> usize {
+ self.counter.load(Ordering::Relaxed)
+ }
+
+ pub fn count_visit(&self) {
+ self.counter.fetch_add(1, Ordering::Relaxed);
+ }
+}

/// The application's state as it is shared across the application, e.g. in controllers and middlewares.
///
/// This function creates an [`AppState`] based on the current [`my_app_config::Config`].
pub async fn init_app_state(_config: Config) -> AppState {
- AppState {}
+ AppState {
+ counter: AtomicUsize::new(0),
+ }
}

We can then modify the web/src/controllers/greeting.rs controller to count each visit and share with the visitors how many visits to the endpoint have been made:

-use axum::response::Json;
+use crate::state::SharedAppState;
+use axum::{extract::State, response::Json};
use serde::{Deserialize, Serialize};

/// A greeting to respond with to the requesting client
pub struct Greeting {
/// Who do we say hello to?
pub hello: String,
+ /// Let them know this is the nth visit
+ pub visit: usize,
}

/// Responds with a [`Greeting`], encoded as JSON.
#[axum::debug_handler]
-pub async fn hello() -> Json<Greeting> {
+pub async fn hello(State(app_state): State<SharedAppState>) -> Json<Greeting> {
+ app_state.count_visit();
Json(Greeting {
hello: String::from("world"),
+ visit: app_state.get_visit_count(),
})
}

Testing

Let's now add a test for that as well. There is a test for the greeting controller already that Gerust generated out-of-the-box when generating the project. However, when we run that, we see it no longer works:

» cargo test
Compiling my-app-web v0.0.1 (/Users/mainmatter/Code/gerust/my-app/web)
error: cannot construct `AppState` with struct literal syntax due to private fields
--> web/src/test_helpers/mod.rs:193:27
|
193 | let app = init_routes(AppState {});
| ^^^^^^^^
|
= note: private field `counter` that was not provided

error: could not compile `my-app-web` (lib) due to 1 previous error

The problem is we added a new private field to our AppState that we're no specifying when creating the AppState for the app under test. Also, we cannot specify the value of the field when creating the AppState for the test anyway, since it's private. The easiest solution is to derive std::default::Default for the AppState since AtomicUsize implements that as well and will simply default to 0:

// web/src/state.rs
use my_app_config::Config;
+use std::default::Default;


+#[derive(Default)]
pub struct AppState {
counter: AtomicUsize,
}
// web/src/test-helpers/mod.rs
use crate::routes::init_routes;
use crate::state::AppState;
use axum::{
body::{Body, Bytes},
http::{Method, Request},
response::Response,
Router,
};
+use std::default::Default;


pub async fn setup() -> TestContext {
let init_config: OnceCell<Config> = OnceCell::new();
let _config = init_config.get_or_init(|| load_config(&Environment::Test).unwrap());

+ let app_state: AppState = Default::default();
+ let app = init_routes(app_state);
- let app = init_routes(AppState {});

TestContext { app }
}

That fixes the tests:

» cargo test
Compiling my-app-web v0.0.1 (/Users/mainmatter/Code/gerust/my-app/web)
Finished `test` profile [unoptimized + debuginfo] target(s) in 1.75s
Running unittests src/lib.rs (target/debug/deps/my_app_web-425f75e35e5cfe7b)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running unittests src/main.rs (target/debug/deps/my_app_web-73f82937d51059a9)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Running tests/api/main.rs (target/debug/deps/api-2ba7a788d4d16867)

running 1 test
test greeting_test::test_hello ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

We can now add a test for the visit value we introduced in the response of the /greet endpoint to the existing test in web/tests/api/greeting_test.rs:

use googletest::prelude::*;
use my_app_macros::test;
use my_app_web::controllers::greeting::Greeting;
use my_app_web::test_helpers::{BodyExt, RouterExt, TestContext};

#[test]
async fn test_hello(context: &TestContext) {
let response = context.app.request("/greet").send().await;

let greeting: Greeting = response.into_body().into_json().await;
assert_that!(greeting.hello, eq(&String::from("world")));
}
+
+#[test]
+async fn test_visit_count(context: &TestContext) {
+ let response = context.app.request("/greet").send().await;
+
+ let greeting: Greeting = response.into_body().into_json().await;
+ assert_that!(greeting.visit, eq(1));
+
+ let response = context.app.request("/greet").send().await;
+
+ let greeting: Greeting = response.into_body().into_json().await;
+ assert_that!(greeting.visit, eq(2));
+}

Since every test receives its own instance of the application under test which has its own application state which initializes the visit counter with 0, we can simply request the /greet endpoint twice and assert the first visit is indeed reported as the first visit, and the second visit as the second. This test can run in parallel with other tests without the visit count getting messed up because of that isolation.


Let's now add a new endpoint to the application.