본문 바로가기
스터디-공부/테스트

6장 단위 테스트 스타일 - 스타일 비교

by jonghoonpark 2023. 7. 12.

단위테스트 (블라디미르 코리코프)


아래 내용에서 이어지는 글입니다.

https://jonghoonpark.tistory.com/43

2. 단위 테스트 스타일 비교

좋은 단위 테스트의 4대 요소를 중심으로 각각의 단위 테스트 스타일 비교

 

[Note] 좋은 단위 테스트의 4대 요소 는 다음과 같다.
- 회귀 방지
- 리팩터링 내성
- 빠른 피드백
- 유지 보수성

 

2.1 회귀 방지와 피드백 속도 지표로 스타일 비교하기

회귀 방지 지표는 특정 스타일에 따라 달라지지 않는다. 회귀 방지 지표는 다음 세 가지 특성으로 결정된다.

- 테스트 중에 실행되는 코드의 양
- 코드 복잡도
- 도메인 유의성

 

어떤 스타일도 이 부분에서 도움이 되지는 않는다. 따라서 크게 연관이 없다.

 

다만 통신 기반 스타일의 경우에는 남용하면 작은 코드 조각을 검증하고 다른 것은 모두 목을 사용하는 등 피상적인 테스트가 될 수 있지만. 이는 통신 기반 스타일의 문제라기 보다는 기술을 남용하는 극단적인 사례에 가깝다.

 

테스트 스타일과 테스트 피드백 속도 사이에는 상관관계가 거의 없다.

목은 런타임에 지연 시간이 생기는 편이므로 약간의 지연이 생길 수 있지만 테스트가 수만 개 수준이 아니라면 별로 차이는 없다.

 

2.2 리팩터링 내성 지표로 스타일 비교하기

리팩터링 내성 지표에 관련해서는 상황이 다르다.

 

리팩터링 내성은 리팩터링 중에 발생하는 거짓 양성(허위 경보) 수에 대한 척도다. 결국 거짓 양성은 식별할 수 있는 동작이 아니라 코드의 구현 세부 사항에 결합된 테스트의 결과다.

 

출력 기반 테스트는 테스트가 테스트 대상 메서드에만 결합되므로 거짓 양성 방지가 가장 우수하다. 테스트가 구현 세부 사항에 결합해야 하는 경우는 테스트 대상 메서드가 구현 세부 사항일 때 뿐이다.

 

상태기반 테스트는 일반적으로 거짓 양성이 되기 쉽다. 이러한 테스트는 테스트 대상 메서드 외에도 클래스 상태와 함께 작동한다. 확률적으로 말하면, 테스트와 제품 코드 간의 결합도가 클수록 유출되는 구현 세부 사항에 테스트가 얽매일 가능성이 커진다. 상태 기반 테스트는 큰 API 노출 영역에 의존하므로, 구현 세부 사항과 결합할 가능성도 더 높다.

 

통신 기반 테스트는 허위 경보에 가장 취약하다.
테스트 대역으로 상호 작용을 확인하는 테스트는 대부분 깨지기 쉽다. 이는 항상 스텁과 상호 작용하는 경우다. 이러한 상호 작용을 확인해서는 안 된다.
애플리케이션 경계를 넘는 상호 작용을 확인하고 해당 상호 작용의 사이드 이펙트가 외부 환경에 보이는 경우에만 목이 괜찮다. 보다시피, 리팩터링 내성을 잘 지키려면 통신 기반 테스트를 사용할 때 더 신중해야 한다.

 

그러나 피상적인 테스트가 통신 기반 테스트의 결정적인 특징이 아닌 것처럼, 불안정성도 통신 기반 테스트의 결정적인 특징이 아니다. 캡슐화를 잘 지키고 테스트를 식별할 수 있는 동작에만 결합하면 거짓 양성을 최소로 줄일 수 있다.

 

2.3 유지 보수성 지표로 스타일 비교하기

마지막으로 유지 보수성 지표는 단위 테스트 스타일과 밀접한 관련이 있다.

 

그러나 리팩터링 내성과 달리 완화할 수 있는 방법이 많지 않다. 유지 보수성은 단위 테스트의 유지비를 측정하며, 다음 두 가지 특성으로 정의 한다.

- 테스트를 이해하기 얼마나 어려운가(테스트 크기에 대한 함수)?
- 테스트를 실행하기 얼마나 어려운가(테스트에 직접적으로 관련 있는 프로세스 외부 의존성 개수에 대한 함수)?

 

테스트가 크면, 파악도 변경도 어려우므로 유지 보수가 어렵다.

 

하나 이상의 외부 의존성과 직접 작동하는 테스트는 데이터베이스 서버 재부팅, 네트워크 연결 문제 해결등과 같이 운영하는 데 시간이 필요하므로 유지 보수가 어렵다.

 

출력 기반 테스트의 유지 보수성

다른 두 가지 스타일과 비교하면, 출력 기반 테스트가 가장 유지 보수하기 용이하다. 출력 기반 테스트는 거의 항상 짧고 간결하므로 유지 보수가 쉽다. 이러한 이점은 메서드로 입력을 공급하는 것과 해당 출력을 검증하는 두 가지로 요약할 수 있다는 사실에서 비롯한다. 단 몇 줄로 이 두 가지를 수행할 수 있다.

 

출력 기반 테스트의 기반 코드는 전역 상태나 내부 상태를 변경할 리 없으므로, 프로세스 외부 의존성을 다루지 않는다. 따라서 두 가지 유지 보수성 모두의 측면에서 출력 기반 테스트가 가장 좋다.

 

상태 기반 테스트의 유지 보수성

상태 기반 테스트는 일반적으로 출력 기반 테스트보다 유지 보수가 쉽지 않다. 상태 검증은 종종 출력 검증보다 더 많은 공간을 차지하기 때문이다.

 

다음은 상태 기반 테스트의 예제다.

[Fact]
public void Adding_a_comment_to_an_article()
{
    var sut = new Article();
    var text = "Comment text";
    var author = "John Doe";
    var now = new DateTime(2019, 4, 1);

    sut.AddComment(text, author, now);

    Assert.Equal(1, sut.Comments.Count);
    Assert.Equal(text, sut.Comments[0].Text);
    Assert.Equal(author, sut.Comments[0].author);
    Assert.Equal(now, sut.Comments[0].DateCreated);
}

이 테스트는 단순하고 댓글이 하나만 있지만, 검증부는 네 줄에 걸쳐 있다. 상태 기반 테스트는 종종 더 많은 데이터를 확인해야 하므로 크기가 대폭 커질 수 있다.

 

대부분 코드를 숨기고 테스트를 단축하는 헬퍼 메서드로 문제를 완화할 수 있지만 이러한 메서드를 작성하고 유지하는 데 상당한 노력이 필요하다. 여러 테스트에 이 메서드를 재사용할 때만 이러한 노력에 명분이 생기지만, 그런 경우는 드물다.

 

다음은 헬퍼 메서드 사용 예제다.

[Fact]
public void Adding_a_comment_to_an_article()
{
    var sut = new Article();
    var text = "Comment text";
    var author = "John Doe";
    var now = new DateTime(2019, 4, 1);

    sut.AddComment(text, author, now);

    sut.ShouldContainNumberOfComments(1)
        .With(Comment(text, author, now));
}

상태 기반 테스트를 단축하는 또 다른 방법으로 검증 대상 클래스의 동등 멤버를 정의할 수 있다.

 

다음은 값으로 비교하는 Comment 예제다.

[Fact]
public void Adding_a_comment_to_an_article()
{
    var sut = new Article();
    var comment = new Comment(
        "Comment text",
        "John Doe",
        new DateTime(2019, 4, 1));

    sut.AddComment(comment.Text, comment.Author, comment.DateCreated);

    sut.Comments[0].Should().BeEquivalentTo(comment);
}

* 책에서는 sut.Comments[0] 가 아닌 sut.Comments 으로 되어있는데 array 에 add 한 것이므로 [0]이 맞을 것 같아서 수정하였다. 첫번째 예제 코드에서도 [0]을 이용하여 Comment 객체를 지정하였다.

 

다만 이 방법은 본질적으로 클래스가 값에 해당하고 값 객체로 변환할 수 있을 때만 효과적이다.

 

이러한 방법을 적용할 수 있더라도 상태 기반 테스트는 출력 기반 테스트보다 공간을 더 많이 차지하므로 유지 보수성이 떨어진다.

 

통신 기반 테스트의 유지 보수성

통신 기반 테스트는 다른 두 가지 테스트에 비해 점수가 낮다. 통신 기반 테스트에는 테스트 대역과 상호 작용 검증을 설정해야 하며 이는 공간을 많이 차지한다. 목이 사실 형태로 있을 때 (mock chain, 목이 다른 목을 반환하고, 그 다른 목은 또 다른 목을 반환하는 식으로 여러 계층이 있는 목이나 스텁) 테스트는 더 커지고 유지 보수하기가 더 어려워진다.

 

2.4 스타일 비교하기: 결론

* 세 가지 스타일 모두 회귀 방지와 피드백 속도 지표에서는 점수가 같다고 가정한다.

  출력 기반 상태 기반 통신 기반
리팩터링 내성을 지키기 위해 필요한 노력 낮음 중간 중간
유지비 낮음 중간 높음

출력 기반 테스트가 가장 결과가 좋다. 이 스타일은 구현 세부 사항과 거의 결합되지 않으므로 리팩터링 내성을 적절히 유지하고 주의를 많이 기울일 필요가 없다. 이러한 테스트는 간결하고 프로세스 외부 의존성이 없기 때문에 유지 보수도 쉽다.

 

상태 기반 테스트와 통신 기반 테스트는 두 지표 모두 좋지 않다. 유출된 구현 세부 사항에 결합할 가능성이 높고, 크기도 커서 유지비가 많이 든다.

 

그러므로 항상 다른 것보다 출력 기반 테스트를 선호하라.

 

하지만 안타깝게도 말하기는 쉬워도 행하기는 어렵다.

 

출력 기반 스타일은 함수형을 작성된 코드에만 적용할 수 있고, 대부분의 객체지향 프로그래밍 언어에는 해당하지 않는다.

 

다음 글에서는 어떻게 출력 기반 테스트를 만들 수 있을지에 대해 알아볼 것이다.
코드를 순수 함수로 만들면 상태 기반 테스트나 통신 기반 테스트 대신 출력 기반 테스트가 가능해진다.

 

fin.

댓글