All posts

Rust 초급 11편: 테스트와 디버깅으로 목표 계산 검증하기

프로그램이 컴파일된다는 사실만으로 계산이 맞는 것은 아닙니다. 60분 이상이면 목표 달성이라고 정했는데 >를 써서 정확히 60분을 빠뜨릴 수도 있습니다. 이번에는 함수의 약속을 테스트로 적고, 일부러 만든 경계값 오류를 찾아봅니다.

확대경과 점검표로 장치를 살피는 게로 테스트와 디버깅을 표현한 일러스트

프로그램이 컴파일된다는 사실만으로 계산이 맞는 것은 아닙니다. 60분 이상이면 목표 달성이라고 정했는데 >를 써서 정확히 60분을 빠뜨릴 수도 있습니다. 이번에는 함수의 약속을 테스트로 적고, 일부러 만든 경계값 오류를 찾아봅니다.

준비와 목표

이번에는 라이브러리 프로젝트를 만듭니다. 함수와 테스트를 src/lib.rs에 작성하고 cargo test로 실행합니다. 실행 파일이 없으므로 cargo run은 사용하지 않습니다.

cargo new study_tests --lib
cd study_tests

목표는 정상값과 경계값을 구분하고, 실패한 테스트의 의미를 읽는 것입니다.

핵심 실습: 세 가지 입력 검사하기

생성된 src/lib.rs의 내용을 모두 아래로 바꿉니다.

pub fn reached_goal(minutes: u32, goal: u32) -> bool {
    minutes >= goal
}

#[cfg(test)]
mod tests {
    use super::reached_goal;

    #[test]
    fn below_goal_is_false() {
        assert!(!reached_goal(59, 60));
    }

    #[test]
    fn exact_goal_is_true() {
        assert!(reached_goal(60, 60));
    }

    #[test]
    fn above_goal_is_true() {
        assert_eq!(reached_goal(61, 60), true);
    }
}
cargo test

출력에서 다음과 같은 결과의 요점을 확인합니다.

test result: ok. 3 passed; 0 failed

실제로는 테스트 이름과 문서 테스트 결과도 함께 표시됩니다. 병렬 실행 때문에 테스트 이름의 표시 순서는 일정하지 않을 수 있습니다.

속성과 아사션 읽기

#[test]는 해당 함수를 테스트로 등록합니다. #[cfg(test)]가 붙은 모듈은 테스트용 빌드에서 사용됩니다. use super::reached_goal은 부모 모듈의 함수를 가져오는 지정입니다.

assert!는 조건이 true인지, assert_eq!는 두 값이 같은지 확인합니다. 조건에 맞지 않으면 그 테스트가 실패합니다. bool에는 assert!가 읽기 편한 경우가 많지만 이번에는 차이를 보여 주기 위해 둘 다 사용했습니다. 테스트 공식 설명

일부러 경계값을 잘못 처리하기

reached_goal의 계산을 minutes > goal로 바꾸고 저장한 뒤 cargo test를 실행합니다. 59분과 61분 테스트는 통과하지만 정확히 60분인 테스트는 실패합니다.

요구사항은 "이상"인데 구현은 "초과"로 작성했기 때문입니다. 실패한 테스트는 고칠 지점을 알려 주는 단서입니다. 기대값을 false로 바꿔 테스트를 통과시키기 전에 요구사항과 구현 중 어느 쪽을 고쳐야 하는지 판단하세요.

이번에는 요구사항을 유지하므로 >=로 되돌립니다. cargo test를 다시 실행하고 세 테스트가 통과하는지 확인합니다.

테스트 하나에 집중하기

cargo test exact_goal_is_true

이름으로 실행 대상을 좁혀 원인을 조사할 수 있습니다. 수정이 끝나면 cargo test로 전체도 확인합니다. 테스트 하나가 통과했다고 다른 부분에 영향이 없다고 할 수는 없습니다.

중간 값을 조사하려면 함수 안에 dbg!(minutes, goal);을 잠시 추가할 수 있습니다. dbg!는 표준 에러에 정보를 출력합니다. 테스트 출력은 보통 캡처되므로 화면에서 보려면 다음 명령을 사용합니다.

cargo test -- --nocapture

첫 -- 뒤는 테스트 실행 측에 전달하는 인자입니다. 조사가 끝나면 dbg!를 지우고 필요한 확인을 assert로 남기면 다음 변경에서도 검증에 사용할 수 있습니다.

코드 정리와 정적 검사

일반적인 rustup 환경에서 구성 요소가 없다면 다음 명령으로 코드 정리와 Lint 도구를 추가할 수 있습니다.

rustup component add rustfmt clippy
cargo fmt
cargo clippy

cargo fmt는 서식을 정리하고 clippy는 개선할 부분이나 오류로 이어질 수 있는 표현을 지적합니다. 앞의 bool 비교에는 assert!가 낫다는 제안이 나올 수 있습니다. 제안의 의미를 읽고 적용 여부를 판단하세요. 코드 서식과 정적 검사는 실행 결과를 확인하는 테스트와 역할이 다릅니다.

컴파일 오류와 테스트 실패 구분하기

컴파일 오류가 있다면 테스트는 아직 실행되지 않았습니다. 먼저 타입이나 참조 문제를 해결해야 합니다. 테스트 실패라면 함수는 실행됐지만 실제 결과가 기대와 달랐다는 뜻입니다. error라는 단어만 보지 말고 어느 단계에서 멈췄는지 확인하세요.

연습과 해설

목표가 0분이면 실제 학습이 0분이어도 달성으로 처리하는 규칙을 추가로 확인합니다. reached_goal(0, 0)이 true가 되는 테스트를 작성해 보세요.

해설: tests 안에 #[test] fn zerogoalismet() { assert!(reachedgoal(0, 0)); }를 추가합니다. 이 동작이 서비스 요구사항과 다르다면 함수의 입력 검증 규칙부터 정해야 합니다. 테스트가 요구사항을 대신 결정해 주지는 않습니다.

다음 12편에서는 이 검사 습관을 이용해 파일을 검색하는 CLI 프로그램을 완성합니다.

실행 확인

이 글의 핵심 실습은 Rust 1.99.1, 2024 Edition의 Linux 환경에서 확인했습니다. 의도적으로 실패하는 예제는 설명한 오류가 발생하는지 구분해 검사했습니다. 운영체제별 오류 문구는 다를 수 있습니다.

Rust 초급 시리즈 전체 목차

  1. 설치와 Cargo로 첫 프로그램 만들기
  2. 변수·자료형·문자열로 학습 기록 만들기
  3. 함수·조건문·반복문으로 학습 기록 계산하기
  4. String으로 이해하는 소유권과 이동
  5. 참조와 빌림으로 문자열 읽고 수정하기
  6. 구조체와 메서드로 학습 할 일 만들기
  7. 열거형과 패턴 매칭으로 학습 상태 표현하기
  8. Option·Result로 빈 값과 오류 처리하기
  9. 벡터·해시맵·문자열로 학습 기록 모으기
  10. 모듈과 외부 크레이트로 코드 정리하기
  11. 테스트와 디버깅으로 목표 계산 검증하기
  12. 파일을 검색하는 CLI 프로젝트 완성하기

시리즈 모아보기

Discussion

No comments yet