app.rs

Let’s start with the same struct as we had before:

/// Application.
#[derive(Debug, Default)]
pub struct App {
  /// should the application exit?
  pub should_quit: bool,
  /// counter
  pub counter: u8,
}

We can add additional methods to this Application struct:

impl App {
  /// Constructs a new instance of [`App`].
  pub fn new() -> Self {
    Self::default()
  }

  /// Handles the tick event of the terminal.
  pub fn tick(&self) {}

  /// Set running to false to quit the application.
  pub fn quit(&mut self) {
    self.should_quit = true;
  }

  pub fn increment_counter(&mut self) {
    if let Some(res) = self.counter.checked_add(1) {
      self.counter = res;
    }
  }

  pub fn decrement_counter(&mut self) {
    if let Some(res) = self.counter.checked_sub(1) {
      self.counter = res;
    }
  }
}

We use the principle of encapsulation to expose an interface to modify the state. In this particular instance, it may seem like overkill but it is good practice nonetheless.

The practical advantage of this is that it makes the state changes easy to test.

#[cfg(test)]
mod tests {
  use super::*;
  #[test]
  fn test_app_increment_counter() {
    let mut app = App::default();
    app.increment_counter();
    assert_eq!(app.counter, 1);
  }

  #[test]
  fn test_app_decrement_counter() {
    let mut app = App::default();
    app.decrement_counter();
    assert_eq!(app.counter, 0);
  }
}

Tip

You can test a single function by writing out fully qualified module path to the test function, like so:

cargo test -- app::tests::test_app_increment_counter --nocapture

Or even test all functions that start with test_app_ by doing this:

cargo test -- app::tests::test_app_ --nocapture

The --nocapture flag prints stdout and stderr to the console, which can help debugging tests.