Rust Book 랩: 유닛 테스트 및 통합 테스트

Beginner

This tutorial is from open-source community. Access the source code

소개

Test Organization에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.

이 랩에서는 Rust 커뮤니티에서 사용되는 두 가지 주요 테스트 범주에 대해 배우게 됩니다. 즉, 격리된 개별 모듈을 테스트하는 데 초점을 맞춘 작고 세분화된 유닛 테스트 (unit tests) 와 라이브러리의 공개 인터페이스를 사용하고 테스트당 잠재적으로 여러 모듈을 실행하는 통합 테스트 (integration tests) 입니다.

테스트 구성

이 장의 시작 부분에서 언급했듯이, 테스트는 복잡한 분야이며, 다양한 사람들이 서로 다른 용어와 구성을 사용합니다. Rust 커뮤니티는 테스트를 두 가지 주요 범주, 즉 유닛 테스트 (unit tests) 와 통합 테스트 (integration tests) 로 생각합니다. 유닛 테스트는 작고 더 집중적이며, 한 번에 하나의 모듈을 격리하여 테스트하며, 비공개 인터페이스 (private interfaces) 를 테스트할 수 있습니다. 통합 테스트는 라이브러리에 완전히 외부적이며, 다른 외부 코드와 동일한 방식으로 코드를 사용하며, 공개 인터페이스 (public interface) 만 사용하고 테스트당 잠재적으로 여러 모듈을 실행합니다.

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

유닛 테스트 (Unit Tests)

유닛 테스트의 목적은 코드의 각 유닛을 나머지 코드와 격리하여 테스트하여 코드가 예상대로 작동하는지 여부를 빠르게 파악하는 것입니다. 유닛 테스트는 테스트 중인 코드가 있는 각 파일의 src 디렉토리에 넣습니다. 규칙은 테스트 함수를 포함하는 tests라는 모듈을 각 파일에 생성하고 해당 모듈에 cfg(test)를 주석 처리하는 것입니다.

테스트 모듈 (Tests Module) 과 #[cfg(test)]

tests 모듈의 #[cfg(test)] 어노테이션은 cargo build를 실행할 때가 아니라 cargo test를 실행할 때만 테스트 코드를 컴파일하고 실행하도록 Rust 에 지시합니다. 이렇게 하면 라이브러리만 빌드하려는 경우 컴파일 시간을 절약하고, 테스트가 포함되지 않으므로 결과 컴파일된 아티팩트의 공간을 절약할 수 있습니다. 통합 테스트는 다른 디렉토리에 있기 때문에 #[cfg(test)] 어노테이션이 필요하지 않다는 것을 알 수 있습니다. 그러나 유닛 테스트는 코드와 동일한 파일에 있기 때문에 #[cfg(test)]를 사용하여 컴파일된 결과에 포함되지 않도록 지정합니다.

이 장의 첫 번째 섹션에서 새로운 adder 프로젝트를 생성했을 때 Cargo 가 우리를 위해 이 코드를 생성했음을 기억하십시오.

파일 이름: src/lib.rs

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

이 코드는 자동으로 생성된 tests 모듈입니다. cfg 속성은 *구성 (configuration)*을 의미하며, 특정 구성 옵션이 주어질 때만 다음 항목을 포함하도록 Rust 에 지시합니다. 이 경우 구성 옵션은 test이며, 이는 테스트를 컴파일하고 실행하기 위해 Rust 에서 제공됩니다. cfg 속성을 사용하면 cargo test로 테스트를 활발하게 실행하는 경우에만 Cargo 가 테스트 코드를 컴파일합니다. 여기에는 #[test]로 주석 처리된 함수 외에도 이 모듈 내에 있을 수 있는 모든 도우미 함수가 포함됩니다.

비공개 함수 테스트 (Testing Private Functions)

테스팅 커뮤니티 내에서는 비공개 함수를 직접 테스트해야 하는지에 대한 논쟁이 있으며, 다른 언어에서는 비공개 함수를 테스트하기 어렵거나 불가능하게 만듭니다. 어떤 테스팅 이념을 따르든, Rust 의 개인 정보 보호 규칙을 통해 비공개 함수를 테스트할 수 있습니다. 비공개 함수 internal_adder가 있는 Listing 11-12 의 코드를 고려하십시오.

파일 이름: 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));
    }
}

Listing 11-12: 비공개 함수 테스트

internal_adder 함수는 pub로 표시되지 않았습니다. 테스트는 단지 Rust 코드일 뿐이며, tests 모듈은 또 다른 모듈일 뿐입니다. "모듈 트리에서 항목을 참조하는 경로"에서 논의했듯이, 자식 모듈의 항목은 상위 모듈의 항목을 사용할 수 있습니다. 이 테스트에서는 use super::*를 사용하여 test 모듈의 모든 부모 항목을 범위 내로 가져온 다음, 테스트에서 internal_adder를 호출할 수 있습니다. 비공개 함수를 테스트해서는 안 된다고 생각한다면, Rust 에는 그렇게 하도록 강요하는 것은 없습니다.

통합 테스트 (Integration Tests)

Rust 에서 통합 테스트는 라이브러리와 완전히 외부적으로 존재합니다. 다른 코드와 마찬가지로 라이브러리를 사용하며, 이는 라이브러리의 공개 API 의 일부인 함수만 호출할 수 있음을 의미합니다. 통합 테스트의 목적은 라이브러리의 여러 부분이 함께 올바르게 작동하는지 테스트하는 것입니다. 자체적으로 올바르게 작동하는 코드 단위는 통합될 때 문제가 발생할 수 있으므로, 통합된 코드에 대한 테스트 커버리지도 중요합니다. 통합 테스트를 생성하려면 먼저 tests 디렉토리가 필요합니다.

tests 디렉토리

프로젝트 디렉토리의 최상위 레벨에 src 옆에 tests 디렉토리를 생성합니다. Cargo 는 이 디렉토리에서 통합 테스트 파일을 찾도록 되어 있습니다. 그런 다음 원하는 만큼 많은 테스트 파일을 만들 수 있으며, Cargo 는 각 파일을 개별 크레이트 (crate) 로 컴파일합니다.

통합 테스트를 만들어 보겠습니다. Listing 11-12 의 코드가 여전히 src/lib.rs 파일에 있는 상태에서 tests 디렉토리를 만들고 tests/integration_test.rs라는 새 파일을 만듭니다. 디렉토리 구조는 다음과 같아야 합니다.

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

Listing 11-13 의 코드를 tests/integration_test.rs 파일에 입력합니다.

파일 이름: tests/integration_test.rs

use adder;

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

Listing 11-13: adder 크레이트의 함수에 대한 통합 테스트

tests 디렉토리의 각 파일은 별도의 크레이트이므로, 각 테스트 크레이트의 범위로 라이브러리를 가져와야 합니다. 이러한 이유로 코드 상단에 use adder;를 추가했는데, 이는 단위 테스트에서는 필요하지 않았습니다.

tests/integration_test.rs의 어떤 코드에도 #[cfg(test)]를 주석 처리할 필요가 없습니다. Cargo 는 tests 디렉토리를 특별하게 취급하며, cargo test를 실행할 때만 이 디렉토리의 파일을 컴파일합니다. 지금 cargo test를 실행하십시오.

[object Object]

출력의 세 섹션에는 단위 테스트, 통합 테스트 및 문서 테스트가 포함됩니다. 섹션의 테스트가 실패하면 다음 섹션은 실행되지 않습니다. 예를 들어, 단위 테스트가 실패하면 모든 단위 테스트가 통과하는 경우에만 해당 테스트가 실행되므로 통합 및 문서 테스트에 대한 출력이 없습니다.

단위 테스트에 대한 첫 번째 섹션 [1]은 우리가 보아온 것과 동일합니다. 각 단위 테스트에 대한 한 줄 (Listing 11-12 에서 추가한 internal이라는 하나) 과 단위 테스트에 대한 요약 줄입니다.

통합 테스트 섹션은 Running tests/integration_test.rs [2]로 시작합니다. 다음으로, 해당 통합 테스트의 각 테스트 함수에 대한 한 줄 [3]과 Doc-tests adder 섹션이 시작되기 직전의 통합 테스트 결과에 대한 요약 줄 [4]이 있습니다.

각 통합 테스트 파일에는 자체 섹션이 있으므로, tests 디렉토리에 더 많은 파일을 추가하면 더 많은 통합 테스트 섹션이 있을 것입니다.

cargo test에 테스트 함수의 이름을 인수로 지정하여 특정 통합 테스트 함수를 계속 실행할 수 있습니다. 특정 통합 테스트 파일의 모든 테스트를 실행하려면 cargo test--test 인수를 파일 이름과 함께 사용하십시오.

[object Object]

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

통합 테스트의 서브모듈 (Submodules)

통합 테스트를 더 추가하면서, 테스트를 구성하기 위해 tests 디렉토리에 더 많은 파일을 만들고 싶을 수 있습니다. 예를 들어, 테스트하는 기능별로 테스트 함수를 그룹화할 수 있습니다. 앞서 언급했듯이, tests 디렉토리의 각 파일은 자체 크레이트로 컴파일되므로, 최종 사용자가 크레이트를 사용하는 방식을 더 가깝게 모방하기 위해 별도의 범위를 만드는 데 유용합니다. 그러나 이는 tests 디렉토리의 파일이 코드 분할 방법을 다룬 7 장에서 배운 것처럼 src의 파일과 동일한 동작을 공유하지 않음을 의미합니다.

tests 디렉토리 파일의 다른 동작은 여러 통합 테스트 파일에서 사용할 헬퍼 함수 집합이 있고, "모듈을 다른 파일로 분리하기"의 단계를 따라 공통 모듈로 추출하려는 경우 가장 두드러집니다. 예를 들어, tests/common.rs를 생성하고 setup이라는 함수를 배치하면, 여러 테스트 파일의 여러 테스트 함수에서 호출하려는 코드를 setup에 추가할 수 있습니다.

파일 이름: tests/common.rs

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

테스트를 다시 실행하면, 이 파일에 테스트 함수가 없고 setup 함수를 어디에서도 호출하지 않았음에도 불구하고 common.rs 파일에 대한 새로운 섹션이 테스트 출력에 표시됩니다.

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와 함께 나타나는 것은 우리가 원했던 것이 아닙니다. 우리는 단지 다른 통합 테스트 파일과 일부 코드를 공유하고 싶었습니다. common이 테스트 출력에 나타나지 않도록 하려면, tests/common.rs를 생성하는 대신 tests/common/mod.rs를 생성합니다. 이제 프로젝트 디렉토리는 다음과 같습니다.

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

이것은 "대체 파일 경로"에서 언급한 Rust 가 이해하는 이전 명명 규칙입니다. 파일을 이렇게 명명하면 Rust 가 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;

mod common;

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

mod common; 선언은 Listing 7-21 에서 시연한 모듈 선언과 동일합니다. 그런 다음 테스트 함수에서 common::setup() 함수를 호출할 수 있습니다.

바이너리 크레이트 (Binary Crate) 에 대한 통합 테스트

프로젝트가 src/main.rs 파일만 포함하고 src/lib.rs 파일이 없는 바이너리 크레이트인 경우, tests 디렉토리에 통합 테스트를 생성하고 src/main.rs 파일에 정의된 함수를 use 문을 사용하여 범위 내로 가져올 수 없습니다. 다른 크레이트가 사용할 수 있는 함수를 노출하는 것은 라이브러리 크레이트뿐입니다. 바이너리 크레이트는 자체적으로 실행되도록 설계되었습니다.

이것이 바이너리를 제공하는 Rust 프로젝트가 src/lib.rs 파일에 있는 로직을 호출하는 간단한 src/main.rs 파일을 갖는 이유 중 하나입니다. 해당 구조를 사용하면 통합 테스트가 use를 사용하여 라이브러리 크레이트를 테스트하여 중요한 기능을 사용할 수 있도록 만들 수 있습니다. 중요한 기능이 작동하면 src/main.rs 파일의 소량의 코드도 작동하며, 해당 소량의 코드는 테스트할 필요가 없습니다.

요약

축하합니다! 테스트 구성 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.