정글에서 온 개발자
1/12 TIL 통합테스트 본문
통합테스트 이유
단위 테스트에만 전적으로 의존하면 시스템이 전체적으로 잘 작동하는지 확신할 수 없다.
단위 테스트가 아닌 테스트는 모두 통합테스트다.
단위 테스트는 도메인 모델을 다루고, 통합테스트는 프로세스 외부 의존성과 도메인 모델을 연결하는 코드를 확인한다.
어플리케이션이 간단할수록(도메인 로직이 없을 수록) 통합테스트 수와 단위테스트 수가 같아진다. (단위테스트가 오히려 없을 수도 있다.)
주의사항
통합테스트는 주요흐름(happy path)와 단위테스트가 못 다루는 예외상황(edge case)를 다룬다.
외부 의존성과의 상호작용을 모두 확인하도록 작성하고, 부족하면 모두 확인하게 추가 작성한다.
빠른 실패 (코드에서의 예외 던지기) 를 하면 테스트가 필요없다. 이를 통해 통합테스트의 추가 작성 비용을 줄일 수 있다.
엔드 투 엔드 테스트
엔드 투 엔드 테스트는 어떤 프로세스 외부 의존성도 목으로 대체하지 않는 것이다. (이를 실행하는 건 선택적이다.)
외부 애플리케이션을 모방하는 객체가 테스트 하므로, 목을 안 쓰고 데이터 베이스 확인은 API를 통해 상태 확인한다.
관리 의존성 VS 비관리 의존성
관리 의존성(대개 데이터베이스)과의 통신
- 구현 세부사항
- 실제 인스턴스 사용 - 최종상태를 확인할 수 있는 장점이 있다.
- 구현이 하나라면 인터페이스도 필요 없다.
- 상호작용을 신경쓰지 않는다. 최종 상태만 확인한다.
- 테스트 시 목으로 만들면 안된다
- 리팩터링 내성이 저하된다.
- 회귀 방지도 떨어진다.
비관리 의존성과의 통신
- 식별할 수 있는 동작
- 목으로 대체 - 하위 호환성을 지키고, 통신 패턴 영속성을 보장할 수 있다.
관리 의존성이면서 비관리 의존성인 프로세스 외부 의존성
- 이런 경우는 되도록 안 만드는 게 좋다. (API나 메세지 버스로 대체)
- 두 부분을 구분하여 관리한다.
- 다른 곳에서 볼 수 있는 부분은 비관리 의존성으로 취급한다 -> 사실상 메세지 버스 역할을 한다. -> 목을 사용할 수 있다.
통합 테스트에서 실제 데이터베이스를 못 쓰는 경우
- 이런 경우에도 목을 쓰면 안된다.
- 차라리 통합 테스트를 아예 작성하지 말고, 도메인 모델의 단위 테스트에만 집중
- 목으로 대체한 통합테스트가 있어봤자 리팩터링 내성이 떨어지고, 회귀 방지도 떨어지기 때문 (실제 동작과의 괴리가 있기 때문에)
- 데이터베이스가 유일한 프로세스 외부 의존성이라면, 기존 단위 테스트 세트와 다를바 없게 되기도 함
여기서 말하는 실제 데이터베이스는, 운영 데이터베이스를 말하는 것이 아닌 사용할 데이터베이스와 같은 버전의 데이터베이스를 말함
인터페이스 사용
인터페이스 사용의 오해
- 프로세스 외부 의존성을 추상화해 느슨한 결합을 달성
- 기존 코드를 변경하지 않고 새로운 기능을 추가해 공개 폐쇄 원칙(OCP)를 지킴
둘다 틀렸다.
- 단일 구현을 위한 인터페이스는 추상화가 아니다. 이렇게 구현하면 구체 클래스보다 결합도가 낮지 않다.
- 추상화는 발명하는 것이 아니라, 발견하는 것이다.
- 따라서 인터페이스가 진정으로 추상화되려면 구현이 적어도 두 가지는 있어야 한다.
- 더 기본적 원칙인 YAGNI를 위반한다. - 향후 기능을 설명하려고 기능을 개발하거나, 기존 코드를 수정하지 말라.
- 기회비용 - 나중되면 기획은 바뀌거나 더 고도화된다.
- 프로젝트 코드는 작을수록 좋다. 코드는 문제 해결의 값비싼 방법이다.
통합 테스트를 위한 리팩토링
도메인 모델 경계 명시하기
- 모델을 잘 알려진 위치에 두기
계층 수 줄이기
- 추상 계층이 너무 많으면 코드베이스를 탐색하기 어렵다.
- 간단한 연산도 숨은 로직을 이해하기 어렵다.
- 정신적으로 부담된다.
- 단위 테스트와 통합테스트에도 도움이 되지 않는다 -> 컨트롤러와 도메인 모델 사이에 명확한 경계가 오히려 없어진다.
- 각 계층이 따로 검증된다. ->리팩터링 내성과, 회귀 방지가 떨어진다.
- 백엔드는 도메인 모델 - 서비스 계층(컨트롤러) - 인프라 계층세가지면 충분하다.
*인프라 계층 : 도메인 모델에 속하지 않는 알고리즘과 프로세스 외부 의존성에 접근할 수 있는 코드
순환 의존성 제거하기
- 코드 읽을 때 부담
- 인터페이스로 컴파일을 통과하더라도 런타임 순환 의존성도 있을 수 있음
- 테스트를 방해함 - 인터페이스를 목으로 처리해야 하는데, 도메인 모델은 이렇게 하면 안됨
모든 순환 의존성을 제거하는 것은 불가능하다.
테스트에서 다중 실행 구절 사용
원하는 상태로 만들기 어려운 외부 의존성 프로세스는 예외
의존성 안티패턴 제거
- 앰비언트 컨텍스트 - 정적 메서드를 사용한 의존성 주입
- 서비스 로케이터 - 의존성에 대한 제한 없는 전역 접근
클래스 생성자나, 메서드 인수로 명시적으로 주입하자
로깅 테스트
- 로깅이 식별할수 있는 동작이라면 테스트, 구현 세부사항이면 패스
- 지원로깅은 식별할 수 있는 동작
- 진단 로깅은 개발자만 보기 때문에 구현 세부사항
테스트 방법
- 목을 사용한다. - 지원로깅만 테스트할 것이기 때문이다. 외부에서 식별할 수 있는 동작은 인스턴스가 아니라 목을 쓴다.
- ILogger 인터페으스를 바로 목으로 쓰지 않는다.
- 진단 로깅도 같이 쓰는 인터페이스이기 때문이다.
- DomainLogger같은 클래스를 만들고 해당 클래스와의 상호작용을 확인한다.
- 외부 의존성이 있어 도메인 모델에서 외부 의존성을 사용하게 된다면, 이벤트를 도입한다.
로깅의 양
과도한 로깅은 코드를 혼란스럽게 한다. 신호 대 잡음 비율이 핵심이다.
도메인 모델에서는 진단 로깅을 절대 사용하지 않는다. 디버깅용으로 사용해도 바로 지운다.
내 생각
드디어 책을 읽기 시작한 이유인 통합테스트다 .
데이터베이스 테스트 방법부터, 인터페이스나 로깅에 대한 원칙적인 내용까지 좋은 내용이 많았다.
그러나 이번 장에서도 SQL에 대한 테스트 얘기는 나오지 않았다
리팩토링을 하기 전에 오히려 단위테스트보다 happy path에 대한 통합테스트부터 작성하는 것도 괜찮을 것 같다는 생각이 들었다.
빠른 실패를 위한 예외만 잘 작성해도 테스트가 줄어든다는 사실을 알았다. 그런 면에서 go는 예외를 거의 convention처럼 써서 좋은 것 같다. 그러나, 예외를 잘못 썼을까봐 테스트를 하는 것도 있지않나? 예외 검사는 정말 하지 않아도 되는걸까?
데이터베이스를 다룰 때 구현이 하나라면 인터페이스도 안 쓴다는 대목이 있는데, 이 부분은 클린아키텍처랑은 조금 부딪히는 부분이 있는 것 같다. 세부사항은 언제든 바뀔 수 있기 때문이다. 인프라 계층(내 프로젝트 기준 repository) 구현체에서의 추가적인 인터페이스를 쓰지 말라는 것으로 해석된다. 아니면 repository를 어플리케이션 계층의 구현으로 생각할 수도 있겠다.
중간에 로깅되는 데이터와 데이터가 보이는 템플릿을 분리하는 구조화된 로깅에 대한 내용이 나왔는데, 한번 공부해볼 만 한 것 같다.
참고
'TIL' 카테고리의 다른 글
1/14 TIL 데이터베이스 테스트 (0) | 2025.01.15 |
---|---|
1/13 TIL 목 처리 (0) | 2025.01.13 |
1/11 TIL 가치 있는 단위 테스트를 위한 리팩토링 (0) | 2025.01.12 |
1/9 TIL 테스트, 리팩토링, 볼링게임 (0) | 2025.01.09 |
1/8 TIL [CKA] 기본 개념 (1) | 2025.01.09 |