#15 테스트

Pt J·2020년 8월 27일
0

[完] Rust Programming

목록 보기
18/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

다음은 Edsger W. Dijkstra가 그의 에세이 "The Humble Programmer"에서 한 말이다.

Program testing can be a very effective way to show the presence of bugs, but it is hopelessly inadequate for showing their absence.
프로그램 테스트는 버그의 존재를 보여주는 효과적인 방법이지만, 불행히도 그것의 부재를 보여주기에는 불합리한 방법이다.

// 다익스트라... 그 Dijkstra가 맞다. 뭐하는 인간인지 모르겠다 그저 超人...

Rust는 프로그램에 존재할 수 있는 버그를 보다 쉽게 찾아낼 수 있도록
언어 차원에서 자동화된 테스트의 작성을 지원한다.
이것은 컴파일러가 잡아내지 못하는 논리적인 오류를 잡아낸다.

테스트 작성

테스트는 테스트 함수를 통해 이루어지며
테스트 함수의 본문은 다음과 같은 세 가지 동작으로 이루어져 있다.

  1. 필요한 자료나 상태를 설정한다.
    Set up any needed data or state.
  2. 테스트하고자 하는 코드를 실행한다.
    Run the code you want to test.
  3. 기대했던 결과가 맞는지 확인한다.
    Assert the results are what you expect.

테스트 함수

테스트 함수는 test 특성이 부여된 함수다.
특성이란 모듈 및 함수 등에 부여할 수 있는 메타데이터로, #[] 애노테이션을 통해 부여된다.
우리가 출력할 수 없는 값을 출력하고자 할 때 사용했던 #[derive(Debug)] 또한 특성이다.
테스트 함수를 만들기 위해서는 #[test]라는 애노테이션을 사용한다.
우리가 cargo new를 할 때 --lib 옵션을 주어 라이브러리 프로젝트로 생성하면
테스트 모듈과 테스트 함수가 포함된 예제 프로젝트가 생성된다.

이를 확인하기 위해 라이브러리 프로젝트를 하나 만들어보자.

peter@hp-laptop:~/rust-practice$ mkdir chapter11
peter@hp-laptop:~/rust-practice$ cd chapter11
peter@hp-laptop:~/rust-practice/chapter11$ cargo new adder --lib
     Created library `adder` package
peter@hp-laptop:~/rust-practice/chapter11$ cd adder/
peter@hp-laptop:~/rust-practice/chapter11/adder$ vi src/lib.rs

src/lib.rs

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

#[cfg(test)]는 모듈 tests가 테스트 모듈임을 명시하고
#[test]는 함수 it_works가 테스트 함수임을 명시한다.
그리고 함수 본문에 있는 assert_eq! 매크로는 기대했던 결과가 맞는지 확인하는 역할을 한다.
이 녀석에게 전달된 두 인자의 값이 일치하면 테스트는 성공하는 것이다.
그렇지 않을 경우 이 녀석은 panic! 매크로를 호출한다.

프로젝트에 존재하는 모든 테스트를 수행하기 위해 cargo test 명령어를 사용한다.

peter@hp-laptop:~/rust-practice/chapter11/adder$ cargo test
   Compiling adder v0.1.0 (/home/peter/rust-practice/chapter11/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.22s
     Running target/debug/deps/adder-78d3f1eae8c361b6

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

peter@hp-laptop:~/rust-practice/chapter11/adder$

총 1개의 테스트가 수행되었고 그것이 test 모듈의 it_works 함수이며
성공적으로 ok가 떴다는 것을 확인할 수 있다.
그리고 테스트 전체의 성공 여부와 함께 짧은 통계를 확인할 수 있다.
Doc-tests adder와 그 아래 부분은 API 문서의 예제 코드 테스트인데
이에 대해서는 문서화에 대한 이야기를 할 때 이야기하도록 하겠다.

테스트 함수는 내부 어딘가에서 패닉이 발생했을 때 실패로 취급된다.
테스트 실패 상황을 알아보기 위해 누가 봐도 실패할 테스트 함수를 추가해보겠다.

peter@hp-laptop:~/rust-practice/chapter11/adder$ vi src/lib.rs

src/lib.rs

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

    #[test]
    fn another() {
        panic!("Make this tst fail");
    }
}
peter@hp-laptop:~/rust-practice/chapter11/adder$ cargo test
   Compiling adder v0.1.0 (/home/peter/rust-practice/chapter11/adder)
    Finished test [unoptimized + debuginfo] target(s) in 0.21s
     Running target/debug/deps/adder-78d3f1eae8c361b6

running 2 tests
test tests::it_works ... ok
test tests::another ... FAILED

failures:

---- tests::another stdout ----
thread 'tests::another' panicked at 'Make this tst fail', src/lib.rs:10:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::another

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--lib'
peter@hp-laptop:~/rust-practice/chapter11/adder$

tests 모듈의 it_works 함수는 이전과 같이 성공하였으나
같은 모듈의 another 함수는 실패한 것을 확인할 수 있다.
그리고 그 아래로 실패한 테스트가 왜 실패하였는지 뜨는 것을 볼 수 있다.

테스트 매크로

위에서 우리는 두 값이 일치하는지 확인하는 테스트 매크로 assert_eq!을 알게 되었다.
테스트 매크로에는 이것 외에도 몇 가지가 더 존재한다.

assert! 매크로는 불리언으로 평가되는 표현식을 인자로 받아
그것이 true일 때 테스트가 성공하도록 하는 매크로다.

간단한 테스트 예제를 작성하여 그 동작을 알아보자.

peter@hp-laptop:~/rust-practice/chapter11/adder$ cd ..
peter@hp-laptop:~/rust-practice/chapter11$ cargo new rectangle --lib
     Created library `rectangle` package
peter@hp-laptop:~/rust-practice/chapter11$ cd rectangle/
peter@hp-laptop:~/rust-practice/chapter11/rectangle$ vi src/lib.rs

src/lib.rs

pub struct Rectanlge {
    length: u32, 
    width: u32,
}

impl Rectanlge {
    pub fn can_hold(&self, other: &Rectanlge) -> bool {
        self.length > other.length && self.width > other.width
    }
}

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

    #[test]
    fn larger_can_hold_smaller() {
        let larger = Rectanlge {
            length: 8,
            width: 7,
        };
        let smaller = Rectanlge {
            length: 5,
            width: 1,
        };

        assert!(larger.can_hold(&smaller));
    }

    #[test]
    fn smaller_cannot_hold_larger() {
        let larger = Rectanlge {
            length: 8,
            width: 7,
        };
        let smaller = Rectanlge {
            length: 5,
            width: 1,
        };

        assert!(!smaller.can_hold(&larger));
    }
}

테스트 모듈 바깥에 정의된 녀석을 사용하기 위해서는
use super::*;를 적어주어야 한다는 것을 유의하자.
이대로 테스트를 진행해보면 다음과 같은 결과를 확인할 수 있다.

peter@hp-laptop:~/rust-practice/chapter11/rectangle$ cargo test
   Compiling rectangle v0.1.0 (/home/peter/rust-practice/chapter11/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.28s
     Running target/debug/deps/rectangle-4d20927d27fa26e2

running 2 tests
test tests::smaller_cannot_hold_larger ... ok
test tests::larger_can_hold_smaller ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests rectangle

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

peter@hp-laptop:~/rust-practice/chapter11/rectangle$ 

만약 우리가 can_hold 메서드에서 실수로 부등호를 반대로 적었다고 하자.
그러면 이것은 can_hold 메서드에 버그가 있는 것이다.
따라서 다음과 같이 테스트가 실패하는 것을 확인할 수 있다.

peter@hp-laptop:~/rust-practice/chapter11/rectangle$ cargo test
   Compiling rectangle v0.1.0 (/home/peter/rust-practice/chapter11/rectangle)
    Finished test [unoptimized + debuginfo] target(s) in 0.24s
     Running target/debug/deps/rectangle-4d20927d27fa26e2

running 2 tests
test tests::larger_can_hold_smaller ... FAILED
test tests::smaller_cannot_hold_larger ... FAILED

failures:

---- tests::larger_can_hold_smaller stdout ----
thread 'tests::larger_can_hold_smaller' panicked at 'assertion failed: larger.can_hold(&smaller)', src/lib.rs:28:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

---- tests::smaller_cannot_hold_larger stdout ----
thread 'tests::smaller_cannot_hold_larger' panicked at 'assertion failed: !smaller.can_hold(&larger)', src/lib.rs:42:9


failures:
    tests::larger_can_hold_smaller
    tests::smaller_cannot_hold_larger

test result: FAILED. 0 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--lib'
peter@hp-laptop:~/rust-practice/chapter11/rectangle$ 

테스트 함수를 사용하면 이렇게 쉽게 버그를 잡아낼 수 있다.

assert! 외에도 앞서 언급된 assert_eq!, 그리고 그것과 반대인 assert_ne!도 많이 쓰인다.
assert_ne!는 전달된 두 인자의 값이 다를 때 성공하는 테스트 매크로다.
전달되는 두 인자를 1st2nd로 표기할 때
assert_eq!assert!(1st == 2nd)와,
assert_ne!assert!(1st != 2nd)와 같은 기능을 한다고 볼 수 있다.
이러한 비교 연산이 사용되기에 이 녀석들에게 전달되는 인자는
PartialEq 트레이트를 구현하고 있어야 하며
실패 시 전달된 인자를 출력해주므로 Debug 트레이트도 구현하고 있어야 한다.
표준 라이브러리가 제공하는 자료형의 경우 이들을 구현하고 있어 크게 신경쓰지 않아도 되지만
직접 만든 녀석의 경우 이 부분을 유의하자.

만약 테스트 매크로를 사용할 때 실패 시 출력할 오류 메시지를 설정하고 싶다면
추가적으로 format! 매크로를 마지막 인자로 전달할 수 있다.
assert!의 경우 두 번째 인자, 나머지 둘은 세 번째 인자다.
format! 매크로는 {}와 그곳에 들어갈 값을 통해 문자열을 지정할 수 있다.
println! 매크로에서 사용하는 것과 같은 방식이다.

peter@hp-laptop:~/rust-practice/chapter11/rectangle$ cd ..
peter@hp-laptop:~/rust-practice/chapter11$ cargo new assert_format --lib     Created library `assert_format` package
peter@hp-laptop:~/rust-practice/chapter11$ cd assert_format/
peter@hp-laptop:~/rust-practice/chapter11/assert_format$ vi src/lib.rs

src/lib.rs

pub fn greeting(name: &str) -> String {
    String::from("Hello?")
}

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

    #[test]
    fn greeting_contains_name() {
        let result = greeting("Peter");
        assert!(result.contains("Peter"),
            "Greeting did not contain name, value was '{}'",
            result
        );
    }
}
peter@hp-laptop:~/rust-practice/chapter11/assert_format$ cargo test
   Compiling assert_format v0.1.0 (/home/peter/rust-practice/chapter11/assert_format)

# snip the warning

    Finished test [unoptimized + debuginfo] target(s) in 0.36s
     Running target/debug/deps/assert_format-969639b2358abb30

running 1 test
test tests::greeting_contains_name ... FAILED

failures:

---- tests::greeting_contains_name stdout ----
thread 'tests::greeting_contains_name' panicked at 'Greeting did not contain name, value was 'Hello?'', src/lib.rs:12:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    tests::greeting_contains_name

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

error: test failed, to rerun pass '--lib'
peter@hp-laptop:~/rust-practice/chapter11/assert_format$ 

이제 panicked at 뒤에 우리가 format! 매크로를 통해 설정한 문자열이 출력된다.

음성 테스트 Negative Test

때로는 정상적인 상황에서 정상적으로 작동하는가 외에도
정상적이지 않은 상황에서 정상적이지 않다고 하는가도 테스트할 필요가 있다.
이러한 테스트를 음성 테스트라고 한다.
Rust에서는 #[should_panic] 애노테이션을 통해 음성 테스트 속성을 부여할 수 있다.
이것이 부여된 테스트 함수는 내부에서 패닉이 발생해야 성공한다.
음성 테스트 속성이 명시적으로 부여되지 않은 모든 테스트 함수는
정상적인 상황에서 정상적으로 작동할 것을 확인하는 양성 테스트 함수다.

음성 테스트가 포함된 코드를 작성해보자.
1 이상 100 이하의 값에 대해서만 구조체 인스턴스를 생성하고
그 범위 밖의 값이 들어오면 panic을 띄우는 연관함수의 음성 테스트다.

peter@hp-laptop:~/rust-practice/chapter11/assert_format$ cd ..
peter@hp-laptop:~/rust-practice/chapter11$ cargo new negative_test --lib     Created library `negative_test` package
peter@hp-laptop:~/rust-practice/chapter11$ cd negative_test/
peter@hp-laptop:~/rust-practice/chapter11/negative_test$ vi src/lib.rs

src/lib.rs

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);
    }
}
peter@hp-laptop:~/rust-practice/chapter11/negative_test$ cargo test
   Compiling negative_test v0.1.0 (/home/peter/rust-practice/chapter11/negative_test)

# snip the warning

    Finished test [unoptimized + debuginfo] target(s) in 0.23s
     Running target/debug/deps/negative_test-af0ef2389088b752

running 1 test
test tests::greater_than_100 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests negative_test

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

peter@hp-laptop:~/rust-practice/chapter11/negative_test$

그런데 여기서 한 가지 문제가 있다.
의도된 panic이 아닌 다른 panic에 의해 테스트가 성공할 수도 있다는 것이다.
다행히도 Rust는 이를 고려하여 의도한 panic이 전달될 때만 성공하도록 구현할 수 있게 하였다.
should_panic 특성을 부여할 때 expected 매개변수를 사용하여
지금 발생한 panic과 테스트 함수가 기댄 panic을 비교할 수 있다.
그 결과 동일한 panic이 발생했을 때만 테스트를 성공했다고 할 수 있다.

peter@hp-laptop:~/rust-practice/chapter11/negative_test$ vi src/lib.rs

src/lib.rs

pub struct Guess {
    value: i32,
}

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);
    }
}
peter@hp-laptop:~/rust-practice/chapter11/negative_test$ cargo test
   Compiling negative_test v0.1.0 (/home/peter/rust-practice/chapter11/negative_test)

# snip the warning

    Finished test [unoptimized + debuginfo] target(s) in 0.22s
     Running target/debug/deps/negative_test-af0ef2389088b752

running 1 test
test tests::greater_than_100 ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests negative_test

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

peter@hp-laptop:~/rust-practice/chapter11/negative_test$ 

이것이 작동하기 위해서는 expected에 인자로 전달된 문자열이 포함된 메시지가 담긴
panic이 발생해야만 한다.
panic이 발생하지 않거나 이 문자열을 포함하지 않는 panic이 발생하면 실패한다.

이 포스트의 내용은 공식문서의 11장 1절 How to Write Tests에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글