I'm learning Rust with unit tests and the below code is the solution of this problem with slight changes.
struct User {
name: String,
age: u8,
weight: f32
}
impl User {
fn new(name: String, age: u8, weight: f32) -> Self {
User {name: name, age: age, weight: weight}
}
fn get_name(&self) -> &str {
&self.name
}
fn get_age(&self) -> u8 {
self.age
}
fn get_weight(&self) -> f32 {
self.weight
}
fn set_name(&mut self, new_name: String) {
self.name = new_name
}
fn set_age(&mut self, new_age: u8) {
self.age = new_age
}
fn set_weight(&mut self, new_weight: f32) {
self.weight = new_weight
}
}
#[test]
fn test_user_get_name() {
let user = User::new(String::from("Bob"), 20, 150.23);
assert_eq!(user.get_name(), "Bob")
}
#[test]
fn test_user_set_name() {
let mut user = User::new(String::from("John"), 24, 150.23);
user.set_name("Jane".to_string()); // no performance cost compared to String::from
assert_eq!(user.get_name(), "Jane")
}
#[test]
fn test_user_get_age() {
let user = User::new(String::from("Bob"), 20, 150.23);
assert_eq!(user.get_age(), 20)
}
#[test]
fn test_user_set_age() {
let mut user = User::new(String::from("Bob"), 20, 150.23);
user.set_age(25);
assert_eq!(user.get_age(), 25)
}
#[test]
fn test_user_get_weight() {
let user = User::new(String::from("Bob"), 20, 150.23);
assert_eq!(user.get_weight(), 150.23)
}
#[test]
fn test_user_set_weight() {
let mut user = User::new(String::from("Bob"), 20, 150.23);
user.set_weight(160.8);
assert_eq!(user.get_weight(), 160.8)
}
I can execute tests and all of them pass:
rustc --test health_stats.rs --verbose -o health_stats_test
./health_stats_test
How can I create a setup()
function with a mutable object like:
let mut user = User::new(String::from("Bob"), 20, 150.23);
so that I can call get
and set
methods in tests without duplicating code?
CodePudding user response:
First, some minor nitpicks:
- Your test functions get compiled into your release build. Use the pattern described in the rust book to layout your tests.
- I made your
struct
and all of yourimpl
spub
, otherwise they are pretty pointless and I didn't like the warnings :)
So, with that out of the way, lets actually talk about the tests :)
There is no built-in mechanism for Rust for test setup
/teardown
, but it's very easy to implement yourself. You can just use write a simple setup()
function that returns a context object with a drop
implementation for teardown.
You could create your user and store it in the context object. Further, you can run any kind of sideeffects. I'll demonstrate this here by printing some messages.
Here, this is kind of how I would implement a setup
/teardown
mechanism:
pub struct User {
name: String,
age: u8,
weight: f32,
}
impl User {
pub fn new(name: String, age: u8, weight: f32) -> Self {
User {
name: name,
age: age,
weight: weight,
}
}
pub fn get_name(&self) -> &str {
&self.name
}
pub fn get_age(&self) -> u8 {
self.age
}
pub fn get_weight(&self) -> f32 {
self.weight
}
pub fn set_name(&mut self, new_name: String) {
self.name = new_name
}
pub fn set_age(&mut self, new_age: u8) {
self.age = new_age
}
pub fn set_weight(&mut self, new_weight: f32) {
self.weight = new_weight
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestContext {
user: User,
}
impl Drop for TestContext {
fn drop(&mut self) {
println!("Test teardown ...");
}
}
fn setup() -> TestContext {
println!("Test setup ...");
TestContext {
user: User::new(String::from("Bob"), 20, 150.23),
}
}
#[test]
fn test_user_get_name() {
let ctx = setup();
assert_eq!(ctx.user.get_name(), "Bob")
}
#[test]
fn test_user_set_name() {
let mut ctx = setup();
ctx.user.set_name("Jane".to_string()); // no performance cost compared to String::from
assert_eq!(ctx.user.get_name(), "Jane")
}
#[test]
fn test_user_get_age() {
let ctx = setup();
assert_eq!(ctx.user.get_age(), 20)
}
#[test]
fn test_user_set_age() {
let mut ctx = setup();
ctx.user.set_age(25);
assert_eq!(ctx.user.get_age(), 25)
}
#[test]
fn test_user_get_weight() {
let ctx = setup();
assert_eq!(ctx.user.get_weight(), 150.23)
}
#[test]
fn test_user_set_weight() {
let mut ctx = setup();
ctx.user.set_weight(160.8);
assert_eq!(ctx.user.get_weight(), 160.8)
}
}
> cargo test
running 6 tests
test tests::test_user_get_age ... ok
test tests::test_user_get_name ... ok
test tests::test_user_get_weight ... ok
test tests::test_user_set_age ... ok
test tests::test_user_set_name ... ok
test tests::test_user_set_weight ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
If you add --nocapture
, you can see the output of the setup/teardown println
messages:
> cargo test -- --nocapture
running 6 tests
Test setup ...
Test teardown ...
Test setup ...
Test teardown ...
Test setup ...
Test teardown ...
Test setup ...
Test teardown ...
test tests::test_user_get_age ... okTest setup ...
Test setup ...
Test teardown ...
Test teardown ...
test tests::test_user_get_name ... ok
test tests::test_user_get_weight ... ok
test tests::test_user_set_age ... ok
test tests::test_user_set_weight ... ok
test tests::test_user_set_name ... ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Important: Side effects
By default, cargo test
runs tests in parallel on multiple threads. If your setup
/teardown
methods have side effects that aren't thread-safe, you might want to force single-threaded test execution with the --test-threads 1
flag:
> cargo test -- --nocapture --test-threads 1
running 6 tests
test tests::test_user_get_age ... Test setup ...
Test teardown ...
ok
test tests::test_user_get_name ... Test setup ...
Test teardown ...
ok
test tests::test_user_get_weight ... Test setup ...
Test teardown ...
ok
test tests::test_user_set_age ... Test setup ...
Test teardown ...
ok
test tests::test_user_set_name ... Test setup ...
Test teardown ...
ok
test tests::test_user_set_weight ... Test setup ...
Test teardown ...
ok
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s