-
3. 단위 테스트 구조개발 도서/Unit Testing 2024. 6. 4. 01:23
이제 직접적으로 적용할 수 있는 개념이 나와서 개인적인 감상을 덧붙힌다.
3.1 단위 테스트를 구성하는 방법
3.1.1 AAA패턴
AAA패턴 : Arrange(준비), Act(실행), Assert(검증)으로 이루어진 테스트 구조.
일반적으로 많이 사용하는 Given-When-Then 패턴과 차이는 없다고 볼 수 있지만, AAA패턴은 조금 더 비 개발자에게 공유하기 적합한 용어이다.
더보기나 또한 Given-When-Then을 사용하고 있었지만, 이 패턴에 사용되는 용어는 개인적으로 추상적인 느낌이 있어 런던파의 방식에 어울리는 용어가 아닌가 하는 생각이 들었다.
회사에 따라 내부 컨벤션을 살펴봐야 겠지만, 앞으로의 개인 프로젝트는 AAA패턴을 적용할 생각이다.
3.1.2 여러개의 준비, 실행, 검증문 피하기
한번의 준비, 실행, 검증으로 테스트를 끝내지 못한다는 것은 한번에 너무 많은 것을 검증한다는 의미다.
이는 통합 테스트의 영역이며, 단위 테스트에서는 이와 같은 상황에서 단위를 나누어 검증하는 것이 옳다.
3.1.3 테스트 내 if문 피하기
if문 또한 한번에 많은 것을 검증한다는 표시이다. 이는 안티 패턴으로 분류된다.
3.1.4 각 구절은 얼마나 커야하는가?
일반적으로 준비 구절이 가장 크다. 다만 실행 + 검증 구절보다 준비 구절이 더 크다면, 비공개 메서드 혹은 팩토리 메서드로 도출하는 것이 좋다.
실행 구절이 한 줄 이상인 것을 경계하라.
실행 구절이 두 줄 이상인 경우 SUT의 공개 API에 문제가 있을 수 있다.
해결책으로는 캡슐화를 항상 지키는 것이다.
하나의 동작이 다른 클래스의 동작에 의존하지 않아야 함을 명심하자.
3.1.5 검증 구절에는 검증 문이 얼마나 있어야 하는가
하나의 테스트로 모든 결과를 평가하는 것이 좋지만, 검증 구절이 너무 커지는 것은 좋지 않다.
대체재로 SUT 내에 적절한 동등 멤버(equality member)를 정의하는 것이 좋다.
이를 통해 단일 검증만으로 객체를 기대값과 비교할 수 있다.
3.1.6 종료 단계
대부분의 단위 테스트는 AAA패턴 내에서 종료된다. 마지막에 생성된 파일을 지운다거나, 연결을 종료하는 등의 작업을 수행하는 종료 단계는 통합 테스트의 영역이다. 이는 이 책의 3부에서 통합 테스트와 함께 다룰 예정이다.
3.1.7 테스트 대상 시스템 구별하기
SUT의 변수 명을 sut로 명명하여 다른 의존성과 구분하자.
동작의 진입점이 되는 SUT는 단 하나만 존재할 수 있으며, 이는 핵심적인 역할이라 볼 수 있다.
때문에 다른 의존성과 SUT를 구분하는 것이 좋다.
더보기위 내용은 약간의 장단점이 나뉠 것 같다.
실행, 검증 구절에서 sut가 무엇인지를 찾기 위해 준비 구절을 거슬러 올라가야하는 불편함이 생기기 때문이다.
다만 글쓴이가 주장하고자 하는 바를 좀 더 명확하게 느끼려면, 다양한 테스트 코드에서 SUT의 중요성을 되새겨 볼 필요성이 있다.
3.2 xUnit 테스트 프레임워크 살펴보기
xUnit은 .NET에서 사용하는 단위 테스트 프레임워크이다.
객체지향 언어는 각각의 프레임워크도 비슷한 구조를 띄기 때문에, 추가 조사와 함께 학습을 진행했다.
테스트 이전에 호출, 테스트 이후에 호출하는 메서드는 JUnit의 @BeforeEach, @AfterEach와 동일한 효과인 듯 하다.
3.3 테스트 간 텍스트 픽스처 재사용
텍스트 픽스처 : 객체를 생성하기 위한 인자값
텍스트 픽스처를 재사용하기 위한 방법으로 두가지를 제시했다.
첫번째는 3.2에서 소개한 SetUp 메서드를 사용해 픽스처를 초기화하는 것이다.
이는 아래와 같은 단점을 가진다.
- 테스트 간 결합도가 높아진다.
- 테스트 가독성이 떨어진다.
예를들어 상품 주문 개수를 10에서 15로 변경하면, 해당 텍스트 픽스처를 사용하는 테스트 메서드의 모든 검증 값 또한 그에 맞게 변경되어야 한다.
또한 테스트만을 보고 이해하는데 어려움을 겪어 SetUp 메서드 까지 봐야하는 불편함이 생긴다.
더 좋은 방법으로서 비공개 팩토리 메서드를 제시했다.
메서드 명을 통해 충분한 가독성을 제공하면서, 코드는 짧게 할 수 있는 장점이 있다.
또한 인자값을 통해 테스트마다 값을 변경할 수 있기 때문에 결합도 또한 낮아진다.
다만 한가지 예외로, 테스트의 대부분 혹은 모두에 사용되는 픽스처라면 기초 클래스(base class)를 만들어 상속 받아 사용하는 것이 더 합리적이다.
더보기해당 내용은 지금까지 프로젝트를 진행하면서 자주 불편함을 겪었던 내용들이 많아 굉장히 유익했다.
아래는 내가 JUnit5를 사용해 테스트를 진행했던 프로젝트이다.
1. 잘못된 텍스트 픽스처 사용 경험 : https://github.com/tangpoo/eCommerce/blob/dev/src/test/java/com/potato/ecommerce/domain/store/StoreTestUtil.java
2. base class를 활용한 텍스트 픽스처 경험 : https://github.com/tangpoo/DataCollector/tree/main/src/test/java/study/tangpoo/livecodingtest
1번 프로젝트의 경우 Util 클래스를 통해 객체를 생성하는 모든 곳에 텍스트 픽스처를 적용했더니, 가독성이 끝장나서 결국 롤백했던 경험이 있다.
이 책에서 제시했던 대로 클래스의 필드가 아닌 팩토리 메서드를 적용했다면 더 좋은 테스트가 되었을 것 같다.
텍스트 픽스처가 해당 클래스에서만 사용되지 않을 확률이 높으니, public 클래스로 분리하여 여러 테스트에서 재사용할 수 있게 하는 것은 여전히 시도해 볼 법 하다.
3.4 단위 테스트 명명법
3.4.1 단위 테스트 명명 지침
표현력 있고 읽기 쉬운 테스트 이름을 지으려면 다음 지침을 따르자
- 염격한 명명 정책을 따르지 않는다. 복잡한 동작에 대한 높은 수준의 설명을 이러한 정책의 좁은 상자안에 넣을 수 없다. 표현의 자유를 허용하자.
- 문제 도메인에 익숙한 비개발자에게 시나리오를 설명하는 것 처럼 테스트 이름을 짓자. 도메인 전문가나 비즈니스 분석가가 좋은 예다.
- 단어를 밑줄(_) 표시로 구분한다. 특히 긴 이름에서 가독성 향상에 도움이 된다.
책에선 테스트 클래스 명에 [클래스 명]Tests 패턴을 사용하지만, 테스트가 해당 클래스만 검증하는 것은 아니다.
테스트 클래스에 적힌 클래스 명은 진입점 또는 API로 여기자.
3.4.2 예제: 지침에 따른 테스트 이름 변경
다음은 가장 엄격한 명명 정책을 통해 만든 메서드 명을 위 지침에 맞게 변경하는 과정이다.
해당 메서드는 과거 배송일이 유효하지 않다는 것을 검증하는 테스트이다.
IsDeliveryValid_InvalidDate_ReturnFalse()
프로그래머가 아닌 사람도 알 수 있게 변경 -> Delivery_with_invalid_date_should_be_considered_invalid()
중복되는 invalid 대신 유효한 배송 날짜를 뜻하는 past 사용 -> Delivery_with_past_date_should_be_considered_invalid()
장황한 단어 (considered) 제거 -> Delivery_with_past_date_should_be_invalid()
소망이나 욕구 제거(should_be) -> Delivery_with_past_date_is_invalid()
관사 적용(a) -> Delivery_with_a_past_date_is_invalid()
더보기어라? 비 개발자도 알 수 있고 직관적인 이름이 된 것은 좋지만,
성공과 실패 두가지 가능성을 내포했던 테스트 명이 성공만을 검증하는 이름으로 변경됐다.
이에 대한 의문은 3.5에서 어느정도 해결된다.
3.5 매개변수회된 테스트 리팩터링하기
위에서 만든 Delivery_with_a_past_date_is_invalid() 메서드는 배송일이 유효한지 검증하는 테스트이다.
하지만 오늘이 유효한지, 내일이 유효한지, 내일 모래가 유효한지에 대해 모두 테스트 케이스를 만들 수는 없다.
좋은 방법은 매개변수화된 테스트를 통해 이러한 테스트를 하나로 묶는 것이다.
이와 같은 상황에 사용되는 것이 xUnit에 있는 [InlineData]라는 특성이다. (JUnit에선 @ValueSource를 사용하면 될 것 같다.)
인자 값으로 보낼 데이터의 집합을 만들고, 메서드에 [Theory] 특성을 사용하면 [InlineData]에 정의한 데이터를 순차적으로 해당 테스트의 인자값으로 실행한다.
이렇게 테스트 코드의 양을 크게 줄일 수 있었지만, 테스트 메서드가 나타내는 사실을 파악하기가 어려워졌다.
절충안으로, 긍정적인 테스트는 고유한 테스트로 도출하고, 가장 중요한 부분을 잘 설명하는 이름을 쓰면 좋다.
다음 예제와 같이 유효한 배송 날짜와 유효하지 않은 배송 날짜를 구별하는 요소로 결정하는 것이 그렇다.
부정적인 시나리오
[InlineData(-1)]
...
Detects_an_invalid_delivery_date(int daysFromNow) { ... }
긍정적인 시나리오
The_soonest_delivery_date_is_two_days_from_now() { ... }
3.6 검증문 라이브러리를 사용한 테스트 가독성 향상
검증문 라이브러리를 사용하면, 쉬운 영어처럼 읽도록 검증문에서 단어 순서를 재구성해 테스트 가독성을 더욱 향상시킬 수 있다. [주어] [행동] [목적어]
더보기책에서 소개하는 .NET 기준으로는 Fluen Assertions라는 외부 라이브러리를 추가했지만,
JUnit에는 AssertJ가 동일한 역할을 한다.
'개발 도서 > Unit Testing' 카테고리의 다른 글
6. 단위 테스트 스타일 (0) 2024.06.18 5. 목과 테스트의 취약성 (0) 2024.06.17 4. 좋은 단위 테스트의 4대 요소 (0) 2024.06.14 2. 단위 테스트란 무엇인가 (0) 2024.06.03 1. 단위 테스트의 목표 (0) 2024.06.01