테스트 유닛을 잘 작성하는 방법론들은 많다. 이들에 대한 정리를 하다보면 테스트를 어떻게 작성해야 할 것인지 감을 잡을 수 있을 것 같아서 정리해본다.
FIRST
- Fast: 테스트 유닛은 빨라야 한다. 테스트라는 작업을 개발중에 자주 실행되는 작업이기 때문에 테스트의 실행 속도는 빨라야 한다.
- Independent(Isolated): 테스트 유닛의 실행은 독립적이어야 한다. 테스트 유닛 하나의 결과가 다른 테스트 유닛들에게 전파되어서는 안된다. I에 해당되는 내용은 Independent로 적시한 글도 있고 Isolated도 적시한 글도 있다. 이들의 사용한 의미는 동일하기에 필자는 둘 다 맞다고 생각한다.
- Repeatable: 테스트 유닛은 반복적으로 실행 가능해야 한다. 즉 네트워크, OS 등 외부 환경의 변화에 따라 테스트의 결과가 달라져서는 안된다.
- Self-Validating: 테스트의 결과는 항상 참, 거짓으로 판별하도록 하여 개발자가 결과물들을 보고 임의로 판단해서는 안된다.
- Timely: 테스트 유닛은 적시에 만들어야 한다. 예를 들어 red-green-refactor 개발 사이클을 가진 경우에는 red단계에서 테스트 유닛을 만들어야 한다.
A-trip
FIRST원칙과 마찬가지로 좋은 테스트를 작성하기 위한 원칙이다.
- Automatic: 실행과 결과의 확인은 자동화되어야 한다 => 사람이 눈으로 실행결과를 비교해 테스트의 통과 여부를 판단해선 안된다.
- Through: 테스트 코드에 문제가 발생할 경우 고쳐야 한다
- Repeatable: 순서에 관계 없이 반복실행이 가능해야 한다
- Independent: 테스트 코드는 독립적이어야 한다
- Professional: 테스트 코드의 역할에 맞게 작성해야 한다.
RIGHT-BICEP
테스트할 대상을 정하기 위한 원칙으로서 다음과 같다.
- Right: 결과가 올바른가
- Boundary: 경계 조건에서도 정상동작 하는가
- Inverse: 역 관계가 존재하는가
- Cross-check: 다른 수단을 이용해 결과를 교차확인할 수 있는가.
- Ex) 정렬알고리즘을 구현할 때 자바에 내장되어 있는 정렬 함수를 통해 값을 검증할 수 있어야 한다.
- Error condition: 에러 조건을 강제로 만들 수 있는가
- Performance: 성능이 한도가 존재하는가
- Ex) 정규식을 이용할 때 무제한으로 string이 입력들어올 경우.
Mockito에서 제안하는 좋은 테스트 작성법
java에서 Mock객체를 생성해주는 Mockito 프레임워크에서 제안하는 좋은 테스트를 작성하기 위한 원칙이다. 이 원칙에선 위에 설명한 내용들과 달리 Mock을 어떻게 다뤄야 하는지에 대한 가이드를 설명해준다.
- 테스트 코드는 간결하고 읽기 쉬워야 한다
- 반복적인 코딩은 피해라
- 성공 케이스와 실패 케이스를 많이 보여주기 위해 Coverage를 가능한 채워야 한다.
- 직접 만들지 않는 객체에 대해 Mocking하지 말라
- 모든 객체를 Mocking 하는 것은 안티패턴이다.
- 값 객체는 Mocking하지 말아라
3번에서 주의해야할 것은 Coverage를 채우는 것이 목적이 아니다. 앞의 수식어인 "성공 케이스와 실패 케이스를 최대한 보여주기 위해" 가 주 목적이다. 즉 성공과 실패 케이스를 많이 보여주는 것이 주목적이지 Coverage를 채우는 것이 목적이 아니다.
4~6 번에 대해선 작성할 내용이 많기에 소주제로 빼서 설명하겠다.
4. 직접 만들지 않는 객체에 대해 Mocking하지 말아라
이는 반드시 지켜야 하는 원칙은 아니라고 설명한다. 그러나 이 원칙을 준수하면 사이드 이팩트를 감소하는 이점이 있기에 설명했다고 생각한다.
Mockito에서는 다음과 같은 이유로 사용하지 말라고 권유한다.
- 외부 라이브러리의 로직이 변경되어서 테스트 환경과 운용 환경에서의 실행 결과가 달라진다. 테스트 유닛에서는 Mock 객체이므로 외부 로직의 변경사항이 반영되지 않아 테스트에서는 성공하여도 운용 환경에선 실패할 수 있다.
- 외부 라이브러리를 Mocking하는 것은 객체가 외부 라이브러리와 결합도가 높다는 의미이므로 객체의 책임을 분리해야 한다는 신호이다.
- 외부 라이브러리는 내부 동작을 알 수 없다. 그렇기에 얼마나 많은 객체를 Mocking해야 하는지를 알 수 없다. 만약 이 내용이 복잡하다면 필요한 Mock 객체를 찾기 위한 시간을 써야하고 수많은 객체들을 Mocking해야 하기 때문에
테스트 코드는 간결하고 읽기 쉬워야 한다
는 원칙을 위배한다.
위의 내용을 요약하자면 외부 라이브러리라는 알 수 없는 로직을 가진 객체를 Mocking하는 것은 외부 라이브러리에 의존한다는 의미가 되며 외부 라이브러리의 수정이 실제 운용되는 코드에 전파되어 영향을 미칠 수 있지만 테스트 유닛에서 Mock객체로 만들어서 사용하면 이 영향을 알 수 없는 사이드 이팩트가 발생한다고 알 수 있다.
Mockito에서는 외부 라이브러리를 직접 Mocking하는 것보단 이를 wrapping한 객체를 만들고 이를 Mocking하는 것을 권고한다. 단 이 방식은 Leaky Abstraction 문제가 발생할 수 있다.
Leaky Abstraction(추상화의 구멍): 추상화를 완전히 하지 못한 상황에서 추상화를 하지 못한 부분에 문제가 발생하여 추상화의 역할을 할 수 없는 상황
5. 모든 객체를 Mocking하는 것은 안티패턴이다
이는 간단하게 설명할 수 있다. 모든 객체를 Mocking한다는 의미는 실제 테스트해야하는 코드가 포함된 객체도 Mock 객체로 만들어진다. 즉 진짜 테스트가 필요한 객체까지 Mocking하기 때문에 테스트 코드를 작성하는 의미가 없다. 그래서 안티패턴이다.
6. 값 객체를 Mocking하지 말라
값 객체를 만드는 것이 어려워 Mock 객체로 만들려는 것은 값 객체에 리팩토링이 필요하다는 의미이다. 이를 피하기 위해 값 객체 생성을 builder 패턴으로 만들거나 테스트 클래스에서 팩토리 메소드를 통해 값 객체를 만드는 것을 추천한다. 다음의 코드를 보자.
@ExtendWith(MockitoExtension.class)
class ArticleServiceTest {
@InjectMocks
private ArticleService sut;
@Mock
private ArticleRepository repository;
@Test
void givenArticle_WhenCreateArticle_ThenAddCount() {
// Given
ArticleDto articleDto = ArticleDto.builder()
.title("title!!")
.content("content!!")
.build();
given(repository.save(any(Article.class))).willReturn(any(Article.class));
// When
sut.saveArticle(articleDto);
// Then
then(repository)
.should().save(any(Article.class));
}
}
ArticleDto객체는 Article객체의 DTO객체이다. 이 DTO객체는 저장하는 곳 뿐만 아닌 수정, 삭제를 요청할 때도 사용한다. 이처럼 같은 DTO객체를 여러 장소에서 사용해서 값 객체를 생성하기가 어려울 때는 builder 패턴이나 테스트 클래스에서 Factory Method 패턴을 통해 만듬으로서 값 객체를 반드시 만들어서 사용하는 것을 권장한다.
결론
대부분의 테스트 원칙은 비슷한 말을 반복하고 있다. 테스트 유닛은 하나의 코드이자 문서의 역할을 하기에 이 관점으로 위의 원칙들을 바라본다면 어렵지 않게 이해될 것이다. 필자 입장에서는 Mockito의 Mock객체를 다루는 방법에 대해서 흥미로웠다. 이는 다른 원칙들에서 찾아볼 수 없는 오직 Mock을 다룰 때 필요한 주의점들이 흥미로웠다.