#unit-testing #unit #runner #test-framework #aaa #test-cases

test4a

Testing library that provides some tools to apply "Advanced" Arrange-Act-Assert testing design

2 releases

0.1.1 Jun 23, 2019
0.1.0 Jun 23, 2019

#396 in Testing

MIT/Apache

43KB
864 lines

Test4A

Testing library written in Rust that provides some tools to apply "Advanced" Arrange-Act-Assert testing design.

Table of Contents

Introduction

Arrange-Act-Assert is a widely used way to design unit tests. When applied, each use case can be written with the following form:

#[test]
fn test_use_case() {
    // Arrange    
    // Act
    // Assert
}

This technique ensures that our tests are clear and well delimited. However, if we want to ensure that all methods of a type work correctly, we have to defined a lot of use cases, and so the test code quickly grows.
The main idea behind Test4A is to define use cases in a compact and reusable way, in order to write unit tests more efficiently.

Here are the main ideas of this library:

  • An alternative way to define tests in order to be more readable
  • More assertion types
  • A simple way to define custom assertions
  • Support of panic testing
  • A clear description of the error and detailed information about the use case when a test fails
  • A great integration with tools provided by Rust and Cargo
  • No restriction for the types that are tested (some testing frameworks require that the type implements Clone, but no trait is required with Test4A)
  • Not a single necessary macro

Installation

To use Test4A in your crate, simply include this in your Cargo.toml:

[dev_dependencies]
test4a = "0.1"

Usage

To clearly understand how to use Test4A, let's write some unit tests for the usize type.

First use case

We can start by writing our first use case:

#[cfg(test)]
mod tests {
    use test4a::{Equal, Runner};

    struct Expected {
        value: usize,
    }

    #[test]
    fn test_from_0() {
        Runner::arrange(|message| {
            message.set("Value of 0");
            0
        })
        .act(
            |message, value| {
                message.set("Add 1");
                *value += 1;
            },
            || Expected { value: 1 },
        )
        .assert(|message, value, expected| {
            message.set("Value is correct");
            Equal::new(value, expected.value)
        });
    }
}

Here, we define a test that init the usize variable to 0, add 1 and test that the value is effectively 1. The code is organized with a Arrange-Act-Assert design, as we can do classically.

Expect is a type you define to pass data between the Act step and the Assert one. You can use the message parameter to define a custom label for each step. These labels will be printed for the failing cases in order to quickly find which use case has failed.

Multiple use cases

In the previous section, we have defined a single use case. However, we can see that a lot of code is written to only define it. Test4A becomes interesting when we want to define multiple cases.

First of all, let's define some initial states for our usize object:

  • x = 0 (zero)
  • x = 1 (non zero value)

For each initial state, we want to test some actions:

  • x += 0 (add zero)
  • x += 1 (add a non zero value)
  • x -= 1 (subtract a non zero value)

We can write a function for each action:

fn add_0(message: &mut Message, value: &mut usize) {
    message.set("Add 0");
    *value += 0;
}

fn add_1(message: &mut Message, value: &mut usize) {
    message.set("Add 1");
    *value += 1;
}

fn subtract_1(message: &mut Message, value: &mut usize) {
    message.set("Subtract 1");
    *value -= 1;
}

Then, we can define all the assertions we want to check. Here, we only want to ensure the value is correct after the execution of an action. Here is the corresponding function:

fn value_expected(
    message: &mut Message,
    value: usize,
    expected: Expected,
) -> Equal<usize> {
    message.set("Value is correct");
    Equal::new(value, expected.value)
}

The function should return a type that implement the Assert trait. Equal is one of those types that are included in the library. More information can be found in the Assertions section.

Now, we can define a runner for each initial state, and use the previous defined functions to know what actions and assertions to execute:

#[test]
fn test_from_0() {
    Runner::arrange(|message| {
        message.set("Initial value of 0");
        0
    })
    .act(add_0, || Expected { value: 0 })
    .act(add_1, || Expected { value: 1 })
    .act(subtract_1, || Expected { value: 0 }) // This test will fail, see next section
    .assert(value_expected);
}

#[test]
fn test_from_1() {
    Runner::arrange(|message| {
        message.set("Initial value of 1");
        1
    })
    .act(add_0, || Expected { value: 1 })
    .act(add_1, || Expected { value: 2 })
    .act(subtract_1, || Expected { value: 0 })
    .assert(value_expected);
}

All possible combinations of action/assertion will be executed independently (the arrange step is executed for each case).

You can define as actions and assertions as you want in each runner.
It has to be noted that in case of parallel execution with cargo test, all use cases of a runner are executed sequentially.

Ensure a code panics

If you execute the test defined above, one test will fail. This is when we try to subtract 1 to 0. In this case, we don't expect a value for the usize object, but instead that the code panics.

To define an action that should panic for a given arrangement, you can use act_panic:

Runner::arrange(|message| {
    message.set("Initial value of 0");
    0
})
.act(add_0, || Expected { value: 0 })
.act(add_1, || Expected { value: 1 })
.act_panic(PanicWhen::Debug, subtract_1)
.assert(expect_value);

Here, we indicate to Test4A that the substract_1 action should panic only when the code is built in debug mode.
The use case defined by this action is immediately stopped after that: the following assertions are not executed.

In the same way, you can use assert_panic to assert that a code panics while testing an assertion.

Assertions

In the previous sections, we have seen one type of assertion defined by Test4A: Equal.
To provide more assertions and control them, the library defines an assertion with a type that implement Assert.

More assertions are available:

  • compare two values (Equal, NotEqual, Greater, Less, GreaterEqual, LessEqual)
  • test a boolean (True, False)
  • ensure a value is included in a vector (Contains)
  • group multiple assertions defined by iterators (Multiple)

You can also create your own assertion types by implementing the Assert trait.

License

Licensed under either of

at your option.

No runtime deps