mirror of
https://github.com/kata-containers/kata-containers.git
synced 2025-04-27 19:35:32 +00:00
This PR removes the jenkins reference from unit testing presentation as this is not longer supported on the kata containers project. Signed-off-by: Gabriela Cervantes <gabriela.cervantes.tellez@intel.com>
331 lines
7.6 KiB
Markdown
331 lines
7.6 KiB
Markdown
## Why write unit tests?
|
|
|
|
- Catch regressions
|
|
|
|
- Improve the code being tested
|
|
|
|
Structure, quality, security, performance, "shakes out" implicit
|
|
assumptions, _etc_
|
|
|
|
- Extremely instructive
|
|
|
|
Once you've fully tested a single function, you'll understand that
|
|
code very well indeed.
|
|
|
|
## Why write unit tests? (continued)
|
|
|
|
- Fun!
|
|
|
|
Yes, really! Don't believe me? Try it! ;)
|
|
|
|
## Run all Kata Containers agent unit tests
|
|
|
|
As an example, to run all agent unit tests:
|
|
|
|
```bash
|
|
$ cd $GOPATH/src/github.com/kata-containers/kata-containers
|
|
$ cd src/agent
|
|
$ make test
|
|
```
|
|
|
|
## List all unit tests
|
|
|
|
- Identify the full name of all the tests _in the current package_:
|
|
|
|
```bash
|
|
$ cargo test -- --list
|
|
```
|
|
|
|
- Identify the full name of all tests in the `foo` "local crate"
|
|
(sub-directory containing another `Cargo.toml` file):
|
|
|
|
```bash
|
|
$ cargo test -p "foo" -- --list
|
|
```
|
|
|
|
## Run a single unit test
|
|
|
|
- Run a test in the current package in verbose mode:
|
|
|
|
```bash
|
|
# Example
|
|
$ test="config::tests::test_get_log_level"
|
|
|
|
$ cargo test "$test" -vv -- --exact --nocapture
|
|
```
|
|
|
|
## Test coverage setup
|
|
|
|
```bash
|
|
$ cargo install cargo-tarpaulin
|
|
```
|
|
|
|
## Show test coverage
|
|
|
|
```bash
|
|
$ cd $GOPATH/src/github.com/kata-containers/kata-containers/src/agent
|
|
$ cargo -v tarpaulin --all-features --run-types AllTargets --count --force-clean -o Html
|
|
$ xdg-open "file://$PWD/tarpaulin-report.html"
|
|
```
|
|
|
|
## Testability (part 1)
|
|
|
|
- To be testable, a function should:
|
|
- Not be "too long" (say >100 lines).
|
|
- Not be "too complex" (say >3 levels of indentation).
|
|
- Should return a `Result` or an `Option` so error paths
|
|
can be tested.
|
|
|
|
- If functions don't conform, they need to be reworked (refactored)
|
|
before writing tests.
|
|
|
|
## Testability (part 2)
|
|
|
|
- Some functions can't be fully tested.
|
|
- However, you _can_ test the initial code that checks
|
|
the parameter values (test error paths only).
|
|
|
|
## Writing new tests: General advice (part 1)
|
|
|
|
- KISS: Keep It Simple Stupid
|
|
|
|
You don't get extra points for cryptic code.
|
|
|
|
- DRY: Don't Repeat Yourself
|
|
|
|
Make use of existing facilities (don't "re-invert the wheel").
|
|
|
|
- Read the [unit test advice document](https://github.com/kata-containers/kata-containers/blob/main/docs/Unit-Test-Advice.md)
|
|
|
|
## Writing new tests: General advice (part 2)
|
|
|
|
- Attack the function in all possible ways
|
|
|
|
- Use the _table driven_ approach:
|
|
- Simple
|
|
- Compact
|
|
- Easy to debug
|
|
- Makes boundary analysis easy
|
|
- Encourages functions to be testable
|
|
|
|
## Writing new tests: Specific advice (part 1)
|
|
|
|
- Create a new "`tests`" module if necessary.
|
|
- Give each test function a "`test_`" prefix.
|
|
- Add the "`#[test]`" annotation on each test function.
|
|
|
|
## Writing new tests: Specific advice (part 2)
|
|
|
|
- If you need to `use` (import) packages for the tests,
|
|
_only do it in the `tests` module_:
|
|
```rust
|
|
use some_test_pkg::{foo, bar}; // <-- Not here
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use some_test_pkg:{foo, bar}; // <-- Put it here
|
|
}
|
|
```
|
|
|
|
## Writing new tests: Specific advice (part 3)
|
|
|
|
- You can add test-specific dependencies in `Cargo.toml`:
|
|
```toml
|
|
[dev-dependencies]
|
|
serial_test = "0.5.1"
|
|
```
|
|
|
|
## Writing new tests: Specific advice (part 4)
|
|
|
|
- Don't add in lots of error handling code: let the test panic!
|
|
```rust
|
|
// This will panic if the unwrap fails.
|
|
// - NOT acceptable generally for production code.
|
|
// - PERFECTLY acceptable for test code since:
|
|
// - Keeps the test code simple.
|
|
// - Rust will detect the panic and fail the test.
|
|
let result = func().unwrap();
|
|
```
|
|
|
|
## Debugging tests (part 1)
|
|
|
|
- Comment out all tests in your `TestData` array apart from the failing test.
|
|
|
|
- Add temporary `println!("FIXME: ...")` statements in the code.
|
|
|
|
- Set `RUST_BACKTRACE=full` before running `cargo test`.
|
|
|
|
## Debugging tests (part 2)
|
|
|
|
- Use a debugger (not normally necessary though):
|
|
```bash
|
|
# Disable optimisation
|
|
$ RUSTFLAGS="-C opt-level=0" cargo test --no-run
|
|
|
|
# Find the test binary
|
|
$ test_binary=$(find target/debug/deps | grep "kata_agent-[a-z0-9][a-z0-9]*$" | tail -1)
|
|
|
|
$ rust-gdb "$test_binary"
|
|
```
|
|
|
|
## Useful tips
|
|
|
|
- Always start a test with a "clean environment":
|
|
|
|
Create new set of objects / files / directories / _etc_
|
|
for each test.
|
|
|
|
- Mounts
|
|
- Linux allows mounts on top of existing mounts.
|
|
- Bind mounts and read-only mounts can be useful.
|
|
|
|
## Gotchas (part 1)
|
|
|
|
If a test runs successfully _most of the time_:
|
|
|
|
- Review the test logic.
|
|
|
|
- Add a `#[serial]` annotation on the test function
|
|
Requires the `serial_test` package in the `[dev-dependencies]`
|
|
section of `Cargo.toml`.
|
|
|
|
If this makes it work the test is probably sharing resources with
|
|
another task (thread).
|
|
|
|
## Gotchas (part 2)
|
|
|
|
If a test works locally but fails in the CI, consider the following
|
|
attributes of each environment (local and CI):
|
|
|
|
- The version of rust being used.
|
|
- The hardware architecture.
|
|
- Number (and spec) of the CPUs.
|
|
|
|
## Before raising a PR
|
|
|
|
- Remember to check that the test runs locally:
|
|
- As a non-privileged user.
|
|
- As the `root` user (carefully!)
|
|
|
|
- Run the [static checker](https://github.com/kata-containers/kata-containers/blob/main/tests/static-checks.sh)
|
|
on your changes.
|
|
|
|
Checks formatting and many other things.
|
|
|
|
## If in doubt
|
|
|
|
- Ask for help! ;)
|
|
|
|
## Quiz 1
|
|
|
|
What's wrong with this function?
|
|
|
|
```rust
|
|
fn foo(config: &Config, path_prefix: String, container_id: String, pid: String) -> Result<()> {
|
|
let mut full_path = format!("{}/{}", path_prefix, container_id);
|
|
|
|
let _ = remove_recursively(&mut full_path);
|
|
|
|
write_number_to_file(pid, full_path);
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
## Quiz 1: Answers (part 1)
|
|
|
|
- No check that `path_prefix`, `container_id` and `pid` are not `""`.
|
|
- No check that `path_prefix` is absolute.
|
|
- No check that `container_id` does not contain slashes / contains only valid characters.
|
|
- Result of `remove_recursively()` discarded.
|
|
- `remove_recursively()` _may_ modify `full_path` without `foo()` knowing!
|
|
|
|
## Quiz 1: Answers (part 2)
|
|
|
|
- Why is `pid` not a numeric?
|
|
- No check to ensure the PID is positive.
|
|
- No check to recreate any directories in the original `path_prefix`.
|
|
- `write_number_to_file()` could fail so why doesn't it return a value?
|
|
- The `config` parameter is unused.
|
|
|
|
## Quiz 1: What if...
|
|
|
|
Imagine if the caller managed to do this:
|
|
|
|
```rust
|
|
foo(config, "", "sbin/init", r#"#!/bin/sh\n/sbin/reboot"#);
|
|
```
|
|
|
|
## Quiz 2
|
|
|
|
What makes this function difficult to test?
|
|
|
|
```rust
|
|
fn get_user_id(username: String) -> i32 {
|
|
let line = grep_file(username, "/etc/passwd").unwrap();
|
|
let fields = line.split(':');
|
|
|
|
let uid = fields.nth(2).ok_or("failed").unwrap();
|
|
|
|
uid.parse::<i32>()
|
|
}
|
|
```
|
|
|
|
## Quiz 2: Answers (part 1)
|
|
|
|
- Unhelpful error message ("failed").
|
|
|
|
- Panics on error! Return a `Result` instead!
|
|
|
|
- UID's cannot be negative so function should return an unsigned
|
|
value.
|
|
|
|
## Quiz 2: Answers (part 2)
|
|
|
|
- Hard-coded filename.
|
|
|
|
This would be better:
|
|
|
|
```rust
|
|
const PASSWD_DB: &str = "/etc/passwd";
|
|
|
|
// Test code can now pass valid and invalid files!
|
|
fn get_user_id(filename: String, username: String) -> i32 {
|
|
// ...
|
|
}
|
|
|
|
let id = get_user_id(PASSWD_DB, username);
|
|
```
|
|
|
|
## Quiz 3
|
|
|
|
What's wrong with this test code?
|
|
|
|
```rust
|
|
let mut obj = Object::new();
|
|
|
|
// Sanity check
|
|
assert_eq!(obj.num, 0);
|
|
assert_eq!(obj.wibble, false);
|
|
|
|
// Test 1
|
|
obj->foo_method(7);
|
|
assert_eq!(obj.num, 7);
|
|
|
|
// Test 2
|
|
obj->bar_method(true);
|
|
assert_eq!(obj.wibble, true);
|
|
```
|
|
|
|
## Quiz 3: Answers
|
|
|
|
- The test code is "fragile":
|
|
- The 2nd test re-uses the object created in the first test.
|
|
|
|
## Finally
|
|
|
|
- [We need a GH action to run the unit tests](https://github.com/kata-containers/kata-containers/issues/2934)
|
|
|
|
Needs to fail PRs that decrease test coverage<br/> by "x%".
|