단위 테스트(Unit Testing)에서 단위의 경계는 무엇인가? (feat. 고전파와 런던파)

5 minute read

단위 테스트(Unit Testing)에서 단위(Unit)의 경계는 무엇일까? OOP(Object-Oriented Programming) 언어로 테스트를 짜고 있다면 클래스(Class)를 단위로 생각하면 되는 걸까? FP(Functional Programming) 언어로 테스트를 짜고 있다면 함수를 단위로 생각하면 되는 걸까? 아니면 함수의 그룹을 정의할 수 있는 모듈(Module)을 단위로 생각해야 하는 걸까?

모호하다. 단위에 대한 설명을 찾아보기 힘들다. 그래서 ’단위 테스트 (블라디미르 코리코프, 2021)’ 책 2장 내용이 얼마나 반가웠는지 모른다. ’고전파’와 ’런던파’가 낯설지만 단위 테스트의 단위에 대한 정의를 가장 정확하게 하는 책이다. 감명받아서 정리해 봤다.

단위 테스트의 정의

단위 테스트는 뭔가? 이것부터 알아보자.

단위 테스트는

  1. 작은 코드 조각(단위라고도 함)을 검증
  2. 빠르게 수행
  3. 격리된 방식으로 처리하는 자동화 테스트

단위 테스트 (블라디미르 코리코프, 2021) p.52

개량할 수 있는 정의가 하나도 없다니 감동이다. 이러니 단위 테스트에서 단위에 대한 정의가 각기 다르다. 1번과 2번은 저런 느낌이다로만 보면 된다. 3번의 ’격리된 방식’에서 어떻게 격리해서 테스트할까? 여기서 격리하는 경계가 바로 ’단위(Unit)’를 어떻게 정의하는지를 결정한다.

여기서 이 책에서 처음 들어보는 ’고전파’와 ’런던파’가 등장한다. 격리에 대한 경계가 다르다.

고전파와 런던파

고전파는 대충 감이 온다. 원론적인 접근 뭐 이런 거겠지? 런던파는 뭔가?

고전파는 모든 사람이 단위 테스트와 테스트 주도 개발에 원론적으로 접근하는 방식이기 때문에 ’고전’이라고 한다. 런던파는 런던의 프로그래밍 커뮤니티에서 시작됐다.

단위 테스트 (블라디미르 코리코프, 2021) p.51

런던 프로그래밍 커뮤니티에서 시작돼서 런던파라고 부른다. 커뮤니티 이름이 멋지면 그걸 썼을 텐데, 안 쓴 걸 보면 이름이 별로였나보다. 런던 스타일은 어떤 걸까?

런던 스타일은 때때로 ’목 추종자(mockist)’로 표현된다. 목 추종자라는 용어가 널리 퍼져 있지만, 런던 스타일을 따르는 사람들은 보통 그렇게 부르는 것을 좋아하지 않으므로 이 책에서는 런던 스타일이라고 소개한다. 이 방식의 가장 유명한 지지자는 스티브 프리먼(Steve Freeman)과 냇 프라이스(Nat Pryce)다. 이 주제에 대한 좋은 자료로 이들이 저술한 ’Growing Object-Oriented Software, Guided by Test’를 추천한다.

단위 테스트 (블라디미르 코리코프, 2021) p.52

그렇게 부르는 거 싫어한다지만 ’목 추종자(mockist)’란 단어를 들으니 바로 감이 온다. ’런던파’와 ’고전파’가 단위 테스트를 정의할 때, 가장 다른 견해를 보이는 건 어떤 것일까?

런던파와 고전파로 나눠진 원인은 격리 특성에 있다. 런던파는 SUT(System Under Test, 테스트 대상 시스템)에서 협력자(Collaborator)를 격리하는 것으로 보는 반면, 고전파는 단위 테스트끼리 격리하는 것으로 본다. p64

’런던파’는 단일 클래스를 단위의 크기로 정의해 격리한다. 테스트 대상 클래스와 상호작용하는 클래스는 격리 대상이 아니므로 모두 Mock으로 대체한다. 반면 ’고전파’는 단위 테스트끼리만 격리되면 된다. 단위 테스트 실행 순서에 따라 결과가 달라지거나 동시에 실행된다고 실패하면 안 된다. 테스트 간 공유하는 의존성이 있으면 이때 Mock을 사용한다.

표를 나타내면 다음과 같다.

  격리 주체 단위의 크기 테스트 대역 사용 대상
런던파 단위 단일 클래스 불변 의존성 외 모든 의존성
고전파 단위 테스트 단일 클래스 또는 클래스 세트 공유 의존성(shared dependency)

고전파는 단위의 크기가 클래스 세트가 될 수도 있다. 게임 아이템을 넣는 인벤토리를 예로 들어보자. 여기서 슬롯은 아이템을 넣을 수 있는 스토리지다. 인벤토리는 40개의 슬롯이 있다. 이런 식으로 정의할 수 있다. ’런던파’는 인벤토리 클래스가 테스트 대상이라면 슬롯 클래스를 Mock으로 대체해서 테스트한다. 반면 ’고전파’는 인벤토리 클래스를 테스트하면서 슬롯 동작도 같이 테스트하도록 테스트 코드를 작성한다.

고전파와 런던파 스타일 테스트 코드 비교

단위 테스트 (블라디미르 코리코프, 2021)’ 책 2.1.1 챕터에 ’고전파’와 ’런던파’ 스타일로 작성된 테스트 코드가 있어서 스타일을 비교하기 좋다.

고전파 스타일로 작성한 테스트

[Fact]
public void Purchase_succeeds_when_enough_inventory()
{
    // 준비
    var store = new Store();
    store.AddInventory(Product.Shampoo, 10);
    var customer = new Customer();

    // 실행
    bool success = customer.Purchase(store, Product.Shampoo, 5);

    // 검증
    Assert.True(success);
    Assert.Equal(5, store.GetInventory(Product.Shampoo)); // 상점 제품 다섯 개 감소 검증
}

[Fact]
public void Purchase_fails_when_not_enough_inventory()
{
    // 준비
    var store = new Store();
    store.AddInventory(Product.Shampoo, 10);
    var customer = new Customer();

    // 실행
    bool success = customer.Purchase(store, Product.Shampoo, 15);

    // 검증
    Assert.False(success);
    Assert.Equal(10, store.GetInventory(Product.Shampoo));
}

고객(Customer)이 테스트 대상을 나타내는 SUT(System Under Test), 상점(Store)이 협력자(Collaborator)이다. 협력자인 Store를 Mock으로 대체하지 않고 프로덕션 코드를 그대로 사용한다.

런던파 스타일로 작성한 테스트

[Fact]
public void Purchase_succeeds_when_enough_inventory()
{
    // 준비
    var storeMock = new Mock<IStore>();
    storeMock
        .Setup(x => x.HasEnoughInventory(Product.Shampoo, 5))
        .Returns(true);
    var customer = new Customer();

    // 실행
    bool success = customer.Purchase(
        storeMock.Object, Product.Shampoo, 5);

    // 검증
    Assert.True(success);
    storeMock.Verify(
        x => x.RemoveInventory(Product.Shampoo, 5),
        Times.Once);
}

[Fact]
public void Purchase_fails_when_not_enough_inventory()
{
    // 준비
    var storeMock = new Mock<IStore>();
    storeMock
        .Setup(x => x.HasEnoughInventory(Product.Shampoo, 5))
        .Returns(false);
    var customer = new Customer();

    // 실행
    bool success = customer.Pruchase(
        storeMock.Object, Product.Shampoo, 5);

    // 검증
    Assert.False(success);
    storeMock.Verify(
        x => x.RemoveInventory(Product.Shampoo, 5),
        Times.Never);
}

협력자인 Store 클래스를 Mock으로 대체했다. ’목 추종자(mockist)’라고 불리는 이유다. 아참! 이 단어 싫어한다고 했지.

나는 고전파 스타일로 테스트를 작성하는 걸 선호한다

어떤 스타일로 테스트를 짜는 건 개인의 선택이다. 더 테스트를 잘 짤 수 있는 방식으로 짜면 된다. 사실 스타일을 논하는 건 행복한 고민이 아닐까? 테스트를 짜는 허들을 넘는 게 힘들다.

테스트는 코드의 단위를 검증해서는 안 된다. 오히려 동작의 단위, 즉 문제 영역에 의미가 있는 것, 이상적으로는 비즈니스 담당자가 유용하다고 인식할 수 있는 것을 검증해야 한다. 동작 단위를 구현하는 데 클래스가 얼마나 필요한지는 상관없다. 단위는 여러 클래스에 걸쳐 있거나 한 클래스에만 있을 수 있고, 심지어 아주 작은 메서드가 될 수 있다.

단위 테스트 (블라디미르 코리코프, 2021) p.70

동작 단위를 테스트 하는 걸 추천한다. 즉 단위가 클래스가 아니라 클래스 세트가 된다. 테스트는 스토리를 짜는 거라는 ’It is not about writing tests, its about writing stories’ 글과도 맥이 일치한다.

난 처음에는 ’런던파’ 스타일의 테스트 코드를 짰다. 이렇게 테스트를 짜라는 것 같아서 짜긴 하는데, 수많은 Mock을 보며 이런 테스트가 의미가 있는지 의문이 들었다. Mock으로 대체한 협력자 동작이 변경되면 Mock을 사용한 테스트 코드를 모두 변경해야 한다. 만약 변경하지 않으면 Mock이 제대로 동작을 시뮬레이션하지 못하므로 False negative 결과가 나온다. 즉, 실제 프로덕션 코드는 다르게 동작하는데, Mock 때문에 실패해야 할 테스트가 성공하게 된다.

’런던파’ 스타일 테스트 코드에서 Mock으로 협력자를 대체하면 협력자 코드 변경이 다른 테스트 코드에 주는 영향을 최소화할 수 있다. 즉 테스트가 우르르 깨지는 현상이 덜할 수 있다. 하지만 위에서 얘기한 것처럼 변경 사항이 있을 때, Mock으로 대체한 코드의 동작이 일치하는지 확인해야 한다. 테스트가 깨지면 그걸 고치면 괜찮은데, 만약 통과하게 된다면 Mock 때문에 프로덕션 코드와 다른 동작을 테스트 코드에서 테스트하는 꼴이 된다.

정리하면 내가 고전파 스타일의 테스트를 선호하는 이유는 다음과 같다.

  1. 스토리를 테스트하는 테스트 코드가 의미 있다.
  2. 클래스 구현을 변경했을 때, Mock 코드를 변경하지 않아서 프로덕션 코드와 간격이 발생할 수 있다.
  3. Mock을 치렁치렁 달다 보면 이게 과연 의미 있는 테스트인가 효용성에 의문을 가지게 된다.

마치며

단위 테스트 (블라디미르 코리코프, 2021)’ 책에서 단위 경계를 설명하는 글이 좋아서 정리했다. ’고전파’ 스타일은 동작의 단위를 테스트한다. 반면 ’런던파’ 스타일은 클래스 단위를 테스트한다. SUT(System Under Test)가 아닌 협력자는 Mock으로 대체한다.

책을 쓴 사람처럼 나도 ’고전파’ 스타일의 테스트를 선호한다. 스토리를 테스트할 때, 가장 테스트 효용성이 높았다.

링크