본문 바로가기

개발

[TIL] Rust에서 자동화된 테스트 작성하기

반응형

Rust에서 테스트코드 작성하는 법

  • lib 프로젝트 생성한다.
cargo new adder --lib
  • lib 프로젝트를 만들면 아래와 같이 src/lib.rs에 아래와 같은 테스트 코드가 작성되어 있다.
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}
  • test 돌리기
cargo test
  • 테스트 하고 싶은 함수는 함수 위에 #[test]라고 붙여주면 된다. 일단 mod포함 상단 부분은 신경쓰지 말자.
#[cfg(test)]
mod tests {
    #[test]
    fn exploration() {
        assert_eq!(2 + 2, 4);
    }
}

assert! Macro로 결과 확인하기

  • assert!매크로는 값이 true인지 확인한다. false라면 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
    }
}

#[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! Macros로 결과 비교하기

  • assert_eq!는 두 개의 값이 같은지 테스트, assert_ne!는 같지 않은지 테스트
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));
    }
}

커스터마이즈된 실패 메시지 출력하기

  • assert!, assert_eq!, assert_ne!의 필수 인자 뒤에 커스텀 메시지를 추가 가능하다. 내부적으로 format! 매크로를 호출하기 때문에 {}placeholdrs와 placeholders에 들어갈 값들을 써줄 수 있다.
pub fn greeting(name: &str) -> String {
    String::from("Hello!")
}

#[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
        );
    }
}

should_panic으로 패닉 테스트하기

  • #[test]의 아래에 #[should_panic]을 써주면 panic여부를 테스트 가능하다
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {}.", value);
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic]
    fn greater_than_100() {
        Guess::new(200);
    }
}
  • panic 메시지 역시 테스트 가능하다.
pub struct Guess {
    value: i32,
}

// --snip--
impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 {
            panic!(
                "Guess value must be greater than or equal to 1, got {}.",
                value
            );
        } else if value > 100 {
            panic!(
                "Guess value must be less than or equal to 100, got {}.",
                value
            );
        }

        Guess { value }
    }
}

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

    #[test]
    #[should_panic(expected = "Guess value must be less than or equal to 100")]
    fn greater_than_100() {
        Guess::new(200);
    }
}

테스트에서 Result<T, E> 사용하기

  • 실패했을 때 panic하는 대신에 Result<T, E>를 사용할 수 있다.
  • 리턴 타입을 Result<T, E>로 사용한다.
  • 이렇게 하면 테스트 본문에서 ?를 사용할 수 있게 해준다.
  • 단, #[should_panic]을 사용할 수 없다.
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() -> Result<(), String> {
        if 2 + 2 == 4 {
            Ok(())
        } else {
            Err(String::from("two plus two does not equal four"))
        }
    }
}

테스트 플로우 컨트롤하기

테스트를 병렬적/순차적으로 실행하기

  • 기본적으로 Rust의 테스트는 쓰레드를 활용하여 병렬적으로 돌아간다.
  • 만약 같은 파일에 무언가를 쓰는 테스트가 있다면 정상적인 코드라도 날 확률이 있다.
    • 이 때는 다른 파일을 쓰거나, 테스트를 한 번에 하나씩만 돌아가게 한다.
  • 아래의 옵션을 통해 하나의 쓰레드에서 돌아가도록 할 수 있다.
cargo test -- --test-threads=1

함수의 결과 출력하기

  • 기본적으로 Rust에서는 함수에서 사용한 println!을 보여주지 않는다.
  • 아웃풋을 보고 싶다면 다음의 커맨드 사용.한다
cargo test -- --show-output

이름으로 일부 테스트만 실행하기

// src/lib.rs

pub fn add_two(a: i32) -> i32 {
    a + 2
}

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

    #[test]
    fn add_two_and_two() {
        assert_eq!(4, add_two(2));
    }

    #[test]
    fn add_three_and_two() {
        assert_eq!(5, add_two(3));
    }

    #[test]
    fn one_hundred() {
        assert_eq!(102, add_two(100));
    }
}
  • one_hundred 하나의 함수를 테스트하고 싶다면
cargo test one_hundred
  • add라는 이름을 포함한 여러개의 테스트를 돌리고 싶다면
cargo test add
  • 위의 커맨드를 실행하면 add_two_and_two, add_three_and_two함수가 테스트 된다.

무거운 테스트 무시하기

  • 시간이 오래걸리는 테스트는 ignore속성으로 테스트에서 제외할 수 있다.
// src/lib.rs

#[test]
fn it_works() {
    assert_eq!(2 + 2, 4);
}

#[test]
#[ignore]
fn expensive_test() {
    // code that takes an hour to run
}
  • 이렇게 하면, expensive_test는 테스트시에 테스트 되지 않는다.
  • ignore된 테스트는 아래의 커맨드로 따로 돌릴 수 있다.
cargo test -- --ignored

테스트 조직화

  • 유닛테스트
    • 작고, 더 포커스되어 있음
    • 한 번에 하나의 모듈을 독립적으로 테스트함
    • private 인터페이스도 테스트 가능
  • 통합테스트
    • 라이브러리와 분리
    • 외부 코드가 사용하는 방식과 똑같이 코드를 사용
    • 오로지 public 인터페이스만 테스트
    • 테스트당 여러개의 모듈을 활용할 가능

유닛 테스트 (Unit Tests)

  • 컨벤션: 각 파일 안에 tests라는 이름을 가진 모듈을 만든다. 해당 모듈은 테스트 함수를 포함하고 그 모듈을 cfg(test) 로 어노테이트 한다.

tests 모듈과 #[cfg(tests)]

  • 유닛테스트에서 #[cfg(tests)]가 들어가면, 해당 모듈은 테스트 시에만 빌드
// src/lib.rs

#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}

Private 함수 테스트하기

  • 러스트에서는 private함수도 테스트 가능하다.
// src/lib.rs
pub fn add_two(a: i32) -> i32 {
    internal_adder(a, 2)
}

fn internal_adder(a: i32, b: i32) -> i32 {
    a + b
}

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

    #[test]
    fn internal() {
        assert_eq!(4, internal_adder(2, 2));
    }
}

통합테스트 (Integration Tests)

  • 통합테스트는 완전 라이브러리와 분리되어 있다.
  • 다른 코드가 라이브러리를 사용하는 방식과 동리하게 사용한다. 즉, 라이브러리의 public API만을 호출할 수 있다.
  • 통합테스트의 목적은 라이브러리의 많은 부분이 함께 올바르게 동작할 수 있는지를 테스트하는 것이다.

tests 디렉토리

  • 프로젝트 최상단 디렉토리에 tests디렉토리를 만든다. (src와 동등한 위치)
  • tests디렉토리에 파일을 만든다.
  • tests내의 파일은 각각 별개의 crate이므로 라이브러리를 테스트 crate의 scope로 가져와야함. (use adder)
// tests/integration_test.rs

use adder;

#[test]
fn it_adds_two() {
    assert_eq!(4, adder::add_two(2));
}
  • 특정 테스트만 실행
cargo test --test integration_test

통합테스트의 서브모듈

  • 많은 테스트에서 공통으로 사용되는 부분을 모아서 helper 함수들을 만들면 유용할 수 있다. 그냥 tests디렉토리 내에 파일을 만들면 해당 파일역시 테스트로 인식되므로 다음과 같은 tests디렉토리내에 common이라는 폴더를 만들고 그 안에 mod.rs를 만든다.
// tests/common/mod.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}
  • 이는 통합테스트 어느곳에서나 사용가능하다.
// tests/integration_test.rs

use adder;

mod common;

#[test]
fn it_adds_two() {
    common::setup();
    assert_eq!(4, adder::add_two(2));
}

Binary Crates를 위한 통합 테스트

만약 프로젝트가 src/main.rs만을 포함하고 있는 binary crate라면 통합테스트를 만들 수 없다. 오로지 library crates만이 다른 crates가 사용할 수 있는 함수를 노출 시 킬 수 있다.


반응형