언어별 생태계의 현재 — pytest·RSpec·JUnit·Go의 testing

테스트 프레임워크 대부분은 Kent Beck의 SUnit에 뿌리를 두지만, 각 언어 커뮤니티는 저마다 다른 문화를 키워왔다. 파이썬의 pytest, 루비의 RSpec, 자바/코틀린의 JUnit 5, Go의 표준 testing 패키지, 러스트의 내장 테스트, C#의 삼파전, 스위프트의 세대교체까지 — 「표준 라이브러리 내장」 대 「서드파티가 사실상의 표준」이라는 설계 사상의 분기를 횡단 비교한다.

testinghistorylanguage-ecosystemspytestjunitgorust

테스트 프레임워크 대부분은 Kent Beck의 SUnit(Smalltalk, 1998년)에서 비롯된 xUnit 계보를 조상으로 삼는다. 하지만 각 언어 커뮤니티는 그로부터 저마다 다른 문화를 키워왔다. 표준 라이브러리에 테스트 기구를 내장한 언어(Go, Rust)도 있고, 서드파티 도구가 사실상의 표준으로 군림하는 언어(Java, JavaScript, Ruby)도 있다. 이 글에서는 주요 언어의 테스트 생태계를 횡단적으로 비교하고, 현재 널리 채택되는 구성을 정리한다.

비교표 — 주요 언어의 테스트 생태계

언어 사실상의 표준 어서션/모킹 특징적인 사상
Python pytest(표준은 unittest) 내장 assert 문을 재작성, unittest.mock fixture·parametrize를 통한 선언적 유연성
Ruby RSpec(표준에 가까운 것은 minitest) RSpec matcher / minitest assertion DSL의 가독성 대 순수 루비의 단순함
Java/Kotlin JUnit 5(Jupiter) AssertJ, Mockito, Spock 애너테이션+확장 모델, JVM 언어군이 공유
Go 표준 testing 패키지 testify(선택) 「프레임워크 불필요」라는 작은 핵심의 사상
Rust 내장 #[test] / cargo test proptest, quickcheck(선택) 언어 기능으로서 테스트를 내장
C#/.NET xUnit.net / NUnit / MSTest Moq, FluentAssertions 삼파전 공존, Microsoft 공식은 중립
PHP PHPUnit PHPUnit 내장, Pest(신흥) JUnit 직계의 독주 상태
Swift XCTest → Swift Testing XCTAssert 계열 → #expect/#require 매크로 기반으로의 세대교체 진행 중
JS/TS Vitest / Jest 내장 어서션, Testing Library 「올인원」 문화(이전 기사 참조)

Python — pytest가 주류, 표준은 unittest

파이썬 표준 라이브러리에는 unittest 모듈이 있다. 이는 Steve Purcell이 JUnit(≒ Smalltalk의 SUnit)의 설계를 파이썬으로 이식한 것으로, 2001년 출시된 Python 2.1에 표준으로 탑재되었다(당초 이름은 PyUnit). 클래스 기반으로 setUp/tearDown, assertEqual 등의 메서드를 갖는 고전적인 xUnit 스타일이다.

하지만 현재 사실상의 표준은 pytest다. Holger Krekel이 개발한 “py” 라이브러리의 일부로 2000년대 중반에 시작해, 훗날 독립된 프로젝트가 되었다. pytest의 특징은 다음과 같다.

  • 클래스 상속이 불필요하며, 순수한 함수와 순수한 assert 문으로 테스트를 작성할 수 있다(어서션 자동 재작성을 통해 실패 시 상세한 차이를 표시)
  • fixturesetUp/tearDown을 의존성 주입에 가까운 방식(데코레이터로 선언하고, 테스트 함수의 인자로 받는다)으로 대체한 유연한 전처리·후처리 기구
  • @pytest.mark.parametrize — 같은 테스트 로직을 여러 입력 패턴으로 반복 실행하는 구조
  • 거대한 플러그인 생태계(pytest-django, pytest-mock, pytest-asyncio, pytest-cov 등)

pytest는 unittest.TestCase로 작성된 기존 테스트도 그대로 실행할 수 있어, 「표준은 unittest지만 실무는 pytest」라는 이층 구조가 정착되어 있다. 한때 존재했던 nose/nose2는 개발이 정체되어 지금은 쓰이지 않는다. 여러 파이썬 버전이나 패키지 구성 사이에서 테스트 환경을 전환하고 싶을 때는 toxnox가 함께 쓰이는 경우도 많다.

Ruby — RSpec 대 minitest

루비 표준 라이브러리에는 Ryan Davis가 개발한 minitest가 동봉되어 있다. 가볍고 빠르며, xUnit 스타일과 스펙(describe/it) 스타일을 모두 지원한다.

한편 Rails 커뮤니티에서 압도적인 지지를 받는 것은 RSpec이다. Steven Baker가 2005년에 착안해 공개했고, 2007년에 안정 버전 1.0이 출시되었다. describe / context / it이라는 자연어에 가까운 DSL, 풍부한 matcher(expect(x).to eq(y) 등), 모킹 기구를 표준으로 갖추고 있으며, Capybara(수용·시스템 테스트)나 FactoryBot(테스트 데이터 생성)과 조합해 쓰이는 경우가 많다.

두 진영의 대립은 「DSL을 통한 표현력·가독성」 대 「순수 루비 코드의 단순함·디버깅 용이성」이라는 설계 사상의 차이를 상징한다. 루비 온 레일즈의 작성자 David Heinemeier Hansson(DHH)은 순수 루비에 가까운 minitest 계열의 스타일을 선호하는 것으로 알려져 있다. 수용 테스트·E2E에 가까운 영역에서는 Cucumber(Aslak Hellesøy가 개발, Gherkin 표기법으로 Given-When-Then을 작성)가 RSpec과 조합되어 쓰이는 경우도 많다.

Java/Kotlin — JUnit 5와 그 주변

JUnit 5(개발 코드네임 Jupiter)는 2017년 9월에 정식 출시되었다. JUnit Platform(테스트 실행 기반), JUnit Jupiter(새로운 프로그래밍/확장 모델), JUnit Vintage(JUnit 3/4 호환 계층)의 3계층 구조로 개편되었고, @Test·@ParameterizedTest·@Nested 등의 애너테이션과, JUnit 4의 Rule/Runner를 대체하는 유연한 확장 모델(Extension Model) 을 도입했다.

주변 라이브러리도 풍부하다.

라이브러리 역할 작성자/유래
TestNG JUnit의 경쟁자, 병렬 실행·의존 순서 지정에 강함 Cédric Beust(2004년)
AssertJ 유창한 어서션(assertThat(x).isEqualTo(y)) Joel Costigliola(FEST-Assert에서 파생)
Mockito 사실상의 표준 모킹 라이브러리(when/verify) Szczepan Faber(2007~08년 무렵)
Spock Groovy 제작, given-when-then 구조와 데이터 주도 테스트 표 Peter Niederwieser(2008년)

Kotlin에서는 JUnit 5를 그대로 쓰는 경우도 많지만, Kotlin 네이티브 DSL을 가진 Kotest(구 KotlinTest)도 일정한 지지를 얻고 있다. JVM 언어군은 「표준 라이브러리에 테스트 기구가 없고, 서드파티 제품이 완전히 사실상의 표준화되어 있다」는 점에서 Go나 Rust와 대조적이다.

Go — 표준 라이브러리주의와 테이블 주도 테스트

Go는 2009년 공개 당초부터 표준 라이브러리에 testing 패키지를 내장하고 있다. Go의 설계 사상은 「어서션 라이브러리도 모킹 라이브러리도 필요 없다」는 명확한 최소주의로, if got != want { t.Errorf(...) }라는 소박한 비교를 작성하는 것이 표준적인 작법으로 여겨진다.

이 사상의 상징이 테이블 주도 테스트(table-driven test) 라는 관용구다. 테스트 케이스를 구조체 슬라이스로 나열하고, 이를 하나의 루프로 돌린다.

func TestAdd(t *testing.T) {
    cases := []struct{ a, b, want int }{
        {1, 1, 2},
        {2, 3, 5},
        {-1, 1, 0},
    }
    for _, c := range cases {
        if got := Add(c.a, c.b); got != c.want {
            t.Errorf("Add(%d, %d) = %d, want %d", c.a, c.b, got, c.want)
        }
    }
}

더 표현력 높은 어서션을 원하는 개발자를 위해, Mat Ryer 등이 개발한 testify(assert/require/mock 패키지)가 사실상의 보조 라이브러리로 널리 쓰인다. Go 팀 자체의 공식 입장은 지금도 최소주의에 가깝지만, 실무에서는 testify를 함께 쓰는 것이 일반적이다.

Rust — 내장 테스트와 proptest

Rust는 언어 기능으로서 #[test] 속성과 cargo test 명령을 내장하고 있어, 외부 프레임워크 없이 유닛 테스트를 작성할 수 있다. 관례상 테스트 대상 코드와 같은 파일 안에 #[cfg(test)] mod tests { ... }라는 테스트 전용 모듈을 두고, tests/ 디렉터리에는 결합 테스트(크레이트의 공개 API만을 외부에서 두드리는 테스트)를 배치한다.

프로퍼티 기반 테스트를 하고 싶을 때는 proptestquickcheck 같은 크레이트를 추가한다. proptest는 파이썬의 Hypothesis와 비슷한 축소(shrinking) 전략을 가지고 있어, 실패를 재현하는 최소한의 반례를 자동으로 탐색한다. 표준 테스트 러너보다 빠른 대안으로 cargo nextest도 널리 쓰인다.

C#/.NET — xUnit.net·NUnit·MSTest의 삼파전

.NET의 테스트 프레임워크는 역사적으로 3개 계통이 병존해 왔다.

  • NUnit — 가장 오래되었으며, JUnit(≒ SUnit)의 설계를 .NET에 이식하는 형태로 태어났다
  • xUnit.net — 2007년, NUnit의 원작자인 James Newkirk와 Brad Wilson(당시 둘 다 Microsoft 소속)이 NUnit의 설계에 쌓인 복잡함을 재검토하는 형태로 개발한 후계 프레임워크다. [SetUp]/[TearDown] 애너테이션 대신 생성자와 IDisposable을 쓰는 설계 등, 곳곳에 NUnit에 대한 반성이 반영되어 있다
  • MSTest — Microsoft 순정, Visual Studio에 표준으로 동봉된다

현재는 셋 다 널리 쓰이고 있으며, 뚜렷한 「일강」은 없다. 모킹 라이브러리로는 Daniel Cazzulino가 개발한 Moq(LINQ 식 구문으로 기댓값을 작성할 수 있다)가 주류 중 하나이고, FluentAssertions가 유창한 어서션의 정석으로 함께 쓰인다.

PHP — PHPUnit의 독주

PHPUnit은 Sebastian Bergmann이 개발했으며, 2002년에 첫 버전(0.1)이 공개되었다. JUnit의 설계를 PHP로 이식하는 형태로 태어나, 이래로 PHP에서 사실상 유일한 주요 테스트 프레임워크로 군림해 왔다. Laravel·Symfony 등 주요 프레임워크의 테스트 기반으로도 채택되어 있다. 최근에는 PHPUnit 위에 읽기 쉬운 구문을 씌운 Pest가 신흥 세력으로 지지를 넓히고 있다.

Swift — XCTest에서 Swift Testing으로

애플 공식 테스트 프레임워크는 오랫동안 XCTest(OCUnit/SenTestingKit 계보, Xcode에 통합)였다. XCTAssertEqual을 비롯해 40종류 이상 있는 XCTAssert* 계열 매크로로 검증하는 스타일이다.

2024년 WWDC에서 애플은 새로운 프레임워크 Swift Testing을 발표했다. Swift의 매크로 기능을 활용해 XCTAssert*의 난립을 #expect / #require라는 2개의 매크로로 정리했고, 기본적으로 병렬 실행되는 설계를 갖추고 있다. XCTest와 같은 타깃 안에서 공존할 수 있도록 설계되어 있어, 기존 프로젝트로부터의 단계적 이행을 전제로 한다. XCTest는 UI 테스트나 성능 테스트 영역에서 계속 쓰이고 있다.

설계 사상의 대립축 — 표준 탑재 대 서드파티 주도

각 언어의 테스트 문화는 크게 두 축으로 정리할 수 있다.

  1. 표준 라이브러리에 내장하는가(Go, Rust) — 언어 사양 수준에서 「작은 핵심 + 관례」를 제공해, 프레임워크 선택의 고민 자체를 없앤다. 다만 표현력은 제한적이어서, 보조 라이브러리(testify, proptest)가 실무상 거의 필수가 되는 역설도 있다
  2. 서드파티가 사실상의 표준이 되는가(Java, Ruby, JS, PHP) — 언어 커뮤니티가 단일한 지배적 도구로 수렴함으로써 「표준은 없지만 사실상 있다」는 상태를 만든다. 이 수렴에는 시간이 걸리며, 그 과정에서 여러 유력 후보가 병존하는 시기(.NET의 삼파전, 루비의 RSpec/minitest)가 생겨난다

파이썬은 다소 특수한데, 「표준(unittest)」과 「사실상의 표준(pytest)」이 병존하면서도, pytest가 unittest 형식의 테스트도 실행할 수 있기 때문에 긴장 없이 공존하고 있다.

정리

언어마다 다른 테스트 문화의 차이는, 그 언어 커뮤니티 자체의 설계 철학을 비추는 거울이기도 하다. Go·Rust의 최소주의, Java·JVM 계열의 애너테이션 주도, 루비의 DSL 표현력 지향, 자바스크립트의 올인원화——그 밑바탕에는 모두 Kent Beck의 SUnit/xUnit이라는 공통 조상이 있다.

이 기사에서 다음 기사로

언어마다 서로 다른 도구를 살펴보았다. 하지만 「얼마나 많은 종류의 테스트를, 어떤 비율로 써야 하는가」라는 물음에는 또 다른 계보의 논쟁이 있다. 다음 기사에서는 테스트 피라미드·아이스크림콘·테스팅 트로피·허니콤이라는, 테스트의 「형태」를 둘러싼 논쟁을 따라간다.

← Back to 소프트웨어 테스트의 계보