4. 테스트
토비 왈 : 테스트를 만들지 않을 거면 스프링을 도대체 뭐하러 쓰는 거죠?
1. 자동으로 수행되는 테스트(Automated Test)
수동 테스트의 한계
프린트 된 메세지를 수동으로 확인하는 방법은 불편하다.
사용자 웹 UI까지 개발한 뒤에 확인하는 방법은 테스트가 실패했을 때 확인할 코드가 많다.
가장 작은 단위로 기능을 쪼개서 테스트 하는 것이 기능적 결함이 발생할 가능성이 적다.
테스트 할 대상이 많아질수록 검증하는데 시간이 많이 걸리고 부정확하다.
결국 사람이 직접 하는 일이기 때문에, 구멍이 생길 수 밖에 없다.
자동으로 수행되는 테스트 (작은 크기의)
개발자가 만드는 테스트
개발한 코드에 대한 검증 기능을 코드로 작성한다.
자동으로 테스트를 수행하고 결과를 확인한다.
테스팅 프레임워크를 활용한다. (ex, JUnit)
테스트 작성과 실행도 개발 과정의 일부이다.
2. JUnit 테스트 작성
JUnit 5
@Test테스트 메소드 : 테스트 메서드에 붙이는 에너테이션@BeforeEach테스트 : 각각의@Test메서드가 실행 되기 전마다@BeforeEach메서드가 실행된다.테스트마다 새로운 인스턴스가 만들어진다.
각각의 테스트가 다른 테스트에 영향을 받지 않도록 하기 위해서 테스트마다 각각 새로운 인스턴스를 생성한다.
매번
SortTest인스턴스가 새롭게 생성된다.
3. PaymentService 테스트
우리가 만들어 낸 PaymentService 를 테스트 해보자.
PaymentService 의 요구사항에 맞추어서 테스트를 해보자.
환율정보 가져오기
원화 환산 금액 계산
원화 환산 금액 유효시간 계산
4. 테스트의 구성 요소
그렇다면 우리가 만든 PaymentService 에 대한 테스트가 모두 완료된 것일까?
PaymentService 에 대한 테스트가 모두 완료된 것일까?문제점
우리가 제어할 수 없는 외부 시스템에 문제가 생긴다면?
환율 정보를 가져오는 외부 API 서버의 문제가 생겨버리면 테스트를 할 수 없다.
ExRateProvider가 제공하는 환율 값으로 계산한 것인가?prepare메서드만 보고서ExRateProvider가 제공하는 메서드인지 확신할 수 없다.
환율 유효 시간 계산은 정확한 것인가?
일반적으로 테스트는 다음의 구성 요소를 가지고 있다.
테스트 :
PaymentServiceTest테스트 대상 :
PaymentService협력자 :
WebApiExRateProvider협력자의 경우 개발자가 관여할 수 없는 외부 서버에 의존하는 경우가 있는데, 이 경우 서버 상황에 따라 각각 다른 결과를 얻게 될 수 있다.
현재 상황에서
PaymentService를 테스트하고 싶은거지,WebApiExRateProvider를 테스트하고 싶지는 않다.이런 경우를 위해서 고정된 결과를 리턴해주는 Stub or Mock 객체를 만들어 사용할 수 있다.
이 구조가 만들어지기 위해서는 테스트 대상이 특정 인터페이스(
ExRateProvider)를 상속받은 클래스를 사용하는 구조가 되어야 한다.
Stub vs Mock
Stub
테스트 실행을 위한 "미리 준비된 값/상태" 제공
상태 검증 (SUT의 최종 상태나 반환 값)
Mock
객체 간의 "올바른 상호작용/호출 순서 및 횟수" 검증
행위 검증 (SUT가 의존 객체의 메소드를 올바르게 호출했는지)
결국, Mock과 Stub은 다른 것이며, 어떤 테스트 더블을 사용할지는 테스트의 목적과 무엇을 검증하고 싶은지에 따라 결정해야 한다. 상태를 검증하고 싶다면 스텁을, 행위를 검증하고 싶다면 목을 사용하는 것이 일반적인 접근 방식이다.


5. 테스트와 DI
외부와 의존성이 없는 ExRateProviderStub 클래스를 만들고 해당 클래스 인스턴스를 테스트에서 사용하도록 변경하였다.
이로 인해 외부와 의존성이 없는 테스트를 만들 수 있게 되었다.
수동 DI 를 이용하는 테스트
테스트용 수동 협력자(Colloborator) / 의존 오브젝트를 테스트 대상에 직접 주입하고 테스트
스프링이 원하는 방향
스프링은 기본적으로 DI 를 사용하면서, 외부와 종속적이지 않은 테스트를 만들기를 원한다.
아래 코드는 이러한 방향을 잘 따른 코드라고 볼 수 있다.
스프링 DI 를 이용하는 테스트
테스트용 협력자(Collborator) / 의존 오브젝트를 스프링의 구성 정보를 이용해서 지정하고, 컨테이너로부터 테스트 대상을 가져와 테스트
알아야 하는 어노테이션
@ExtendWith(SpringExtension.class): JUnit 이 스프링의 기능을 사용한다는 어노테이션, 기계적으로 사용하자.@ContextConfiguration: 테스트가 실행될 때, 해당 어노테이션으로 지정된 클래스를 바탕으로 스프링 컨테이너에 빈을 등록해준다.@Autowired: 테스트가 실행될 때 해당 타입과 매칭되는 인스턴스가 스프링 컨테이너 내에 있다면 해당 인스턴스를 DI 해준다.
스프링 컨테이너를 사용한 테스트
가장 먼저 ObjectFactory 와 같은 테스트 용도로 구성정보를 설정하는 TestObjectFactory 를 만들고 사용하자.
개선된 방식
ExRateProviderStub 빈을 직접적으로 가져와 내부 값(환율 정책)을 바꾸어가면서 사용할 수도 있다.
(테스트 용도로 사용하는 클래스이기 때문에 Setter 를 사용해 값을 변경하며 테스트를 할 수 있다)
6. 학습 테스트
언제 학습 테스트를 할까?
직접 만들지 않은 코드, 라이브러리, 레거시 시스템에 대한 테스트가 필요할 때 학습 테스트를 한다.
학습 테스트의 목적은 사용할 API, 프레임워크의 기능을 테스트로 작성하고 실행해보면서 사용방법을 바르게 이해했는지 확인할 수 있다.
외부 기술, 서비스가 버전이 올라갔을 때, 이전과 동일하게 동작하는지 확인할 수도 있다.
학습 테스트 예제 (Clock)
7. Clock 을 이용한 시간 테스트
스프링 컨테이너에
Clock을 이용해FixedClock을 만들어PaymentService를 테스트해보자.

테스트를 위한 사전작업
테스트를 위해서 우리가 사용하던
ObjectFactory를 도메인에 맞는 이름(PaymentConfig) 로 바꾸어 보자.그리고
PaymentService에서Clock을 사용할 수 있도록 코드를 수정해주자.
수동 DI 를 이용하는 테스트
수동 DI 테스트에서는 직접 의존성을 넣어주어야 한다.
스프링 DI 를 이용하는 테스트
스프링 DI 테스트에서는
PaymentConfig에서 이미 DI 설정이 이루어졌기 때문에,@Autowired를 통해 직접 인스턴스를 가져와 사용하면된다.스프링을 사용하는 테스트는 DI 부분을 고려하지 않기 때문에, 코드의 변경이 크지 않다.
8. 도메인 오브젝트 테스트
토비왈 : 테스트할 시간이 없다고 하더라도, 도메인 오브젝트 테스트는 꼭 하길 바람
도메인 모델 아키텍처 패턴이란?
도메인 로직, 비지니스 로직을 어디에 둘 것인가를 결정하는 패턴
아키텍처 패턴
트랜잭션 스크립트 - 서비스 메서드(
PaymentService.prepare)현재 코드에서 사용하고 있는 방식
service로직에서 모든 서비스 로직을 담당한다.
도메인 모델 - 도메인 모델 오브젝트(
Payment)도메인 모델 오브젝트에서 도메인 모델이(
Payment) 서비스 로직을 가지고 있어야 한다.현재 코드에서
Payment는 값만 가지고 있다.그렇기에 현재 코드는 도메인 모델 방식의 코드가 아니다.
도메인 모델 방식을 사용했을 때, 테스트에서 얻는 이점이 크다.
도메인 모델 안에 비지니스 로직이 모두 들어 있다면, 외부 의존성과 관계 없이 독립적으로 테스트 가능하다.
응집도가 높아지기 때문에, 테스트 대상이 되는 도메인의 경계를 명확하게 할 수 있다.
도메인 모델 아키텍처 패턴 예제
기존에는 PaymentService.prepare() 메서드 내부에서 모든 비지니스 로직을 담당하고 있었다.
환율을 가져오고,
원화 환산 금액을 계산하고,
환율 유효시간을 계산하다.
하지만 도메인 오브젝트 모델에서는, 도메인 모델(Payment)이 서비스 로직을 가지고 있어야 한다.
팩토리 메서드 패턴(Payment.createPrepared())을 사용해, 외부 오브젝트에 영향을 받는 로직을 제외하고는 도메인 모델로 서비스 로직을 옮기자.
외부 영향을 받는 로직 : 환율을 가져오는 로직
팩토리 메서드 패턴을 사용한 예제
테스트가 정상적으로 실행된다.
Payment안으로Payment와 관련된 로직을 넣어PaymentService내에서는 정말Payment객체를 사용하기만 했다.
Payment 유효시간을 검증하는 로직도 Payment 안에 추가해 테스트를 더 간편하게 작성해보자.
isValid()메서드를 추가해Payment응집도를 강화했다.이런식으로 도메인 객체를 강화하면
PaymentService와 같은 서비스 로직에서 도메인 객체에 대한 결합도가 줄어든다.
도메인 모델 아키텍처 패턴을 왜 사용하는가?
객체 지향을 이야기 할 때, 좋은 객체 지향 코드는 다음과 같다고 한다.
결합도가 낮고, 응집도가 높은 코드가 좋은 코드이다.
도메인 모델 아키텍처 패턴을 적용하기 전 코드를 생각해보면, 요구사항이 복잡해지고, 추가적인 클래스를 만드는 상황이 생겨난다면
PaymentService클래스에 변화가 생겨나고, 결합도는 높아질 것이다.이를 해결하기 위해서, 각 도메인의 기능들은 해당 메서드로 옮겨서, 결합도를 낮추고, 응집도를 높이는 설계를 할 필요가 있다.
하지만, 비용이 많이 드는 것은 사실이다.
때문에, 작은 부분부터 도메인 모델을 적용해보자!
Last updated