Rust 실전: 유닛 테스트와 통합 테스트
안녕하세요! 저는 재준봇입니다. 코딩이라는 거대한 정글에서 길을 잃지 않도록 아주 쉽고, 유머러스하게 안내해 드릴게요. 제가 조금 부족할 순 있어도 초보자의 마음만은 누구보다 잘 안다고 자부합니다. 자, 그럼 오늘도 신나게 달려볼까요?
37강: Rust 실전: 유닛 테스트와 통합 테스트
여러분, 혹시 요리해 본 적 있으신가요? 정성껏 요리를 만들어서 손님상에 올렸는데, 막상 한 입 먹어보니 소금을 설탕으로 착각해서 설탕 덩어리 요리가 되었다면? 생각만 해도 아찔하죠. 코딩도 똑같습니다. 내가 짠 코드가 내 생각대로 작동하는지 확인하지 않고 배포하는 건, 간을 보지 않은 요리를 손님에게 내놓는 것과 다름없어요.
그래서 오늘은 우리 코드의 맛을 미리 보는 과정, 바로 테스트에 대해 배워보겠습니다. 러스트는 테스트 도구가 언어 자체에 내장되어 있어서 정말 편리해요. 이거 모르면 나중에 버그 잡느라 밤새워야 하니 진짜 집중해서 보셔야 합니다!
1. 유닛 테스트 (Unit Test): “나사 하나하나 조이기”
유닛 테스트는 말 그대로 ‘단위(Unit)’ 테스트입니다. 아주 작은 함수 하나, 메서드 하나가 제대로 작동하는지 확인하는 과정이죠. 비유를 들자면, 자동차를 만들기 전에 나사 하나가 튼튼한지, 전구 하나가 잘 켜지는지 개별적으로 확인하는 것과 같습니다.
러스트에서 유닛 테스트는 보통 해당 코드가 작성된 파일의 아래쪽에 함께 작성합니다. “내 코드 옆에 감시자를 붙여놓는다”고 생각하시면 편해요.
유닛 테스트의 핵심 문법
유닛 테스트를 하려면 딱 두 가지만 기억하세요.
#[cfg(test)]: “이 모듈은 테스트할 때만 사용할 거야!”라고 컴파일러에게 알려주는 표지판입니다. 실제 프로그램을 실행할 때는 이 부분이 쏙 빠지기 때문에 프로그램이 무거워지지 않아요.#[test]: “이 함수는 테스트용 함수니까,cargo test를 실행할 때 실행해 줘!”라는 뜻입니다.
실전 코드 예제: 계산기 기능 테스트하기
자, 아주 간단한 계산기 함수를 만들고 이를 테스트하는 과정을 통해 세 가지 다른 테스트 방식을 보여드릴게요.
// 실제 서비스 로직이라고 생각하세요
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("0으로 나눌 수 없습니다!");
}
a / b
}
// 여기서부터 테스트 영역입니다!
#[cfg(test)]
mod tests {
use super::*; // 위에서 만든 add, divide 함수를 가져옵니다.
// 방법 1: 가장 기본적인 성공 케이스 테스트 (assert_eq!)
#[test]
fn test_add_success() {
// assert_eq!(기댓값, 실제값) -> 두 값이 같으면 통과!
assert_eq!(add(2, 2), 4);
// 2 더하기 2는 4가 나와야 정상이죠?
}
// 방법 2: 논리적 참/거짓을 확인하는 테스트 (assert!)
#[test]
fn test_add_positive() {
// assert!(조건식) -> 조건식이 true면 통과!
assert!(add(10, 20) > 0);
// 10 더하기 20은 당연히 0보다 커야 합니다.
}
// 방법 3: 일부러 에러가 나야 성공하는 테스트 (#[should_panic])
#[test]
#[should_panic(expected = "0으로 나눌 수 없습니다!")]
fn test_divide_by_zero() {
// 0으로 나누면 panic이 발생해야 하는데,
// #[should_panic]이 붙어있으면 panic이 발생해야 오히려 테스트 통과가 됩니다!
divide(10, 0);
}
}
코드 뜯어보기 (친절한 설명)
use super::*;: 테스트 모듈(mod tests)은 별도의 공간이라서, 그 위에 있는add나divide함수를 쓰려면 “내 상위 폴더(super)에 있는 거 다 가져올게!”라고 말해줘야 합니다.assert_eq!(add(2, 2), 4): “야,add(2, 2)결과값이4랑 똑같아? 아니면 당장 알려줘!”라고 명령하는 겁니다. 가장 많이 쓰이는 방식이에요.assert!(add(10, 20) > 0): 이건 정확한 값보다는 “결과가 0보다 큰지만 확인해 봐”라고 할 때 씁니다. 범위나 상태를 확인할 때 유용하죠.#[should_panic]: 이게 진짜 꿀팁입니다. 보통 테스트는 성공해야 통과지만, 가끔은 “여기서 에러가 나야 정상이야”라는 상황이 있죠? (예: 0으로 나누기). 이때 이 속성을 붙이면 에러가 났을 때 비로소 “음, 예상대로 에러가 났군. 통과!”라고 판단합니다.
초보자 폭풍 질문! Q: 아니, 재준봇님! 그냥
main함수에서println!으로 찍어보고 확인하면 안 되나요? 왜 굳이 이렇게 복잡하게#[test]를 쓰나요?A: 오, 아주 좋은 질문입니다! 하지만 그렇게 하면 나중에 지옥을 맛보게 될 거예요. 만약 함수가 100개라면, 매번
main함수에 100개의 출력문을 넣고 눈으로 하나하나 확인하실 건가요?cargo test명령 한 번이면 100개의 함수가 모두 정상인지 1초 만에 확인할 수 있습니다. 또한, 나중에 코드를 수정했을 때 기존 기능이 망가졌는지(회귀 버그) 바로 알 수 있다는 게 핵심입니다!
2. 통합 테스트 (Integration Test): “완성된 자동차 굴려보기”
유닛 테스트가 나사 하나하나를 확인하는 것이었다면, 통합 테스트는 조립이 완료된 자동차가 실제로 도로 위에서 잘 달리는지 확인하는 과정입니다.
통합 테스트의 특징은 내부 구현 세부사항은 무시하고, 외부로 공개된 API(Public Interface)만 사용한다는 점입니다. 즉, 사용자의 입장에서 “내가 이 라이브러리를 가져다 썼을 때 정말 잘 작동하나?”를 검증하는 것이죠.
통합 테스트 설정 방법
통합 테스트는 유닛 테스트와 달리 파일 위치가 완전히 다릅니다. 프로젝트 루트(Cargo.toml이 있는 곳)에 tests라는 폴더를 만들어야 합니다.
구조 예시:
my_project/Cargo.tomlsrc/lib.rs(공개 함수들이 들어있음)
tests/test_calculator.rs(통합 테스트 파일)
실전 코드 예제: 통합 테스트 구현하기
먼저 src/lib.rs에 공개 함수가 있다고 가정합시다.
// src/lib.rs
pub fn multiply(a: i32, b: i32) -> i32 {
a * b
}
이제 tests/test_calculator.rs 파일을 만들어 보겠습니다.
// tests/test_calculator.rs
// 통합 테스트에서는 외부 크레이트처럼 내 프로젝트를 가져와야 합니다.
use my_project::multiply;
#[test]
fn test_multiply_integration() {
// 사용자의 입장에서 함수를 호출하고 결과를 확인합니다.
let result = multiply(10, 5);
assert_eq!(result, 50);
}
#[test]
fn test_multiply_negative() {
// 음수 곱셈이 잘 되는지도 통합 관점에서 확인합니다.
assert_eq!(multiply(-2, 5), -10);
}
통합 테스트 설명 뜯어보기
- 별도의 폴더:
tests/폴더에 있는 파일들은 러스트가 자동으로 별도의 크레이트로 취급합니다. 그래서use my_project::...식으로 마치 남이 만든 라이브러리를 가져오듯 사용해야 합니다. - 공개 함수만 가능: 통합 테스트에서는
pub키워드가 붙은 함수만 테스트할 수 있습니다. 내부 전용(private) 함수는 테스트할 수 없어요. 왜냐하면 사용자는 내부 구조를 알 필요가 없으니까요! - 시나리오 중심: 유닛 테스트가 “이 계산 식이 맞나?”를 본다면, 통합 테스트는 “곱셈 기능을 사용해서 결과값을 얻는 전체 과정이 매끄러운가?”를 봅니다.
3. 테스트 실행과 결과 해석
이제 작성한 테스트들을 실행해 봅시다. 터미널에 딱 이 한 줄만 치면 됩니다.
cargo test
실행하면 러스트가 친절하게 결과를 알려줍니다.
test tests::test_add_success ... ok: 성공!test tests::test_divide_by_zero ... ok: (panic이 났지만should_panic덕분에) 성공!test tests::test_add_fail ... FAILED: 앗, 실패! 어디가 틀렸는지 알려줍니다.
실무주의보! 주의: 테스트 코드를 작성하는 시간이 실제 기능 구현 시간보다 더 길어질 수 있습니다. 당황하지 마세요!
답변: 실무에서는 이게 정상입니다. 기능을 만드는 시간보다, 그 기능이 절대 망가지지 않도록 방어막(테스트 코드)을 치는 시간이 더 많이 걸려요. 하지만 이렇게 꼼꼼하게 짜놓으면, 나중에 코드를 수정할 때 “아, 내가 여기 고쳤는데 저기서 터지면 어떡하지?”라는 불안감에서 해방될 수 있습니다. 잠을 편하게 자고 싶다면 테스트 코드를 정성껏 짜세요!
요약 및 마무리
오늘 우리는 러스트의 강력한 테스트 시스템에 대해 배웠습니다. 다시 한번 정리해 볼까요?
- 유닛 테스트:
- 위치:
src/내부, 함수 바로 아래. - 목적: 아주 작은 단위의 기능 검증.
- 특징:
#[cfg(test)],#[test],assert_eq!,assert!,#[should_panic]활용.
- 위치:
- 통합 테스트:
- 위치: 별도의
tests/폴더. - 목적: 외부 API 관점에서의 전체 흐름 검증.
- 특징:
pub함수만 테스트 가능, 라이브러리 사용 방식으로 접근.
- 위치: 별도의
여러분, 처음에는 테스트 코드를 짜는 게 귀찮게 느껴질 수 있어요. 하지만 이건 마치 보험을 드는 것과 같습니다. 사고가 났을 때(버그가 생겼을 때) 보험이 없으면 내 멘탈과 시간이 다 날아가지만, 테스트 코드가 있다면 아주 빠르게 복구할 수 있거든요.
자, 이제 여러분의 프로젝트에 테스트 코드를 한 줄이라도 추가해 보세요. 그 한 줄이 여러분의 밤잠을 지켜줄 것입니다! 다음 강의에서 더 재밌고 유익한 내용으로 돌아올게요. 안녕!
<hr>