일반적인 테스트 작성 방법
1. 필요한 데이터나 상태 설정
2. 테스트할 코드 실행
3. 의도한 결과가 나오는지 확인
테스트 함수 작성하기
- 러스트에서 테스트란 test 속성이 애너테이션된 함수. 속성은 러스트 코드 조각에 대한 메타데이터임
- 함수의 fn 이전 중에 #[test] 키워드를 추가하면 테스트 함수로 변경됨
- 테스트는 cargo test 명령으로 실행되며, 이 명령을 실행하면 러스트는 속성이 표시된 함수를 실행하고 결과를 보고하는 테스트 실행 바이너리를 빌드함
- 카고로 새 라이브러리 프로젝트를 생성할 때마다 테스트 함수가 포함된 테스트 모듈이 자동 생성됨
- 이 모듈이 테스트 작성을 위한 템플릿을 제공
- 테스트 할 라이브러리 생성
# adder 라이브러리 프로젝트 생성
cargo new adder --lib
cd adder
// src/lib.rs
// 이 코드가 cargo build가 아닌 cargo test 명령어 실행 시에만 컴파일 및 실행됨을 러스트에 전달
#[cfg(test)]
mod tests {
#[test] // 테스트 함수임을 표시
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4); // result에 대해 정답이 4라고 단언
}
}
- 테스트 실행
# 테스트 실행
cargo test
running 1 test
# 함수 it_works 테스트 결과 OK
test tests::it_works ... ok
# 테스트 전체 요약 (passed: 통과, ignored: 무시됨, measured: 성능 측정 벤치마크, filtered out: 특정 테스트만 실행하면서 필터링 여부
test result: ok. 1 passed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
# 문서 테스트 결과
Doc-tests adder
running 0 tests
test result: ok. 1 passed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
테스트에 실패할 경우에는 실패한 함수와 전체 결과에 `FAILED`로 출력되며, 실패한 자세한 이유를 보여줌
assert! 매크로로 결과 검사하기
어떤 조건이 true임을 보장하는 테스트를 작성할 때는 표준 라이브러리가 제공하는 assert! 매크로가 유용
assert! 매크로는 불리언값으로 평가되는 인수를 전달받음
true 값일 경우 아무일도 일어나지 않고 테스트는 통과됨
false 값일 경우 asser! 매크로는 panic! 매크로를 호출하여 테스트를 실패하도록 만듦
// src/lib.rs
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
// src/lib.rs
#[cfg(test)]
mod tests {
use super::*; // 외부 모듈 전부를 내부 스코프로 가져옴
// 테스트 함수
#[test]
fn larger_can_hold_smaller() {
let larger = Rectangle {
width: 8,
height: 7,
};
let smaller = Rectangle {
width: 5,
height: 1,
};
assert!(larger.can_hold(&Smaller));
}
}
assert_eq!, assert_ne! 매크로를 이용한 동등 테스트
두 매크로는 각각 두 인수를 비교하고 동등한지 그렇지 않은지를 판단
단언 코드가 실패하면 두 값을 출력하여 테스트의 실패사유를 더 알기 쉽게 보여줌
assert! 매크로에 ==을 사용하면 동일한 테스트가 가능하지만 표현식이 false임을 알려줄 뿐 어떤 값으로 인해 false 값이 나왔는지 출력하지 않음
assert_eq는 이 값이 되어야함을 단언, assert_ne!는 이 값이 되면 안된다를 단언
// src/lib.rs
pub fn add_two(a: i32) -> i32 {
a + 2
}
#[cfg(test]
mod tests {
use super::*;
#[test]
fn it_adds_two() {
assert_eq!(4, add_two(2));
}
}
cargo test
<생략>
# 틀린 경우
left: `4`,
right: `5`, src/lib.rs:11:9
커스텀 실패 메시지 추가하기
assert!, assert_eq!, asser_ne! 매크로에는 추가 인수로 실패 메시지에 출력될 내용을 추가할 수 있음
필수적인 인수들 이후의 인수는 format! 매크로로 전달되기 때문에 {} 자리 표시자가 들어있는 포맷 문자열과 자리 표시자에 들어갈 값을 전달할 수 있음
// src/lib.rs
pub fn greeting(name: &str) -> String {
format!("Hello {}!", name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greeting_contains_name() {
let result = greeting("Carol");
assert!(
result.contains("Carol"),
"Greeting did not contain name, value was `{}`",
result,
);
}
}
pub fn greeting(name: &str) -> String {
String::from("Hello!");
}
should_panic 매크로로 패닉 발생 검사하기
예상한대로 에러 조건을 잘 처리하는지 검사. 내부에서 패닉이 발생하면 통과, 패닉이 발생하지 않으면 실패
// src/lib.rs
pub struct Guess {
value: i32,
}
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("must be between 1 and 100, got: {}", value);
}
Guess { value }
}
}
#[cfg(test)]
mod tests {
use super::8;
#[test]
#[should_panic]
fn greeter_than_100() {
Guess::new(200);
}
}
cargo test
< 생략 >
# panic!이 발생하지 않았을 때
running 1 test
test testes::greater_than_100 - should panic ... FAILED
failures:
---- tests::greater_than_100 stdout ----
note: test did not panic as expected
failures:
tests::greater_than_100
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out;
Result<T,E>를 이용한 테스트
#[cfg(test)]
mod tests {
#[test]
fn it_works() -> Result<(), String> {
if 2 + 2 == 4 {
Ok()
} else {
Err(String::from("failed"))
}
}
}
Result<T, E>를 반환하는 테스트에서는 ? 연산자를 사용할 수 있기 때문에 내부 작업이 Err를 반환할 경우 실패해야하는 테스트를 작성하기 편리
Result<T, E> 테스트에서는 #[should_panic] 애너테이션을 사용할 수 없음
연산이 Err 배리언트를 반환하는 것을 단언하기 위해서는 Result<T, E> 값에 ? 연산자를 사용하는 대신
`assert!(value.is_err())`를 사용하는 것을 권장
테스트 실행 방법 제어하기
cargo test 명령은 코드를 테스트 모드에서 컴파일하고 생성된 바이너리를 실행
cargo test에 의해 생성된 바이너리의 기본 동작은 모든 테스트를 병렬로 실행하고 테스트가 수행되는 동안 발생된 출력을 캡처하여 출력이 표시되는 것을 막고 테스트 결과와 관련된 출력을 읽기 편하게 해줌
그러나 커맨드 라인 옵션을 지정하여 이러한 기본 동작을 변경할 수 있음
명령 옵션은 cargo test에 전달되는 것도 있고 테스트 바이너리에 전달되는 것도 있음
cargo test에 전달할 인수를 먼저 나열하고 -- 구분자 뒤에 테스트 바이너리에 전달할 인수를 나열할 수 있음
- 테스트를 순차적으로 실행
# 테스트 파이너라에서 사용할 스레드 개수 지정
# 1 = 프로그램이 어떠한 병렬 처리도 사용하지 않음
cargo test -- --test-threads=1
- 함수 출력 표시
# 성공한 테스트에서 출력한 내용도 표시
cargo test -- --show-output
- 이름을 지정하여 하나의 테스트만 실행하기
cargo test 테스트_함수명
# 이 경우 테스트_함수명이 겹칠경우 ex) add_one, add_one_plus
# 하나의 테스트만 실행하려면 정규식으로 걸러야함
# ex) cargo test '^add_one$'
테스트 함수 이름과 일치하지 않는 항목은 결과 출력에 filtered out에 몇 개의 테스트가 필터링 되었는지 표시됨
- 테스트를 필터링하여 여러 테스트 실행하기
cargo test 테스트_함수명_일부_문자
# ex) 테스트 함수가 add_one, add_two, minus_one 이라면
# cargo test add 명령 실행 시 add_one과 add_two가 실행됨
- 특별한 요청이 없다면 일부 테스트 무시하기
// src/lib.rs
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {}
# #[ignore] 애너테이션을 가진 테스트를 무시
cargo test -- --ignored
# #[ignore] 애너테이션을 가진 테스트까지 모두 실행
cargo test -- --include-ignored
유닛 테스트 (unit test / 단위 테스트)
한 번에 하나의 모듈만 테스트. 모듈의 비공개 인터페이스도 테스트 가능
각 코드 단위를 나머지 코드와 분리하여 제대로 작동하지 않는 코드가 어느 부분인지 빠르게 파악하는 것이 목적
유닛 테스트는 src 디렉터리 내의 각 파일에 테스트 대상이 될 코드를 함께 작성
각 파일에 tests 모듈을 만들고 #[cfg(test)]를 애너테이션하는 것이 일반적인 관례
유닛 테스트는 일반 코드와 같은 파일에 위치하기 때문에 애너테이션을 통해 컴파일 결과물에 포함되지 않도록 명시해야함
통합 테스트 (integration test)
완전한 라이브러리 외부에 위치하여 작성한 코드를 외부 코드에서 사용할 떄와 똑같은 방식을 사용
하나의 테스트에서 잠재적으로 여러 모듈이 사용될 수 있음
통합 테스트의 경우 별도의 디렉터리에 위치하기 때문에 #[cfg(test)] 애너테이션이 불필요
외부 코드와 마찬가지로 개발한 라이브러리의 공개 API만 호출 가능
통합 테스트를 위해서는 tests 디렉터리를 만들어야함
카고는 디렉터리 내 통합 테스트 파일을 자동으로 인식
tests 디렉터리를 만들어서 원하는 만큼 통합 테스트 파일을 만들면 카고가 각 파일을 개별 크레이트로 컴파일
adder
|__ Cargo.lock
|__ Cargo.toml
|__ src
|__ lib.rs
|__ tests
|__ integration_test.rs
// tests/integration_test.rs
use adder;
#[test]
fn it_add_two() {
assert_eq!(4, adder::add_two(2));
}
cargo test
<생략>
Running tests/integration_test.rs (target/debug/deps/integration_test-생략)
# 특정 통합 테스트 파일의 모든 테스트 실행
cargo test -- test iontegratio_test
통합 테스트 내 서브모듈
통합 테스트를 추가하다 보면 조직화를 위해 tests 디렉터리에 더 많은 파일이 필요할 수 있음
tests 내 각 파일은 각각의 크레이트로 컴파일 되는데,
이는 각 통합 테스트 파일이 각각의 크레이트로 취급되기 때문에 코드와 분리된 스코프를 가지기 때문임
이로인해 src 디렉터리에서 코드를 모듈과 파일로 분리하여 동일한 동작을 공유하는 것을 tests 디렉터리내 파일에서는 불가
공통 모듈은 tests 디렉터리 아래에 common 디렉터리를 두고 여기에 모듈을 작성하면 서브모듈을 둘 수 있음
|__ Cargo.lock
|__ Cargo.toml
|__ src
|__ lib.rs
|__ tests
|__ common
|__ mod.rs
|__ integration_test.rs
// tests/intergration_test.rs
use adder;
mod common;
#[test]
fn it_adds_two() {
common::setup();
assert_eq!(4, adder::Add_two(2));
}
바이너리 크레이트에서의 통합테스트
src/lib.rs 파일이 없고 src/main.rs 파일만 있는 바이너리 크레이트의 경우 통합 테스트에서 use 구문으로 함수를 가져올 수 없음
다른 크레이트에서 사용할 수 있도록 함수를 노출하는건 라이브러리 크레이트 뿐이기 때문
바이너리 크레이트는 자체적으로 실행되게 되어있음
바이너리를 제공하는 러스트 프로젝트들이 src/main.rs 파일은 간단하게 작성하고
로직은 src/lib.rs 파일에 위치키는 이유 중 하나가 이 때문임
'Programming > Rust' 카테고리의 다른 글
| 러스트 반복자와 클로저 (0) | 2025.10.08 |
|---|---|
| 러스트 커맨드라인 프로그램 만들기 (0) | 2025.09.12 |
| 러스트 - 제네릭 타입, 트레이트, 라이프타임 (1) | 2025.09.08 |
| 러스트 에러 처리 (0) | 2025.09.08 |
| 러스트 컬렉션 (collection) (1) | 2025.09.02 |
