laboratory

A simple, expressive unit test framework for Rust

GitHub Workflow Status (branch) Crates.io Crates.io

Features

Installation

In Cargo.toml: toml [dev-dependencies] laboratory = "*" Then in your test files ```rust

[cfg(test)]

mod tests { use laboratory::{describe, describeskip, it, itskip, it_only, expect}; } ```

Getting Started

Testing a simple function

```rust // from examples/simple.rs fn main() { add_one(0); }

// Here we have one function that does // one thing: Adds one to whatever number // we pass to it. fn add_one (n: u64) -> u64 { n + 1 }

[cfg(test)]

mod tests {

// lets pull our add_one function into scope
use super::*;

// now let's pull in our lab tools into scope
// to test our function
use laboratory::{describe, it, expect};

// From Rust's perspective we will only define
// one test, but inside this test we can define
// however many test we need.
#[test]
fn suite() {

    // let's describe what our add_one function will do.
    // Notice the method "specs" which takes a Vec as it's
    // argument. Inside this vec is where we will define
    // the tests related to add_one.
    describe("add_one()").specs(vec![

        // when describing what it should do, feel free to be
        // as expressive as you would like.
        it("should return 1 when passed 0", |_| {

            // here we will use the default expect function
            // that comes with laboratory.
            // We expect the result of add_one(0) to equal 1
            expect(add_one(0)).to_equal(1)

        }),

        // just as a sanity check, let's add a second test
        it("should return 2 when passed 1", |_| {

            expect(add_one(1)).to_equal(2)

        })

    ]).run();

}

} ```

Then run: shell script $ cargo test -- --nocapture

Result:
```

running 1 test

add_one() ✓ should return 1 when passed 0 (0ms) ✓ should return 2 when passed 1 (0ms)

✓ 2 tests completed (0ms)

test tests::suite ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

```

Nested suites

```rust // from examples/nested-suites.rs

// Here we will define a struct with // two members, an associated function, // and two methods. struct Foo { line: String, count: i32 } impl Foo { pub fn new() -> Foo { Foo { line: String::new(), count: 0 } } pub fn append(&mut self, str: &str) { self.line += str; } pub fn increase(&mut self) { self.count += 1; } }

fn main () { let mut foo = Foo::new(); foo.append("fizzbuzz"); foo.increase(); }

[cfg(test)]

mod tests {

// Pull the Foo struct into scope
use super::*;

// Now pull in the lab tools
use laboratory::{describe, it, expect};

// define single test
#[test]
fn test() {

    // Now we can describe Foo.
    // Notice the "suites" method that takes a Vec
    // as its argument. This is where we can describe
    // Foo's members and methods.
    describe("Foo").suites(vec![

        // Here we describe the "new" associated function
        describe("#new()").specs(vec![

            it("should return an instance of Foo with two members", |_| {

                let foo = Foo::new();
                expect(foo.line).to_be(String::new())?;
                expect(foo.count).to_equal(0)

            })

        ]),

        // Now we will describe the "append" method
        describe("#append()").specs(vec![

            it("should append \"fizzbuzz\" to Foo#line", |_| {

                let mut foo = Foo::new();
                foo.append("fizzbuzz");
                expect(foo.line).to_be("fizzbuzz".to_string())

            })

        ]),

        // Finally, we will describe the "increase" method
        describe("#increase()").specs(vec![

            it("should increase Foo#count by 1", |_| {

                let mut foo = Foo::new();
                foo.increase();
                expect(foo.count).to_equal(1)

            })

        ])

    ])
    .run();

}

}

Result: text

running 1 test

Foo #new() ✓ should return an instance of Foo with two members (0ms) #append() ✓ should append "fizzbuzz" to Foo#line (0ms) #increase() ✓ should increase Foo#count by 1 (0ms)

✓ 3 tests completed (0ms)

test tests::test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

```

Failed Tests

```rust // from examples/failure.rs

// addone should add one to any number, // but a typo results in 6 being added fn addone (n: i32) -> i32 { n + 6 }

fn add_two (n: i32) -> i32 { n + 2 }

fn main() { addone(0); addtwo(0); }

[cfg(test)]

mod tests {

// let call in our functions
use super::*;

// and now let's bring in our lab tools
use laboratory::{describe,it,expect};

// define our single rust test
#[test]
fn test() {

    // We have two different functions that we
    // want to test in our crate. So, let's
    // describe our crate and nest our functions
    // under that umbrella.
    describe("Crate").suites(vec![

        describe("add_one()").specs(vec![

            it("should return 1 to when passed 0", |_| {

                expect(add_one(0)).to_equal(1)

            })

        ]),

        describe("add_two()").specs(vec![

            it("should return 2 to when passed 0", |_| {

                expect(add_two(0)).to_equal(2)

            })

        ])

    ]).run();

}

}

```

Result: ```text

running 1 test

Crate addone() 0) should return 1 to when passed 0 (0ms) addtwo() ✓ should return 2 to when passed 0 (0ms)

✖ 1 of 2 tests failed:

0) add_one() should return 1 to when passed 0: Expected 6 to equal 1

test tests::test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

```

Hooks

```rust // from examples/hooks.rs

fn alwaysreturntrue() -> bool { true }

fn main() { alwaysreturntrue(); }

[cfg(test)]

mod tests {

use super::always_return_true;
use laboratory::{describe, it, expect};

#[test]
fn test() {

    // In this suite we want to use hooks to
    // perform actions before and after our tests.
    // The actions we to run in this scenario is simply
    // outputting to to stdout.
    describe("always_return_true")

        // We want to run this action before all
        // all tests in this suite is ran. This action
        // will only be ran once.
        .before_all(|_| {

            println!("\n\n  before hook called");

        })

        // We want to run this action just before every test
        // in this suite. Since we have two tests this action
        // will be ran twice.
        .before_each(|_| {

            println!("  before_each hook called");

        })

        // likewise, we also have actions we want to run
        // after our tests.
        .after_each(|_| {

            println!("  after_each hook called");

        }).after_all(|_| {

            println!("  after_all hook called");

        }).specs(vec![

            it("should return true", |_| {
                expect(always_return_true()).to_be(true)
            }),

            it("should return true again", |_| {
                expect(always_return_true()).to_be(true)
            })

        ]).run();

}

}

Result: text

running 1 test

before hook called beforeeach hook called aftereach hook called beforeeach hook called aftereach hook called after_all hook called

alwaysreturntrue ✓ should return true (0ms) ✓ should return true again (0ms)

✓ 2 tests completed (0ms)

test tests::test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

```

Using State

```rust // from examples/state.rs

fn alwaysreturntrue() -> bool { true } fn addone(n: i32) -> i32 { n + 1 } fn addtwo(n: i32) -> i32 { n + 2 }

fn main() { let true = alwaysreturntrue(); let _one = addone(0); let two = addtwo(0); }

[cfg(test)]

mod tests {

use super::*;
use laboratory::{describe, it, expect, Deserialize, Serialize, State};
use std::fmt::{Debug};

// We want a counter to count each time a hook or test is called
// Note that any state we want to use in the suite
// must be able to be serialized and deserialized by serde.

#[derive(Deserialize, Serialize, Debug)]
struct Counter {
    suite: String, // the name of the suite
    call_count: u8 // the number of times a hook or test was called
}

impl Counter {
    fn new(suite: &str) -> Counter {
        Counter {
            suite: String::from(suite),
            call_count: 0
        }
    }
    fn update(&mut self) {
        self.call_count += 1;
        println!("  {} hit count: {}", self.suite, self.call_count);
    }
}

#[test]
fn test() {

    // Here we will define a function to handle all the hook calls
    fn hook_handle(state: &mut State) {

        // We need to call the get_state method in order to get the counter.
        // We also we to tell the Rust compiler what
        // type the result of get_state will be which
        // in this case is the counter.
        let mut counter: Counter = state.get_state();

        // Now we will call the update method on Counter
        counter.update();

        // And if we want to update the state we need to call set_state
        state.set_state(counter);
    }

    // In this example we want to return the state
    // after all the tests are ran so that we can echo the
    // the final result to stdout.
    let state: Counter = describe("My Crate")

        // We can give the suite the initial state by
        // using the state method, but we could very well
        // skip using the state method and define the state
        // in the before_all or even in the before_each hook.
        .state(Counter::new("Parent Level"))

        // Now we will define our hooks
        .before_all(hook_handle)
        .before_each(hook_handle)
        .after_each(hook_handle)
        .after_all(hook_handle)

        .suites(vec![

            // this suite will inherit the parent's state
            describe("add_one()")

                // Here is the set of hooks for the child suite
                .before_all(hook_handle)
                .before_each(hook_handle)
                .after_each(hook_handle)
                .after_all(hook_handle)


                .specs(vec![

                    it("should return 1", |state| {
                        hook_handle(state);
                        expect(add_one(0)).to_be(1)
                    }),

                    it("should return 2", |state| {
                        hook_handle(state);
                        expect(add_one(1)).to_be(2)
                    })

                ])
                .inherit_state(),

            // This suite will use its own state
            describe("add_two()")

                // since this suite will not inherit state
                // from the parent we will give it a new one.
                .state(Counter::new("Child Level"))

                // Here is the set of hooks for the second child suite
                .before_all(hook_handle)
                .before_each(hook_handle)
                .after_each(hook_handle)
                .after_all(hook_handle)
                .specs(vec![

                    it("should return 2", |state| {
                        hook_handle(state);
                        expect(add_two(0)).to_be(2)
                    }),

                    it("should return 4", |state| {
                        hook_handle(state);
                        expect(add_two(2)).to_be(4)
                    })

                ]),

            // this suite will also inherit the parent's state
            describe("always_return_true()")

                // Here is the set of hooks for the child suite
                .before_all(hook_handle)
                .before_each(hook_handle)
                .after_each(hook_handle)
                .after_all(hook_handle)


                .specs(vec![

                    it("should always return true", |state| {
                        hook_handle(state);
                        expect(add_one(0)).to_be(1)
                    })

                ])
                .inherit_state()


        ]).run().to_state();

    println!("{:#?}\n\n", state);

}

}

Result: text

running 1 test Parent Level hit count: 1 Parent Level hit count: 2 Parent Level hit count: 3 Parent Level hit count: 4 Parent Level hit count: 5 Parent Level hit count: 6 Parent Level hit count: 7 Parent Level hit count: 8 Parent Level hit count: 9 Child Level hit count: 1 Child Level hit count: 2 Child Level hit count: 3 Child Level hit count: 4 Child Level hit count: 5 Child Level hit count: 6 Child Level hit count: 7 Child Level hit count: 8 Parent Level hit count: 10 Parent Level hit count: 11 Parent Level hit count: 12 Parent Level hit count: 13 Parent Level hit count: 14 Parent Level hit count: 15

My Crate addone() ✓ should return 1 (0ms) ✓ should return 2 (0ms) addtwo() ✓ should return 2 (0ms) ✓ should return 4 (0ms) alwaysreturntrue() ✓ should always return true (0ms)

✓ 5 tests completed (0ms)

Counter { suite: "Parent Level", call_count: 15, }

test tests::test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

```

Testing Large Packages Divided by Modules

```rust // from examples/importing-tests.rs

fn main() { addone::addone(0); multiplybytwo::multiplybytwo(1); }

// In this crate we have two // public modules: addone, and multiplyby_two

pub mod add_one {

// here is a function that we want to test
pub fn add_one (x: u64) -> u64 { x + 1 }


#[cfg(test)]
pub mod tests {

    use super::*;
    use laboratory::{describe, it, expect, Suite};

    // here is where we will define our suite.
    // Notice that this function returns a Suite struct.
    // Also notice that no other methods are called on this suite.
    pub fn suite() -> Suite {

        describe("add_one()").specs(vec![

            it("should return 1", |_| {
                expect(add_one(0)).to_equal(1)
            }),

            it("should return 2", |_| {
                expect(add_one(1)).to_equal(2)
            })

        ])

    }

}

}

// here is our second module pub mod multiplybytwo {

// ...and the function we want to test
pub fn multiply_by_two (x: u64) -> u64 { x * 2 }

#[cfg(test)]
pub mod tests {

    use super::*;
    use laboratory::{describe, it, expect, Suite};

    // Again, we will define a function that returns a Suite struct
    pub fn suite() -> Suite {

        describe("multiply_by_two()").specs(vec![

            it("should return 2", |_| {
                expect(multiply_by_two(1)).to_equal(2)
            }),

            it("should return 4", |_| {
                expect(multiply_by_two(2)).to_equal(4)
            })

        ])

    }

}

}

// Now here is where we will import and run our // tests under one umbrella of the crate.

[cfg(test)]

mod tests {

// pull our modules into scope
use super::*;

// pull in our lab tools
use laboratory::{describe};

#[test]
fn test() {

    // Describe the crate.
    describe("My Crate")
        .suites(vec![

            // now we will call our functions that simply
            // returns a Suite struct.
            add_one::tests::suite(),
            multiply_by_two::tests::suite()

        ])

        // Now we can run our tests with any other options
        .run();

}

}

Result: text

running 1 test

My Crate addone() ✓ should return 1 (0ms) ✓ should return 2 (0ms) multiplyby_two() ✓ should return 2 (0ms) ✓ should return 4 (0ms)

✓ 4 tests completed (0ms)

test tests::test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

```

Optional Reporting Styles: Spec, Minimum, JSON, and JSON-Pretty

```rust // from examples/reporting-json-pretty.rs

fn main() { let one = addone(0); let two = addtwo(0); }

fn addone (x: u64) -> u64 { x + 1 } fn addtwo (x: u64) -> u64 { x + 5 }

[cfg(test)]

mod tests {

use super::*;
use laboratory::{describe, it, expect};

#[test]
fn suite() {

    // To export to json-pretty we will simply call
    // the json_pretty method on the suite.
    describe("My Crate").suites(vec![

        describe("add_one()").specs(vec![

            it("should return 1", |_| {
                expect(add_one(0)).to_equal(1)
            }),

            it("should return 2", |_| {
                expect(add_one(1)).to_equal(2)
            })

        ]),

        describe("add_two()").specs(vec![

            it("should return 2", |_| {
                expect(add_two(0)).to_equal(2)
            })

        ])

    ]).json_pretty().run();

}

} Result: text

running 1 test

{ "name": "My Crate", "passing": 2, "failing": 1, "ignored": 0, "childsuites": [ { "name": "addone()", "passing": 2, "failing": 0, "ignored": 0, "childsuites": [], "childtests": [ { "name": "should return 1", "fullname": "addone() should return 1", "pass": true, "errormsg": null, "duration": 0 }, { "name": "should return 2", "fullname": "addone() should return 2", "pass": true, "errormsg": null, "duration": 0 } ], "duration": 0 }, { "name": "addtwo()", "passing": 0, "failing": 1, "ignored": 0, "childsuites": [], "childtests": [ { "name": "should return 2", "fullname": "addtwo() should return 2", "pass": false, "errormsg": "Expected 5 to equal 2", "duration": 0 } ], "duration": 0 } ], "child_tests": [], "duration": 0 }

test tests::suite ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

```