xUnit의 탄생 — SUnit에서 JUnit으로, 전설의 기내 페어 프로그래밍
1979년, 마이어스가 테스트를 이론으로 체계화했다(제1화). 하지만 테스트를 일상적으로 자동 실행할 도구는 아직 없었다. 켄트 벡이 Smalltalk용으로 만든 작은 프레임워크 SUnit, 그리고 에릭 감마와 비행기 안에서의 페어 프로그래밍으로 태어났다는 JUnit의 일화. TestCase·assert·fixture라는 어휘가 어떻게 단위 테스트의 세계 표준이 되었는지를 따라간다.
이론은 있었다. 하지만 도구가 없었다
앞 화에서 보았듯, 1970년대 말까지 소프트웨어 테스트는 이론적으로는 체계화되어 있었다. 마이어스는 테스트의 목적과 기법을 한 권으로 정리했고, 데이크스트라는 그 한계를 밝혔으며, 보엠은 검증과 타당성 확인을 구별했다. 하지만 실무에서는 단위 테스트(unit test)를 쓰고, 실행하고, 결과를 확인하기 위한 공통 도구가 존재하지 않았다.
많은 현장에서 테스트는 print문으로 출력을 눈으로 확인하거나, 일회용 스크립트를 써서 그때그때 확인하는, 개인 역량에 의존하는 재현성 낮은 작업이었다. 같은 확인을 반복할 때마다 수고가 들었고, 「테스트를 몇 번이고 자동으로 돌린다」는 발상 자체가 일반적이지 않았다.
이 상황을 완전히 바꾼 것이, 켄트 벡이 Smalltalk용으로 만든 작은 테스트 프레임워크 SUnit과, 그것을 Java로 이식한 JUnit이다. 이 글에서는 이 두 프레임워크가 탄생한 경위와, 거기서 확립된 「xUnit 아키텍처」가 단위 테스트의 방식을 어떻게 바꾸었는지를 살펴본다.
켄트 벡과 SUnit — Smalltalk에서의 단위 테스트 실험
켄트 벡은 1980년대 말, Smalltalk 환경에서 자신의 프로그래밍을 지원하기 위한 작은 테스트 프레임워크를 썼다. 이것이 훗날 SUnit이라 불리게 되는 것이다.
벡은 이 설계를 1994년, 잡지 The Smalltalk Report에 게재된 논문 「Simple Smalltalk Testing: With Patterns」에서 패턴집으로 문서화했다. 이 논문은 단순히 동작하는 코드를 보여주는 데 그치지 않고, 「왜 이런 구조로 하는가」라는 설계 판단을 패턴이라는 형식으로 언어화했다는 점에서, 훗날 xUnit 계열 프레임워크의 설계 사상에 큰 영향을 주었다. 참고로 SUnit의 정확한 탄생 연도에 대해서는, 공개된 논문 자체는 1994년 발표이지만 벡 본인이 훗날 「1994년에 썼다고 생각한다」고 말하는 한편, 초기의 착상은 더 거슬러 올라간다는 기술도 보여, 자료에 따라 다소의 흔들림이 있다.
SUnit의 설계는 놀라울 만큼 단순하다.
- 테스트는
TestCase의 서브클래스로 쓴다 - 하나의 테스트 메서드가 하나의 검증 항목에 대응한다
setUp으로 각 테스트의 전제 조건(fixture)을 준비한다assert:같은 메시지로 기대값과 실제값을 비교한다- 여러 테스트 케이스를
TestSuite로 묶어 일괄 실행한다
중요한 것은, 벡이 이것을 「프레임워크」로 만들었다는 점이다. 즉 테스트의 실행·결과 집계·실패 보고 같은 반복 작업을, 테스트를 쓰는 사람이 아니라 프레임워크 쪽의 책임으로 만들었다. 이 발상은 훗날 벡이 제창하는 익스트림 프로그래밍(Extreme Programming, XP) 및 테스트 주도 개발(Test-Driven Development, TDD)의 실천 기반이 되어 간다. TDD의 자세한 경위는 나중에 제4화에서 다룬다.
전설의 비행기 — JUnit은 이렇게 태어났다
1997년, 켄트 벡은 『디자인 패턴』(Design Patterns, 1994)의 공저자로도 알려진 소프트웨어 공학자 에릭 감마와 함께, 취리히에서 애틀랜타에서 열리는 OOPSLA ’97 콘퍼런스로 향하는 비행기에 함께 탔다.
장시간의 비행 동안, 두 사람은 SUnit의 아이디어를 Java로 이식하는 작업을, 페어 프로그래밍(pair programming)으로, 그것도 테스트 퍼스트(test-first)로 진행했다는 일화가 널리 전해진다. 이 기내 세션에서 쓰인 코드가 JUnit의 첫 버전의 원형이 되었다.
JUnit은 그 후 곧 공개되어, 순식간에 Java 커뮤니티의 표준적인 단위 테스트 도구가 되었다. 벡이 이미 Smalltalk 커뮤니티에서 확립해 두었던 xUnit적 설계 사상과, 감마의 객체 지향 설계에 관한 깊은 지식이 결합되면서, JUnit은 단순한 「SUnit의 이식」을 넘어, 이후 모든 언어의 테스트 프레임워크가 본보기로 삼는 완성도 높은 설계가 되었다.
이 일화가 테스트 역사에서 자주 회자되는 것은, 단순한 미담으로서만은 아니다. 「테스트 프레임워크 자체를 테스트 주도로 만든다」는, 훗날 『Test-Driven Development: By Example』(2002)에서 벡이 체계적으로 제시하게 되는 실천 스타일이, JUnit 탄생의 현장에서 이미 구현되어 있었다는 점에서 상징적이기 때문이다.
xUnit 아키텍처의 구성 요소
SUnit과 JUnit이 확립하고, 이후 거의 모든 단위 테스트 프레임워크가 따르게 되는 기본 구조를 「xUnit 아키텍처」라 부른다. 그 구성 요소는 다음과 같다.
| 구성 요소 | 역할 |
|---|---|
| TestCase | 하나의 검증 항목(테스트 케이스)을 나타내는 클래스 또는 함수. 보통 독립적으로 실행 가능한 최소 단위 |
| TestSuite | 여러 TestCase를 묶어 일괄 실행하기 위한 그릇 |
| TestRunner | TestSuite를 실제로 실행하고, 성공·실패·오류를 수집해 보고하는 프로그램 |
| Assertion(단언) | 「기대값과 실제값이 일치하는가」 등을 검증하고, 불일치하면 테스트를 실패시키는 장치(assertEquals 등) |
| Fixture(픽스처) | 테스트 실행에 필요한 전제 조건·환경(테스트 대상 객체의 초기 상태, 테스트 데이터 등) |
| setUp / tearDown | 각 테스트 실행 전에 fixture를 준비하고(setUp), 실행 후에 뒷정리를 하는(tearDown) 훅 |
이 구조의 핵심은 「테스트 케이스마다 독립된 fixture를 다시 준비한다」는 원칙에 있다. 어떤 테스트가 다른 테스트의 뒷정리 부족 때문에 실패하는 식의 상호 의존을 피함으로써, 테스트의 신뢰성과 재현성이 유지된다. 이 설계 판단은 훗날 플레이키 테스트(flaky test, 제14화 참조)를 막는 기본 원칙으로서도 중요해진다. 테스트의 독립성이라는 원칙은 수수해 보이지만, xUnit 아키텍처가 수십 년에 걸쳐 색바래지 않는 이유 중 하나다.
왜 프레임워크화가 단위 테스트를 일반화·자동화시켰는가
SUnit·JUnit 이전에도, 개발자들은 어떤 형태로든 동작 확인을 하고 있었다. 하지만 그것은 다음과 같은 성질을 가진, 개인 역량에 의존하는 작업이었다.
- 테스트 코드를 쓰는 방식에 공통된 형식이 없어, 팀이나 프로젝트마다 방식이 제각각이었다
- 실행 결과의 집계·보고가 수작업이라, 많은 테스트 케이스를 돌리는 비용이 높았다
- 테스트의 독립성(이전 테스트의 영향을 받지 않는 것)이 보장되지 않아, 신뢰성이 낮았다
- 「테스트를 쓴다」는 행위가 프로그래머의 표준적인 작업으로 인식되지 않았다
xUnit 아키텍처의 등장으로, 이 상황은 다음과 같이 바뀌었다.
- 공통 형식이 생겼다 — TestCase·assert·setUp이라는 어휘를 공유함으로써, 남이 쓴 테스트를 읽고 쓰는 비용이 극적으로 낮아졌다
- 실행과 집계가 자동화되었다 — TestRunner가 모든 테스트를 한꺼번에 실행하고, 초록(전체 성공)인지 빨강(실패 있음)인지를 순식간에 보여주게 되었다
- IDE나 빌드 도구와의 통합이 진행되었다 — JUnit은 IDE(Eclipse 등)나 빌드 도구(Ant, Maven 등)에 깊이 통합되어, 「저장할 때마다 테스트가 돌아간다」는 개발 경험이 일반화되었다
- 테스트를 쓰는 비용이 극적으로 낮아졌다 — 정형화된 구조를 따라 테스트 메서드를 추가하기만 하면 되어, 테스트를 쓰는 것 자체의 심리적·시간적 비용이 낮아졌다
그 결과, 단위 테스트는 일부 의식 높은 개발자만의 실천에서 업계 전체의 당연한 작업으로 바뀌어 갔다. 이는 시리즈 overview에서 언급한 「수동에서 자동화로」라는 실이 처음으로 크게 전진한 순간이다.
간단한 JUnit풍 코드 예시
xUnit 아키텍처의 이미지를 잡기 위해, 단순한 계산기 클래스에 대한 JUnit풍 테스트 코드 예시를 보인다. 현대적인 JUnit 5(Jupiter)의 애너테이션 스타일로 작성했다.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.BeforeEach;
import static org.junit.jupiter.api.Assertions.assertEquals;
class CalculatorTest {
private Calculator calculator;
@BeforeEach
void setUp() {
// 각 테스트 전에 새 인스턴스를 준비한다(fixture)
calculator = new Calculator();
}
@Test
void addsTwoPositiveNumbers() {
int result = calculator.add(2, 3);
assertEquals(5, result); // assertion
}
@Test
void addsNegativeNumbers() {
int result = calculator.add(-2, -3);
assertEquals(-5, result);
}
}
@BeforeEach(옛 setUp)가 fixture 준비를 맡고, 각 @Test 메서드가 독립된 TestCase로 실행되며, assertEquals가 Assertion의 역할을 한다. 이 구조는 1990년대의 SUnit·JUnit에서 거의 변하지 않았으며, xUnit 아키텍처의 수명이 얼마나 긴지를 말해 준다.
JUnit의 그 후의 진화와 절대적인 영향력
JUnit 자체도 그 후 계속 진화했다. 클래식한 TestCase 상속 스타일의 JUnit 3, 2006년에 애너테이션 @Test 기반 스타일을 도입한 JUnit 4, 그리고 2017년에 모듈화된 아키텍처로 개편된 JUnit 5(Jupiter)로 세대를 거듭해 왔다.
JUnit은 그 후 Java 커뮤니티 바깥에도 널리 영향을 미쳤다. 단위 테스트라는 행위가 「일부 열성적인 개발자만의 습관」에서 「업계 표준의 프랙티스」로 바뀌어 가는 과정에서, JUnit은 그 중심적인 촉매로 계속 남았다.
- 「테스트 클래스에 TestCase라는 단위를 만들고, assert로 검증하며, setUp/tearDown으로 fixture를 관리한다」는 설계는, 거의 그대로의 형태로 다른 언어에 이식되어 갔다. 이 이식의 물결은 다음 제3화에서 다룬다
- JUnit의 성공은 「테스트 코드 또한 유지보수해야 할 일급 산출물이다」라는 사고방식을 업계에 정착시켰다
- 「테스트 프레임워크 자체를 테스트 주도로 만든다」는 실천은, 훗날 TDD로 체계화되는 발상을 앞서 보여준 것이기도 했다
마무리 — 어휘가 세계를 바꾸었다
SUnit과 JUnit의 탄생은, 「테스트를 쓰는 것이 특별한 작업이 아니라 일상적인 프로그래밍 작업의 일부가 된다」는 전환을 가져왔다. 이 전환의 핵심은 TestCase·assertion·fixture·setUp/tearDown이라는 공통의 어휘와 아키텍처를 확립했다는 데 있다.
이 설계는 지극히 보편성이 높아서, 다음 글에서 보듯 C++·.NET·Python·Ruby·PHP 등 거의 모든 주요 언어에 같은 발상 그대로 이식되어 간다. 비행기 안에서 쓰인 작은 코드가 30년 가까이 전 세계 개발자의 일상을 계속 만들어 왔다는 것 — 이것 또한 테스트 역사가 지닌 의외성 중 하나다.
다음 제3화에서는, JUnit의 설계 사상이 어떻게 각 언어의 커뮤니티로 퍼져 나가, 각각의 언어 문화에 맞춰 변용되어 갔는지를 살펴본다.