프로그래밍러스트 개발자 / 테스터

러스트에서 모듈 테스트 시스템은 어떻게 작동하며, 테스트가 언어와 밀접하게 통합되는 이유는 무엇인가요? 테스트를 올바르게 구성하고, 격리 및 가독성을 보장하는 방법은 무엇인가요?

Hintsage AI 어시스턴트로 면접 통과

답변.

러스트는 신뢰성과 안전성에 중점을 두고 설계되었기 때문에 모듈 테스트는 언어 생태계의 필수적인 부분이 되었습니다. 테스트는 언어 및 툴체인 수준에서 통합되어 (cargo test) 개발 프로세스의 유기적인 부분이 됩니다.

문제의 역사

전통적인 생태계(예: C/C++, Python, Java)에서는 테스트가 프로그램과 별개로 존재했습니다. 하지만 러스트에서는 테스트가 코드의 일부이며, 완전한 모듈로 컴파일되고 검증됩니다. 이러한 시너지는 언어 구조와 컴파일러의 특성 덕분에 이루어집니다.

문제

올바른 테스트 없이는 주요 기능의 신뢰성을 보장할 수 없습니다. 복잡하고 다수의 모듈로 구성된 프로젝트에서는 테스트를 편리하게 구성하고, 다른 모듈의 상태에 의존하지 않으면서 프로젝트 구조를 복잡하게 만들지 않는 방법이 필요합니다.

해결책

러스트에서는 테스트를 소스 파일 내에 배치하거나 (#[cfg(test)] 사용) 통합 테스트를 위한 tests라는 별도 폴더에 배치할 수 있습니다. 각 모듈에 대해 비공식 테스트를 추가할 수 있고, 이는 비공식 API에 접근할 수 있습니다.

코드 예:

pub fn add(a: i32, b: i32) -> i32 { a + b } #[cfg(test)] mod tests { use super::*; #[test] fn test_add_positive() { assert_eq!(add(2, 3), 5); } #[test] fn test_add_negative() { assert_eq!(add(-1, -3), -4); } }

주요 특징:

  • 테스트는 코드와 함께 컴파일되며 모듈의 비공식 함수에 접근할 수 있습니다.
  • 통합 테스트(tests 폴더)는 라이브러리가 외부처럼 작동하는 것을 모사합니다.
  • 프로세스에 따라 테스트가 격리됩니다: 각 테스트는 독립적으로 실행되며 병렬 실행이 가능합니다.

함정 질문.

모듈 테스트에서 use super::*;는 무엇을 위한 것인가요?

테스트가 현재 모듈의 함수 및 구조체(비공식 포함)에 접근할 수 있도록 하기 위해, 테스트에서는 일반적으로 use super::*;를 사용합니다.

#[test]는 비동기일 수 있나요?

기본적으로 #[test]는 비동기를 지원하지 않지만, 외부 crate(예: tokio 또는 async-std)를 사용하면 async #[test]를 만들 수 있습니다.

코드 예:

#[tokio::test] async fn test_async_add() { assert_eq!(add(2, 2).await, 4); }

테스트가 전역 상태를 변경할 수 있나요? 이를 피하는 방법은 무엇인가요?

러스트의 테스트는 기본적으로 병렬로 실행되므로, 전역 상태(static mut)를 동기화 없이 사용하면 경합 상태가 발생합니다.

일반적인 오류 및 안티패턴

  • 테스트에서 전역 가변 변수를 사용하는 것.
  • 모듈 테스트와 통합 테스트의 구분이 모호한 것.
  • 부정 테스트 시나리오의 부재.

실생활 예

부정적 케이스

모든 테스트가 같은 전역 변수를 사용합니다:

static mut COUNTER: u32 = 0; #[test] fn test_inc() { unsafe { COUNTER += 1; } }

장점:

  • 카운터를 쉽게 구현할 수 있습니다.

단점:

  • 데이터 경합, 예측 불가능한 동작.
  • 테스트가 서로 의존할 수 있습니다.

긍정적 케이스

각 테스트는 격리되어 있으며, 로컬 변수와 mock 객체를 사용합니다.

장점:

  • 테스트 간 예상치 못한 상호작용 효과가 없습니다.
  • 테스트를 병렬로 실행하는 것이 안전합니다.

단점:

  • 테스트할 코드를 더욱 철저히 설계해야 합니다.
  • 때로는 상태 초기화에 더 많은 코드를 작성해야 합니다.