Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

테스트 구성하기

이 장의 시작에서 이야기했듯, 테스트는 복잡한 분야이고 사람마다 용어나 구성 방식이 조금씩 다릅니다. 러스트 커뮤니티는 테스트를 크게 두 범주로 생각합니다. 단위 테스트와 통합 테스트입니다. 단위 테스트(unit tests) 는 작고 더 집중되어 있으며, 한 번에 한 모듈만 격리해 테스트하고, private 인터페이스도 검증할 수 있습니다. 통합 테스트 (integration tests) 는 라이브러리 바깥에 완전히 독립적으로 존재하며, 공개 인터페이스만 사용해 외부 코드가 쓰는 방식 그대로 라이브러리를 사용합니다. 그리고 한 테스트 안에서 여러 모듈을 함께 건드릴 수도 있습니다.

라이브러리의 각 부분이 개별적으로, 그리고 함께 동작할 때 모두 기대대로 작동하는지 확인하려면 두 종류의 테스트를 모두 작성하는 것이 중요합니다.

단위 테스트

단위 테스트의 목적은 코드의 각 단위를 나머지 코드와 격리해 테스트함으로써, 어디서 코드가 기대대로 작동하는지, 어디서 그렇지 않은지를 빠르게 짚어내는 데 있습니다. 단위 테스트는 src 디렉터리 안에서, 테스트 대상 코드와 같은 파일에 둡니다. 관례적으로 각 파일 안에 tests 라는 모듈을 만들고, 그 안에 테스트 함수를 넣으며, 모듈 자체에는 cfg(test) 를 붙입니다.

tests 모듈과 #[cfg(test)]

tests 모듈에 붙은 #[cfg(test)] 주석은, cargo build 가 아니라 cargo test 를 실행할 때만 그 테스트 코드를 컴파일하고 실행하라고 러스트에게 알려 줍니다. 이 덕분에 라이브러리만 빌드하고 싶을 때는 컴파일 시간을 아끼고, 최종 산출물에도 테스트 코드가 포함되지 않으므로 공간도 절약됩니다. 통합 테스트는 다른 디렉터리에 들어가기 때문에 #[cfg(test)] 주석이 필요 없지만, 단위 테스트는 실제 코드와 같은 파일에 들어가기 때문에 이렇게 명시해 주어야 합니다.

이 장의 첫 절에서 새 adder 프로젝트를 만들었을 때, Cargo가 다음 코드를 자동으로 만들어 준 것을 떠올려 보세요.

파일명: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

자동 생성된 tests 모듈에 붙은 cfg 속성은 configuration 의 약자이며, 어떤 설정 옵션이 주어졌을 때만 바로 다음 항목을 포함하라고 러스트에게 알려 줍니다. 여기서 설정 옵션은 러스트가 테스트를 컴파일하고 실행할 때 제공하는 test 입니다. 따라서 cfg 속성을 사용하면 Cargo는 우리가 명시적으로 cargo test 를 실행했을 때만 테스트 코드를 컴파일합니다. 이는 #[test] 가 붙은 함수뿐 아니라, 이 모듈 안에 있을 수 있는 각종 헬퍼 함수에도 마찬가지로 적용됩니다.

private 함수 테스트하기

테스트 커뮤니티 안에서는 private 함수를 직접 테스트해야 하는지에 대해 논쟁이 있고, 어떤 언어는 private 함수를 테스트하기 어렵거나 아예 불가능하게 만들기도 합니다. 여러분이 어떤 테스트 철학을 따르든 상관없이, 러스트의 privacy 규칙은 private 함수 테스트를 허용합니다. 목록 11-12의 internal_adder private 함수를 보세요.

Filename: src/lib.rs
pub fn add_two(a: u64) -> u64 {
    internal_adder(a, 2)
}

fn internal_adder(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn internal() {
        let result = internal_adder(2, 2);
        assert_eq!(result, 4);
    }
}
Listing 11-12: private 함수 테스트하기

internal_adder 함수에는 pub 가 붙어 있지 않다는 점에 주목하세요. 하지만 테스트는 그냥 러스트 코드이고, tests 모듈도 그냥 또 하나의 모듈입니다. [“모듈 트리의 항목을 경로로 가리키기”][paths] 절에서 설명했듯, 자식 모듈 안의 항목은 조상 모듈 안의 항목을 사용할 수 있습니다. 이 테스트에서는 use super::*; 를 사용해 tests 모듈의 부모 안에 있는 모든 항목을 스코프로 가져오고, 그 덕분에 테스트가 internal_adder 를 호출할 수 있습니다. 만약 private 함수는 테스트하지 않아야 한다고 생각한다면, 러스트가 여러분에게 억지로 그렇게 하게 만들지는 않습니다.

통합 테스트

러스트에서 통합 테스트는 라이브러리 밖에 완전히 독립적으로 존재합니다. 그것들은 외부 코드가 라이브러리를 사용하는 방식과 똑같이 라이브러리를 사용하므로, 라이브러리의 public API 안에 있는 함수만 호출할 수 있습니다. 목적은 라이브러리의 여러 부분이 함께 올바르게 동작하는지 검증하는 것입니다. 개별 단위들은 혼자서는 잘 동작하더라도, 통합되었을 때 문제가 생길 수 있으므로 통합된 코드에 대한 테스트 커버리지도 중요합니다. 통합 테스트를 만들려면 먼저 tests 디렉터리가 필요합니다.

tests 디렉터리

프로젝트 최상위 디렉터리에서 src 와 나란한 위치에 tests 디렉터리를 만듭니다. Cargo는 통합 테스트 파일을 이 디렉터리에서 찾는다는 사실을 알고 있습니다. 그리고 그 안에 원하는 만큼의 테스트 파일을 만들 수 있으며, Cargo는 각 파일을 개별 크레이트처럼 컴파일합니다.

이제 하나의 통합 테스트를 만들어 봅시다. 목록 11-12의 코드가 아직 src/lib.rs 안에 있다고 가정하고, tests 디렉터리를 만든 뒤 tests/integration_test.rs 라는 새 파일을 생성합니다. 디렉터리 구조는 다음처럼 됩니다.

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

tests/integration_test.rs 파일 안에는 목록 11-13의 코드를 넣습니다.

Filename: tests/integration_test.rs
use adder::add_two;

#[test]
fn it_adds_two() {
    let result = add_two(2);
    assert_eq!(result, 4);
}
Listing 11-13: adder 크레이트 함수에 대한 통합 테스트

tests 디렉터리 안의 각 파일은 별도의 크레이트이므로, 우리 라이브러리를 각 테스트 크레이트의 스코프로 가져와야 합니다. 그래서 단위 테스트에서는 필요 없었던 use adder::add_two; 줄을 코드 맨 위에 추가합니다.

또한 tests/integration_test.rs 안의 코드에는 #[cfg(test)] 를 붙일 필요가 없습니다. Cargo는 tests 디렉터리를 특별하게 취급하며, cargo test 를 실행할 때만 그 안의 파일을 컴파일합니다. 이제 cargo test 를 실행해 봅시다.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.31s
     Running unittests src/lib.rs (target/debug/deps/adder-1082c4b063a8fbe6)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

출력은 세 부분으로 이루어져 있습니다. 단위 테스트, 통합 테스트, 문서 테스트입니다. 한 섹션 안에서 테스트 하나라도 실패하면, 뒤의 섹션들은 실행되지 않는다는 점에 주의하세요. 예를 들어 단위 테스트가 실패하면 통합 테스트와 문서 테스트는 실행되지 않습니다. 이 둘은 단위 테스트가 모두 통과했을 때만 실행됩니다.

첫 번째 단위 테스트 섹션은 앞에서 보던 것과 같습니다. 각 단위 테스트마다 한 줄씩 있고(여기서는 목록 11-12에서 추가한 internal 이라는 이름의 테스트 하나가 있습니다), 그 뒤에 단위 테스트 요약 줄이 나옵니다.

통합 테스트 섹션은 Running tests/integration_test.rs 라는 줄로 시작합니다. 그 다음에는 해당 통합 테스트 파일 안 각 테스트 함수마다 한 줄씩, 그리고 Doc-tests adder 섹션 바로 전에 통합 테스트 결과 요약 줄이 나옵니다.

각 통합 테스트 파일은 자기만의 섹션을 가지므로, tests 디렉터리에 파일을 더 추가하면 그만큼 통합 테스트 섹션도 늘어납니다.

특정 통합 테스트 함수 하나만 실행하고 싶다면 그 함수 이름을 cargo test 의 인수로 지정하면 됩니다. 특정 통합 테스트 파일 전체를 실행하려면 cargo test--test 인수 뒤에 파일 이름을 붙입니다.

$ cargo test --test integration_test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
     Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

이 명령은 tests/integration_test.rs 파일 안의 테스트들만 실행합니다.

통합 테스트 안의 하위 모듈

통합 테스트가 많아지면, 이를 정리하기 위해 tests 디렉터리 안에 파일을 더 만들고 싶어질 수 있습니다. 예를 들어 테스트 대상 기능별로 테스트 함수를 묶을 수 있습니다. 앞서 말했듯 tests 디렉터리 안의 각 파일은 별도 크레이트로 컴파일되는데, 이는 최종 사용자가 여러분의 크레이트를 사용하는 방식과 더 비슷한 별도 스코프를 만들 때는 유용합니다. 하지만 이 때문에 tests 안 파일들은, 7장에서 코드와 파일을 모듈로 나누는 법을 배울 때 봤던 src 안 파일들과는 다른 동작을 보입니다.

이 차이는 여러 통합 테스트 파일에서 공통으로 쓸 헬퍼 함수 집합이 있을 때 특히 눈에 띕니다. 그리고 7장의 [“모듈을 여러 파일로 분리하기”] [separating-modules-into-files] 절에서 했던 것처럼, 그 공통 함수를 공용 모듈로 빼내려 할 때 드러납니다. 예를 들어 tests/common.rs 를 만들고 그 안에 setup 이라는 함수를 두면, 여러 테스트 파일의 여러 테스트 함수에서 공통 호출하고 싶은 코드를 거기에 넣을 수 있을 것 같습니다.

파일명: tests/common.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}

이제 테스트를 다시 실행하면, common.rs 파일 안에는 테스트 함수가 하나도 없고 우리가 setup 함수도 어디서도 호출하지 않았는데도, 테스트 출력에 common.rs 에 대한 새 섹션이 생깁니다.

$ cargo test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.89s
     Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)

running 1 test
test tests::internal ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test it_adds_two ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

common 이 테스트 결과에 나타나면서 running 0 tests 라고 나오는 것은 우리가 원하는 결과가 아닙니다. 우리는 단지 다른 통합 테스트 파일과 공유할 코드가 필요했을 뿐입니다. 이 문제를 피하려면 tests/common.rs 를 만드는 대신, tests/common/mod.rs 를 만들면 됩니다. 그러면 프로젝트 디렉터리는 이제 다음처럼 보입니다.

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

이것은 7장의 [“대체 파일 경로”][alt-paths] 절에서 언급했던, 러스트가 여전히 이해하는 예전 스타일 파일 명명 규칙입니다. 파일 이름을 이렇게 두면 러스트는 common 모듈을 통합 테스트 파일로 취급하지 않습니다. setup 함수 코드를 tests/common/mod.rs 로 옮기고 tests/common.rs 파일을 삭제하면, 테스트 출력에 그 섹션은 더 이상 나타나지 않습니다. tests 디렉터리 안의 하위 디렉터리 파일은 별도 크레이트로 컴파일되지 않고, 테스트 결과에 독립 섹션도 생기지 않습니다.

tests/common/mod.rs 를 만든 뒤에는, 어떤 통합 테스트 파일에서도 그것을 모듈처럼 사용할 수 있습니다. 다음은 tests/integration_test.rsit_adds_two 테스트에서 setup 함수를 호출하는 예입니다.

파일명: tests/integration_test.rs

use adder::add_two;

mod common;

#[test]
fn it_adds_two() {
    common::setup();

    let result = add_two(2);
    assert_eq!(result, 4);
}

여기서 mod common; 선언은 목록 7-21에서 보였던 일반 모듈 선언과 똑같습니다. 그 다음 테스트 함수 안에서 common::setup() 을 호출할 수 있습니다.

바이너리 크레이트에 대한 통합 테스트

만약 프로젝트가 오직 src/main.rs 만 가지고 있고 src/lib.rs 가 없는 바이너리 크레이트라면, tests 디렉터리에 통합 테스트를 만들어도 src/main.rs 에 정의된 함수를 use 로 스코프로 가져올 수는 없습니다. 다른 크레이트가 사용할 수 있는 함수를 노출하는 것은 라이브러리 크레이트뿐이고, 바이너리 크레이트는 그 자체로 실행되는 것이 목적이기 때문입니다.

이것이, 바이너리를 제공하는 러스트 프로젝트가 흔히 src/main.rs 는 단순하게 두고 핵심 로직은 src/lib.rs 안에 두는 이유 중 하나입니다. 그런 구조를 사용하면, 통합 테스트는 use 로 라이브러리 크레이트를 가져와 중요한 기능을 테스트할 수 있습니다. 핵심 로직이 올바르게 동작한다면, src/main.rs 안의 적은 양의 코드는 대체로 자연스럽게 올바르게 동작하므로 그 부분은 별도로 테스트하지 않아도 됩니다.

정리

러스트의 테스트 기능은 코드가 어떻게 동작해야 하는지를 명시하고, 이후 코드를 수정해도 여전히 기대한 대로 동작하는지를 확인하게 해 줍니다. 단위 테스트는 라이브러리의 여러 부분을 각각 독립적으로 검증하며, private 구현 세부도 테스트할 수 있습니다. 통합 테스트는 라이브러리의 여러 부분이 함께 올바르게 동작하는지를 검사하며, 외부 코드가 사용하는 방식 그대로 public API 를 통해 코드를 테스트합니다. 러스트의 타입 시스템과 소유권 규칙이 일부 버그를 예방해 주긴 하지만, 코드가 어떤 동작을 해야 하는지에 관한 로직 버그를 줄이기 위해 테스트는 여전히 중요합니다.