Unit test를 작성 하다 보면 간단한 함수에 대해서도 꽤나 많은 수의 테스트 케이스를 작성하게 됩니다. Input 값의 조합, 특히나 Invalid Input 값의 조합은 무지 많아질 수 있기 때문 입니다. 그 모든 케이스를 하나 씩 테스트로 작성 하는 것은 꽤 귀찮기도 하고, 테스트 함수 작명 지옥에 빠지면서 테스트 가독성도 떨어지게 됩니다.
이럴 때 사용할 수 있는 것이 table-driven test, 혹은 parameterised test 입니다. Input-Output 조합을 table로 표현하는 방식입니다.
Rust로 간단한 함수의 parameterised test를 작성해 보고, rust의 test-case
, rtest
crate를 활용해 이를 더 간편하게 작성하는 방법을 알아보겠습니다.
Parameterised Test#
다음과 같은 아주 아주 간단한 함수의 테스트를 작성해 본다고 합시다.(test-case
crate repo의 기본 예시 입니다 ^^) 두 개의 i8
값을 받아 두 수의 곱의 절대값을 반환하는 함수입니다.
1
2
3
|
pub fn multiplication(x: i8, y: i8) -> i8 {
(x * y).abs()
}
|
Input의 두 i8
각각이 음수 & 양수일 때 절대값을 돌려주는 지 확인하는 테스트를 작성해 보겠습니다. 너무 간단해서 와닿지 않을 수 있겠지만 예시로만 참고해 주세요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn multiplication_tests_negative_negative() {
let x = -2;
let y = -4;
let actual = multiplication(x, y);
assert_eq!(8, actual);
}
#[test]
fn multiplication_tests_negative_positive() {
let x = -2;
let y = 4;
let actual = multiplication(x, y);
assert_eq!(-8, actual);
}
#[test]
fn multiplication_tests_positive_negative() {
let x = 2;
let y = -4;
let actual = multiplication(x, y);
assert_eq!(-8, actual);
}
#[test]
fn multiplication_tests_positive_positive() {
let x = 2;
let y = 4;
let actual = multiplication(x, y);
assert_eq!(8, actual);
}
}
|
실행 결과
1
2
3
4
5
6
7
|
running 4 tests
test tests::multiplication_tests_negative_negative ... ok
test tests::multiplication_tests_negative_positive ... ok
test tests::multiplication_tests_positive_negative ... ok
test tests::multiplication_tests_positive_positive ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
양수, 음수의 조합 4가지를 테스트로 작성하였습니다. 간단한 함수의 테스트인데 꽤 많은 코드가 필요합니다. 게다가 테스트 fail 발생할 경우 각 테스트를 하나 하나 읽으며 의도를 파악해야 합니다. 이 예제는 간단하여 금방 의도를 파악할 수 있겠으나, 코드가 조금만 더 복잡해진다면 배로 힘들어 질 것 같습니다. 게다가 만약 이 함수가 절대값이 아닌 곱셈 결과를 반환하도록 바뀌어야 한다면, 테스트 케이스 중 바뀌어야 하는 expected 값을 찾는 게 조금 힘들 수 있겠습니다.
이를 개선하기 위해 여기에 parameterised test를 적용하면 테스트 코드는 다음과 같이 바뀌게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn multiplication_tests() {
let test_cases = vec![
(-2, -4, 8, "when negative-negative value provided"),
(-2, 4, 8, "when negative-positive value provided"),
(2, -4, 8, "when positive-negative value provided"),
(2, 4, 8, "when positive-positive value provided"),
];
for (arg1, arg2, expected, err_msg) in test_cases {
let actual = multiplication(arg1, arg2);
assert_eq!(expected, actual, "Result is wrong {}", err_msg);
}
}
}
|
test case table에 input값, 예상 값과 함께 실패했을 경우 출력할 error message를 정의하였습니다. Fail 발생 시 적절한 error message를 출력하기 위해서 입니다. 만약 multiplication
함수에서 abs()
를 제거한다면 다음과 같이 적절한 에러 메시지가 출력될 것 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
running 1 test
test tests::multiplication_tests ... FAILED
failures:
---- tests::multiplication_tests stdout ----
thread 'tests::multiplication_tests' panicked at 'assertion failed: `(left == right)`
left: `8`,
right: `-8`: Result is wrong when negative-positive value provided', src/bin/test-cases.rs:21:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::multiplication_tests
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass '--bin test-cases'
|
test case table을 만드는 것 만으로도 테스트 코드가 간결해 졌습니다. 다른 모든 unit test code에도 동일하게 적용할 수 있겠습니다. 다만 매 번 table 을 만들고 for loop 를 만드는 수고로움이 있습니다. 이 부분은 test case table 생성을 도와주는 rust crate들의 도움을 받을 수 있습니다!
test-case#
test-case crate를 사용하면 macro로 test case table을 만들 수 있습니다. 위에서 작성한 test case table을 test_case
macro를 사용하여 수정하면 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[test_case(-2, -4 => 8; "when negative-negative value provided")]
#[test_case(-2, 4 => 8; "when negative-positive value provided")]
#[test_case(2, -4 => 8; "when positive-negative value provided")]
#[test_case(2, 4 => 8; "when positive-positive value provided")]
fn multiplication_test(x: i8, y: i8) {
let actual = multiplication(x, y);
assert_eq!(8, actual)
}
}
|
실행 결과는 다음과 같습니다. 실행 결과에서 유추할 수 있듯, 각 macro의 마지막에 적은 string 이 곧 test case 이름이 됩니다.
1
2
3
4
5
6
7
|
running 4 tests
test tests::multiplication_test::when_negative_negative_value_provided ... ok
test tests::multiplication_test::when_positive_positive_value_provided ... ok
test tests::multiplication_test::when_negative_positive_value_provided ... ok
test tests::multiplication_test::when_positive_negative_value_provided ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
test case 이름을 지정하지 않으면 test case name은 input, output 값을 가지고 test case를 생성해 냅니다. 여기서 i8 input의 positive/negative는 구별하지 못하는 것 같더라고요. (-2, 4 => 8)과 (2, 4 => 8)이 같은 이름으로 생성되 에러가 발생하니 참고하세요.
1
2
3
4
5
6
7
|
#[test_case(-2, -4 => 8)]
#[test_case(-3, 4 => 12)]
#[test_case(1, -4 => 4)]
#[test_case(6, 4 => 24)]
fn multiplication_test(x: i8, y: i8) -> i8{
multiplication(x, y)
}
|
1
2
3
4
5
6
7
|
running 4 tests
test tests::multiplication_test::_1_4_expects_4 ... ok
test tests::multiplication_test::_2_4_expects_8 ... ok
test tests::multiplication_test::_3_4_expects_12 ... ok
test tests::multiplication_test::_6_4_expects_24 ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
그 외에도 expect panic, assert function 지정 등 재미있는 syntax들이 많습니다. 더 자세한 사용법은 link를 참고해 주세요.
rtest#
rstest는 test fixture, parameterised test 등을 제공하는 crate 입니다. 앞에서 소개한 test-case 보다 다양한 기능이 있는 것 같습니다. 오늘은 그 중 parameterised test 작성 부분만 살펴보겠습니다.
위에서 작성한 코드를 그대로 rstest를 사용하여 작성하면 다음과 같습니다. 자동 생성 된 Test case 명이 아쉬운데, 테스트 명을 변경하는 방법은 찾지 못하였어요.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(-2, -4, 8)]
#[case(2, -4, 8)]
#[case(-2, 4, 8)]
#[case(2, 4, 8)]
fn multiplication_test(#[case] x: i8, #[case] y: i8, #[case] expected: i8) {
assert_eq!(multiplication(x, y), expected);
}
}
|
1
2
3
4
5
6
7
|
running 4 tests
test tests::multiplication_test::case_1 ... ok
test tests::multiplication_test::case_2 ... ok
test tests::multiplication_test::case_3 ... ok
test tests::multiplication_test::case_4 ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
사용법도 조금 더 귀찮고 아쉽운 부분이 있긴 한데, rstest
는 Test fixture 제공, Test timeout 설정하기 등등 다른 유용한 기능들이 많습니다. 이 기능들은 좀 더 사용해 보고 공유하겠습니다 :)