mirror of
https://github.com/aljazceru/kata-containers.git
synced 2026-01-26 01:34:23 +01:00
Remove the table of contents in the Unit Test Advice document since GitHub auto-generates these now. See: https://github.com/kata-containers/kata-containers/pull/2023 Signed-off-by: James O. D. Hunt <james.o.hunt@intel.com>
320 lines
7.9 KiB
Markdown
320 lines
7.9 KiB
Markdown
# Unit Test Advice
|
|
|
|
## Overview
|
|
|
|
This document offers advice on writing a Unit Test (UT) in
|
|
[`golang`](https://golang.org) and [`rust`](https://www.rust-lang.org).
|
|
|
|
## Assertions
|
|
|
|
### golang assertions
|
|
|
|
Use the `testify` assertions package to create a new assertion object as this
|
|
keeps the test code free from distracting `if` tests:
|
|
|
|
```go
|
|
func TestSomething(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
err := doSomething()
|
|
assert.NoError(err)
|
|
}
|
|
```
|
|
|
|
### rust assertions
|
|
|
|
Use the standard set of `assert!()` macros.
|
|
|
|
## Table driven tests
|
|
|
|
Try to write tests using a table-based approach. This allows you to distill
|
|
the logic into a compact table (rather than spreading the tests across
|
|
multiple test functions). It also makes it easy to cover all the
|
|
interesting boundary conditions:
|
|
|
|
### golang table driven tests
|
|
|
|
Assume the following function:
|
|
|
|
```go
|
|
// The function under test.
|
|
//
|
|
// Accepts a string and an integer and returns the
|
|
// result of sticking them together separated by a dash as a string.
|
|
func joinParamsWithDash(str string, num int) (string, error) {
|
|
if str == "" {
|
|
return "", errors.New("string cannot be blank")
|
|
}
|
|
|
|
if num <= 0 {
|
|
return "", errors.New("number must be positive")
|
|
}
|
|
|
|
return fmt.Sprintf("%s-%d", str, num), nil
|
|
}
|
|
```
|
|
|
|
A table driven approach to testing it:
|
|
|
|
```go
|
|
import (
|
|
"testing"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestJoinParamsWithDash(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
// Type used to hold function parameters and expected results.
|
|
type testData struct {
|
|
param1 string
|
|
param2 int
|
|
expectedResult string
|
|
expectError bool
|
|
}
|
|
|
|
// List of tests to run including the expected results
|
|
data := []testData{
|
|
// Failure scenarios
|
|
{"", -1, "", true},
|
|
{"", 0, "", true},
|
|
{"", 1, "", true},
|
|
{"foo", 0, "", true},
|
|
{"foo", -1, "", true},
|
|
|
|
// Success scenarios
|
|
{"foo", 1, "foo-1", false},
|
|
{"bar", 42, "bar-42", false},
|
|
}
|
|
|
|
// Run the tests
|
|
for i, d := range data {
|
|
// Create a test-specific string that is added to each assert
|
|
// call. It will be displayed if any assert test fails.
|
|
msg := fmt.Sprintf("test[%d]: %+v", i, d)
|
|
|
|
// Call the function under test
|
|
result, err := joinParamsWithDash(d.param1, d.param2)
|
|
|
|
// update the message for more information on failure
|
|
msg = fmt.Sprintf("%s, result: %q, err: %v", msg, result, err)
|
|
|
|
if d.expectError {
|
|
assert.Error(err, msg)
|
|
|
|
// If an error is expected, there is no point
|
|
// performing additional checks.
|
|
continue
|
|
}
|
|
|
|
assert.NoError(err, msg)
|
|
assert.Equal(d.expectedResult, result, msg)
|
|
}
|
|
}
|
|
```
|
|
|
|
### rust table driven tests
|
|
|
|
Assume the following function:
|
|
|
|
```rust
|
|
// Convenience type to allow Result return types to only specify the type
|
|
// for the true case; failures are specified as static strings.
|
|
pub type Result<T> = std::result::Result<T, &'static str>;
|
|
|
|
// The function under test.
|
|
//
|
|
// Accepts a string and an integer and returns the
|
|
// result of sticking them together separated by a dash as a string.
|
|
fn join_params_with_dash(str: &str, num: i32) -> Result<String> {
|
|
if str == "" {
|
|
return Err("string cannot be blank");
|
|
}
|
|
|
|
if num <= 0 {
|
|
return Err("number must be positive");
|
|
}
|
|
|
|
let result = format!("{}-{}", str, num);
|
|
|
|
Ok(result)
|
|
}
|
|
|
|
```
|
|
|
|
A table driven approach to testing it:
|
|
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_join_params_with_dash() {
|
|
// This is a type used to record all details of the inputs
|
|
// and outputs of the function under test.
|
|
#[derive(Debug)]
|
|
struct TestData<'a> {
|
|
str: &'a str,
|
|
num: i32,
|
|
result: Result<String>,
|
|
}
|
|
|
|
// The tests can now be specified as a set of inputs and outputs
|
|
let tests = &[
|
|
// Failure scenarios
|
|
TestData {
|
|
str: "",
|
|
num: 0,
|
|
result: Err("string cannot be blank"),
|
|
},
|
|
TestData {
|
|
str: "foo",
|
|
num: -1,
|
|
result: Err("number must be positive"),
|
|
},
|
|
|
|
// Success scenarios
|
|
TestData {
|
|
str: "foo",
|
|
num: 42,
|
|
result: Ok("foo-42".to_string()),
|
|
},
|
|
TestData {
|
|
str: "-",
|
|
num: 1,
|
|
result: Ok("--1".to_string()),
|
|
},
|
|
];
|
|
|
|
// Run the tests
|
|
for (i, d) in tests.iter().enumerate() {
|
|
// Create a string containing details of the test
|
|
let msg = format!("test[{}]: {:?}", i, d);
|
|
|
|
// Call the function under test
|
|
let result = join_params_with_dash(d.str, d.num);
|
|
|
|
// Update the test details string with the results of the call
|
|
let msg = format!("{}, result: {:?}", msg, result);
|
|
|
|
// Perform the checks
|
|
if d.result.is_ok() {
|
|
assert!(result == d.result, msg);
|
|
continue;
|
|
}
|
|
|
|
let expected_error = format!("{}", d.result.as_ref().unwrap_err());
|
|
let actual_error = format!("{}", result.unwrap_err());
|
|
assert!(actual_error == expected_error, msg);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Temporary files
|
|
|
|
Always delete temporary files on success.
|
|
|
|
### golang temporary files
|
|
|
|
```go
|
|
func TestSomething(t *testing.T) {
|
|
assert := assert.New(t)
|
|
|
|
// Create a temporary directory
|
|
tmpdir, err := ioutil.TempDir("", "")
|
|
assert.NoError(err)
|
|
|
|
// Delete it at the end of the test
|
|
defer os.RemoveAll(tmpdir)
|
|
|
|
// Add test logic that will use the tmpdir here...
|
|
}
|
|
```
|
|
|
|
### rust temporary files
|
|
|
|
Use the `tempfile` crate which allows files and directories to be deleted
|
|
automatically:
|
|
|
|
```rust
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use tempfile::tempdir;
|
|
|
|
#[test]
|
|
fn test_something() {
|
|
|
|
// Create a temporary directory (which will be deleted automatically
|
|
let dir = tempdir().expect("failed to create tmpdir");
|
|
|
|
let filename = dir.path().join("file.txt");
|
|
|
|
// create filename ...
|
|
}
|
|
}
|
|
|
|
```
|
|
|
|
## User running the test
|
|
|
|
[Unit tests are run *twice*](https://github.com/kata-containers/tests/blob/main/.ci/go-test.sh):
|
|
|
|
- as the current user
|
|
- as the `root` user (if different to the current user)
|
|
|
|
When writing a test consider which user should run it; even if the code the
|
|
test is exercising runs as `root`, it may be necessary to *only* run the test
|
|
as a non-`root` for the test to be meaningful.
|
|
|
|
Some repositories already provide utility functions to skip a test:
|
|
|
|
- if running as `root`
|
|
- if not running as `root`
|
|
|
|
### running golang tests as different users
|
|
|
|
The runtime repository has the most comprehensive set of skip abilities. See:
|
|
|
|
- https://github.com/kata-containers/kata-containers/tree/main/src/runtime/pkg/katatestutils
|
|
|
|
### running rust tests as different users
|
|
|
|
One method is to use the `nix` crate along with some custom macros:
|
|
|
|
```
|
|
#[cfg(test)]
|
|
mod tests {
|
|
#[allow(unused_macros)]
|
|
macro_rules! skip_if_root {
|
|
() => {
|
|
if nix::unistd::Uid::effective().is_root() {
|
|
println!("INFO: skipping {} which needs non-root", module_path!());
|
|
return;
|
|
}
|
|
};
|
|
}
|
|
|
|
#[allow(unused_macros)]
|
|
macro_rules! skip_if_not_root {
|
|
() => {
|
|
if !nix::unistd::Uid::effective().is_root() {
|
|
println!("INFO: skipping {} which needs root", module_path!());
|
|
return;
|
|
}
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn test_that_must_be_run_as_root() {
|
|
// Not running as the superuser, so skip.
|
|
skip_if_not_root!();
|
|
|
|
// Run test *iff* the user running the test is root
|
|
|
|
// ...
|
|
}
|
|
}
|
|
```
|