Rust 테스트
Rust는 테스트 기능이 언어와 cargo에 처음부터 들어 있어요. 그래서 테스트를 시작하려고 따로 라이브러리를 찾을 필요가 없어요. 이 페이지에서는 첫 테스트를 작성하고 실행하는 데 필요한 만큼만 살펴볼게요.
기본 테스트 작성하기
섹션 제목: “기본 테스트 작성하기”테스트는 #[test]를 붙인 평범한 함수예요.
fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn it_adds_two_numbers() { assert_eq!(add(2, 3), 5); }}눈여겨볼 점이 몇 가지 있어요.
#[cfg(test)]는 “이 모듈은 테스트를 실행할 때만 컴파일해”라는 뜻이에요. 평소 프로그램에는 포함되지 않아요.use super::*;는 부모 모듈에 있는 것(여기서는add함수)을 전부 가져와요. 그래서 테스트에서add를 호출할 수 있어요.#[test]는 이 함수가cargo test로 실행할 테스트라는 표시예요.
테스트 실행하기
섹션 제목: “테스트 실행하기”프로젝트 폴더에서 이렇게 실행해요.
cargo test그러면 이런 출력이 나와요.
running 1 testtest tests::it_adds_two_numbers ... ok
test result: ok. 1 passed; 0 failed; 0 ignoredassert!와 친구들
섹션 제목: “assert!와 친구들”평소에 필요한 확인은 대부분 이 세 가지 매크로로 해결돼요.
#[test]fn checks() { assert!(2 + 2 == 4); // 조건이 false면 테스트가 실패해요 assert_eq!(2 + 2, 4); // 두 값이 같지 않으면 실패해요 assert_ne!(2 + 2, 5); // 두 값이 같으면 실패해요}보통은 assert!(a == b)보다 assert_eq!와 assert_ne!를 더 많이 써요. 실패하면 두 값을 모두 출력해 주기 때문에, 그냥 “assertion failed”라고만 나오는 대신 무엇이 어긋났는지 바로 알 수 있거든요.
테스트가 실패하면 어떻게 보일까
섹션 제목: “테스트가 실패하면 어떻게 보일까”fn add(a: i32, b: i32) -> i32 { a + b}
#[cfg(test)]mod tests { use super::*;
#[test] fn this_will_fail() { assert_eq!(add(2, 2), 5); }}test tests::this_will_fail ... FAILED
assertion `left == right` failed left: 4 right: 5테스트가 실패했을 때 바로 이런 메시지가 필요해요. left는 코드가 실제로 만든 값이고, right는 테스트가 기대한 값이에요. 두 값이 나란히 보이니까 어디가 잘못됐는지 한눈에 알 수 있어요.
함수가 패닉하는지 테스트하기
섹션 제목: “함수가 패닉하는지 테스트하기”때로는 패닉(panic)하는 것이 올바른 동작일 때도 있어요. 예를 들어 잘못된 입력을 거부해야 하는 함수가 그래요. 이럴 때는 #[should_panic]으로 테스트해요.
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("0으로 나눌 수 없어요"); } a / b}
#[cfg(test)]mod tests { use super::*;
#[test] #[should_panic] fn dividing_by_zero_panics() { divide(10, 0); }}실패 메시지를 더 알기 쉽게
섹션 제목: “실패 메시지를 더 알기 쉽게”테스트가 실패하면 왜 실패했는지 최대한 빨리 알고 싶어요. 작은 것 두 가지만 더해도 큰 도움이 돼요.
직접 쓴 메시지. assert!, assert_eq!, assert_ne!는 모두 값 뒤에 메시지를 하나 더 받을 수 있어요. 쓰는 방법은 println!과 똑같아요. 이 메시지는 확인에 실패했을 때만 출력돼요.
should_panic의 expected. 그냥 #[should_panic]만 쓰면 어떤 패닉이든 통과해요. 전혀 다른 버그 때문에 생긴 패닉이라도요. expected = "..."를 붙이면 패닉 메시지에 그 문자열이 들어 있을 때만 테스트가 통과해요.
아까 본 divide 함수를 다시 가져왔어요. 실행해 볼 수 있게 main을 붙이고, 두 가지 방법을 모두 쓰는 테스트 두 개를 넣었어요.
fn divide(a: i32, b: i32) -> i32 { if b == 0 { panic!("0으로 나눌 수 없어요"); } a / b}
fn main() { println!("10 / 2 = {}", divide(10, 2)); println!("7 / 2 = {}", divide(7, 2));}
#[cfg(test)]mod tests { use super::*;
#[test] fn rounds_down() { let result = divide(7, 2); assert_eq!(result, 3, "7 / 2는 소수점 아래를 버려야 해요. 실제 결과: {result}"); }
#[test] #[should_panic(expected = "0으로 나눌")] fn dividing_by_zero_panics() { divide(10, 0); }}실행 결과:
10 / 2 = 57 / 2 = 3위의 실행 결과는 cargo run을 했을 때 나오는 거예요. main은 테스트를 실행하지 않거든요. 테스트를 실행하려면 cargo test를 써요. Rust Playground에서는 Run 옆의 메뉴를 열고 Test를 고르면 돼요. 그러면 이렇게 나와요.
running 2 teststest tests::rounds_down ... oktest tests::dividing_by_zero_panics - should panic ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out테스트는 병렬로 실행되기 때문에 두 test ... 줄은 어느 쪽이 먼저 나올지 몰라요. 이번에는 테스트가 3 대신 4를 기대했다고 해 볼게요. 실패하면 두 값 바로 옆에 여러분이 쓴 메시지가 함께 나와요.
assertion `left == right` failed: 7 / 2는 소수점 아래를 버려야 해요. 실제 결과: 3 left: 3 right: 4그리고 divide가 다른 메시지로 패닉했다면 expected 확인이 그걸 잡아내요(여기서는 expected = "음수"였다고 가정할게요).
note: panic did not contain expected string panic message: "0으로 나눌 수 없어요" expected substring: "음수"이 영어 메시지는 Rust가 직접 출력하는 거예요. “패닉 메시지에 기대한 문자열이 들어 있지 않았다”는 뜻이에요.
테스트는 왜 쓸까
섹션 제목: “테스트는 왜 쓸까”솔직히 말하면, 짧은 스크립트라면 아마 테스트를 안 쓸 거예요. 테스트는 코드를 나중에 다시 쓰거나 고칠 때부터 진가를 발휘해요. 뭔가를 망가뜨리면 사용자가 알려 주기 전에 cargo test가 바로 알려 주니까, 마음 놓고 리팩터링할 수 있어요.
테스트는 문서 안에 둘 수도 있어요. /// 문서 주석 안의 코드 예제도 cargo test로 실행되기 때문에, 예제가 낡아서 틀리는 일이 생기지 않아요. 방법은 문서화 페이지에서 볼 수 있어요.
연습 문제
섹션 제목: “연습 문제”두 문제 모두 정답에 실행할 수 있는 main과, cargo test(또는 Playground의 Test)로 실행하는 테스트가 들어 있어요.
1. 성적 함수 테스트하기
섹션 제목: “1. 성적 함수 테스트하기”90점 이상이면 'A', 8089점이면 79점이면 'B', 70'C', 그보다 낮으면 'F'를 돌려주는 fn grade(score: u32) -> char를 작성해 보세요. 그다음 90과 89처럼 경계값을 확인하는 테스트를 작성해요. 힌트: 버그는 경계에 숨어 있어요. 딱 90을 확인하는 테스트가 있으면 >= 대신 >를 쓰는 흔한 실수를 잡을 수 있어요.
정답 보기
fn grade(score: u32) -> char { if score >= 90 { 'A' } else if score >= 80 { 'B' } else if score >= 70 { 'C' } else { 'F' }}
fn main() { for score in [95, 85, 72, 40] { println!("{score} -> {}", grade(score)); }}
#[cfg(test)]mod tests { use super::*;
#[test] fn edges_of_a() { assert_eq!(grade(90), 'A'); assert_eq!(grade(89), 'B'); }
#[test] fn edges_of_c() { assert_eq!(grade(70), 'C'); assert_eq!(grade(69), 'F'); }
#[test] fn zero_is_f() { assert_eq!(grade(0), 'F', "0점은 F가 나와야 해요"); }}실행 결과:
95 -> A85 -> B72 -> C40 -> Fcargo test를 실행하면 3 passed가 나와야 해요. score >= 90을 score > 90으로 바꾸고 테스트를 다시 실행해 보세요. edges_of_a가 실패하면서 left: 'B'와 right: 'A'를 보여 줘요.
2. Option을 돌려주는 함수 테스트하기
섹션 제목: “2. Option을 돌려주는 함수 테스트하기”b가 0이면 패닉하는 대신 None을 돌려주는 fn safe_divide(a: i32, b: i32) -> Option<i32>를 작성해 보세요. 평범한 나눗셈 테스트 하나와 0으로 나누는 테스트 하나를 작성해요. 힌트: assert_eq!는 Option에도 쓸 수 있어요. 그래서 assert_eq!(safe_divide(1, 0), None)처럼 쓸 수 있어요.
정답 보기
fn safe_divide(a: i32, b: i32) -> Option<i32> { if b == 0 { None } else { Some(a / b) }}
fn main() { println!("{:?}", safe_divide(10, 2)); println!("{:?}", safe_divide(1, 0));}
#[cfg(test)]mod tests { use super::*;
#[test] fn divides_normally() { assert_eq!(safe_divide(10, 2), Some(5)); }
#[test] fn zero_gives_none() { assert_eq!(safe_divide(1, 0), None); }}실행 결과:
Some(5)None위의 divide 함수와 비교해 보세요. 거기서는 0으로 나누면 패닉하기 때문에 테스트에 #[should_panic]이 필요했어요. 여기서는 그냥 평범한 반환값이라서 assert_eq! 하나면 충분해요. Option이나 Result를 돌려주는 함수가 대개 테스트하기 더 쉬워요.
- 함수에
#[test]를 붙이면 테스트가 되고,cargo test로 모든 테스트를 실행해요. - 일상적인 확인은 대부분
assert!,assert_eq!,assert_ne!로 충분해요. 실패 메시지가 더 친절한assert_eq!/assert_ne!를 우선 쓰세요. #[should_panic]은 함수가 패닉해야 할 때 정말 패닉하는지 테스트해요.expected = "..."를 붙이면 올바른 이유로 패닉했는지까지 확인해요.- 어떤 assert 매크로든 직접 쓴 메시지를 붙여서, 실패했을 때 무엇이 잘못됐는지 설명할 수 있어요.
- 테스트는 나중에 고치거나 다시 쓸 코드에서 가장 큰 가치를 발휘해요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. cargo test가 실행할 테스트라고 함수에 표시하는 속성은 무엇일까요?
2. 보통 assert!(a == b)보다 assert_eq!와 assert_ne!를 더 많이 쓰는 이유는 무엇일까요?
3. #[should_panic]은 무슨 일을 할까요?
점수: 0 / 3
더 알아보기
섹션 제목: “더 알아보기”- 테스트 작성 방법 — The Rust Programming Language 한국어판:
#[test], assert 매크로, 직접 쓴 메시지,should_panic, 그리고Result를 돌려주는 테스트까지 다뤄요. - 테스트 실행 방법 제어하기 — The Rust Programming Language 한국어판: 이름으로 테스트 하나만 실행하기,
println!출력 보기,#[ignore]를 설명해요. - 테스트 조직화 — The Rust Programming Language 한국어판: 단위 테스트와
tests/폴더에 두는 통합 테스트의 차이를 설명해요. - Unit testing — Rust by Example (영어): 통과하는 테스트, 실패하는 테스트, 패닉하는 테스트를 짧은 예제로 보여 줘요.
cargo test— The Cargo Book (영어):cargo test명령의 모든 옵션을 정리해 뒀어요.