좋은 단위 테스트의 4대 요소

김민규·2023년 7월 18일
0

단위 테스트

목록 보기
4/4
post-thumbnail
  • 회귀 방지
  • 리팩터링 내성
  • 빠른 피드백
  • 유지 보수성

회귀 방지

  • 회귀는 소프트웨어 버그다.
  • 코드를 수정한 후 기능이 의도한 대로 작동하지 않는 경우다.
  • 코드베이스가 커질수록 잠재적인 버그에 더 많이 노출된다.
    • 그래서 회귀에 대해 효과적인 보호를 개발하는 것이 중요하다.

회귀 방지 지표에 대한 고려 사항

  • 테스트 중에 실행되는 코드의 양
  • 코드 복잡도
  • 코드의 도메인 유의성
  • 라이브러리, 프레임워크, 외부 시스템을 테스트 범주에 포함시켜서 의존성에 대해 검증이 올바른지 확인해야 한다.
  • 단순한 코드를 테스트하는 것은 가치가 거의 없다.
    public class User {
       public string Name { get; set; }
    }

회귀 방지 지표를 극대화하려면 테스트가 가능한 한 많은 코드를 실행하는 것을 목표로 해야 한다.

리팩터링 내성

리팩터링은 식별할 수 있는 동작을 수정하지 않고 기존 코드를 변경하는 것을 의미한다.
의도는 코드의 비기능적 특징을 개선하는 것으로 가독성을 높이고 복잡도를 낮추는 것이다.

  • 메서드 이름 변경
  • 코드 조각을 새로운 클래스로 추출
  • 거짓 양성(false positive)은 허위 경보다. 즉, 테스트가 실패했다고 나타내지만 그 기능은 의도한대로 작동한다.
    • 거짓 양성을 테스트 스위트에 치명적인 영향을 줄 수 있다.
    • 실제로 기능이 의도한 대로 작동하지만 테스트는 실패를 나타내는 결과다.
  • 거짓 양성은 테스트 대상 시스템의 내부 구현 세부 사항과 테스트 간의 강결합의 결과다. 결합도를 낮추려면 테스트는 SUT가 수행한 단계 아니라 SUT가 만든 최종 결과(관련된 절차가 아니라 식별할 수 있는 동작)를 검증해야 한다.
public class Message
{
    public string Header { get; set; }
    public string Body { get; set; }
    public string Footer { get; set; }
}

public interface IRenderer
{
    string Render(Message message);
}

public class MessageRenderer : IRenderer
{
    public IReadOnlyList<IRenderer> SubRenderers { get; }

    public MessageRenderer()
    {
        SubRenderers = new List<IRenderer>
        {
            new HeaderRenderer(),
            new BodyRenderer(),
            new FooterRenderer()
        };
    }

    public string Render(Message message)
    {
        return SubRenderers
            .Select(x => x.Render(message))
            .Aggregate("", (str1, str2) => str1 + str2);
    }
}

public class FooterRenderer : IRenderer
{
    public string Render(Message message)
    {
        return $"<i>{message.Footer}</i>";
    }
}

public class BodyRenderer : IRenderer
{
    public string Render(Message message)
    {
        return $"<b>{message.Body}</b>";
    }
}

public class HeaderRenderer : IRenderer
{
    public string Render(Message message)
    {
        return $"<h1>{message.Header}</h1>";
    }
}
  • MessageRenderer 클래스에 여러 하위 렌더링 클래스가 있고, 메시지의 일부에 대한 실제 작업을 위임한다.
  • 결과를 HTML 문서로 결합한다.
  • 하위 렌더링 클래스는 원본 문자열을 HTML 태그로 조정한다.
public void MessageRenderer_uses_correct_sub_renderers()
{
    var sut = new MessageRenderer();

    IReadOnlyList<IRenderer> renderers = sut.SubRenderers;

    Assert.Equal(3, renderers.Count);
    Assert.IsAssignableFrom<HeaderRenderer>(renderers[0]);
    Assert.IsAssignableFrom<BodyRenderer>(renderers[1]);
    Assert.IsAssignableFrom<FooterRenderer>(renderers[2]);
}
  • 이 테스트는 하위 렌더링 클래스가 예상하는 모든 유형이고 올바른 순서로 나타나는지 여부를 확인한다.
  • SUT가 생성한 결과가 아니라 SUT의 구현 세부 사항과 결합했다.
public void Rendering_a_message()
{
    var sut = new MessageRenderer();
    var message = new Message
    {
        Header = "h",
        Body = "b",
        Footer = "f"
    };

    string html = sut.Render(message);

    Assert.Equal("<h1>h</h1><b>b</b><i>f</i>", html);
}
  • 이 테스트는 MessageRenderer를 블랙박스로 취급하고 식별할 수 있는 동작에만 신경 쓴다.
    • 리팩터링 내성이 늘었다.
    • HTML 출력을 똑같이 지키는 한, SUT의 변경 사항은 테스트에 영향을 미치지 않는다.

빠른 피드백

  • 빠른 피드백은 단위 테스트의 필수 속성이다.
  • 테스트 속도가 빠를수록 테스트 스위트에서 더 많은 테스트를 수행할 수 있고 더 자주 실행할 수 있다.
  • 테스트가 빠르게 실행되면 코드에 결함이 생기자마자 버그에 대해 경고하기 시작할 정도로 피드백 루프를 대폭 줄여서, 버그를 수정하는 비용을 거의 0까지 줄일 수 있다.

유지 보수성

  • 테스트가 얼마나 이해하기 어려운가
    • 테스트 크기와 관련이 있다.
    • 테스트는 코드 라인이 적을수록 읽기 쉽다.
    • 테스트 코드의 품질은 제품 코드만큼이나 중요하다.
    • 테스트를 작성할 때 절차를 생략하지 말라.
    • 테스트 코드를 '일급 시민(first-class citizen)'으로 취급하라.
  • 테스트가 얼마나 실행하기 어려운가
    • 테스트가 프로세스 외부 종석성으로 작동하면 데이터베이스 서버를 재부팅하고 네트워크 연결 문제를 해결하는 등 의존성을 상시 운영하는 데 시간을 들여야 한다.
profile
Backend Engineer, Vim User

2개의 댓글

comment-user-thumbnail
2023년 7월 18일

잘 봤습니다. 좋은 글 감사합니다.

1개의 답글