오봉이와 함께하는 개발 블로그
Spring Framework - 테스트 코드 작성 (기본 - mock, spy, MockBean, SpyBean, MockWebServer) 본문
BE/Spring
Spring Framework - 테스트 코드 작성 (기본 - mock, spy, MockBean, SpyBean, MockWebServer)
오봉봉이 2024. 3. 31. 23:17728x90
FIRST 원칙
- Fast: 테스트는 빠르게 동작하여 자주 돌릴 수 있어야한다.(단위 테스트로 작성)
- Independent: 테스트는 독립적이며 서로 의존해서는 안 된다.
- Repetable: 반복하여 테스트가 가능해야한다.
- Self-Validating: 테스트는 성공 또는 실패로 bool값으로 결과를 내어 자체적으로 검증되어야한다.
- 예를 들어 로그나 System.out.println() 같이 콘솔에 찍히는 값을 비교하는 테스트는 지양
- Timely: 테스트는 적시에 즉, 테스트하려는 실제 코드를 구현하기 직전에 구현해야 한다.
Test double
- 특정 Service 계층에서 외부 시스템에 의존하는 것들이 많아지면 테스트 코드 작성이 매우 어려워진다.
- 예: DB, 테스트 대상이 아닌 API 서버 등
- 특정 Service를 테스트할 때 다른 의존성에서 정상적으로 처리되었는지 관심 있는 것이 아니라 Service의 메서드 내부 로직에 대한 관심이기 때문에 특정 의존성의 메서드 호출 시 적절한 데이터만 반환되면 테스트에 문제가 생기지 않는다.
- Test double을 만들기 위해 mock과 spy 등의 객체를 사용
stub
- mock 객체나, spy 객체의 특정 행위를 실행했을 때 기대하는 동작을 작성하는 것
- do**(Return, Answer, Throw)().when() 혹은 when().thenReturn() 등이 있음
- BDDMockito 라이브러리를 사용하면 문법이 다르지만 동작은 동일.
public class Service {
public void someMethodA(){ ... } // 내부 구현 코드 생략
public void someMethodB(){ ... } // 내부 구현 코드 생략
public void someMethodC(){ ... } // 내부 구현 코드 생략
}
// 위 클래스에 stubbing 진행
public class ServiceTest {
@Spy
private Service service;
@Test
void serviceTest() {
String someValue = "";
// 1. when().thenReturn()
when(service.someMethodA()).thenReturn(someValue);
// 2. doReturn().when()
doReturn(someValue).when(service).someMethodA();
// 3. doAnswer().when()
doAnswer(invocation -> {
/*
* 이곳에 특정 행위 stubbing
* invocation을 통해 다양한 값 사용 가능
* 예를 들어 invocation.getArgument()를 통해 해당 메서드 실행 시 필요한 파라미터 사용
*/
}.when(service).someMethod()
// 4. doThrow().when()
doThrow(new RuntimeException("someMessage")).when(service).someMethod();
}
}
mock
- 테스트 대상의 의존성 주입을 위한 가짜 객체
- mock을 통해 메서드를 수행하면 실제 내부 동작이 발생하지 않음
- 실제 동작이 발생하지 않기 때문에 반환 값이 있는 경우 null을 반환
- 반환 값이 없는 경우 아무 일도 수행하지 않음
- mock을 통해 테스트 대상이 외부 시스템 영향 없이 독립적인 테스트 수행 가능
- 일반적으로 when().thenReturn()을 사용하나, void 메서드일 경우 do*().when()을 사용
mock 객체 변화 예시
public class Service {
public void someMethodA(){ ... } // 내부 구현 코드 생략
public void someMethodB(){ ... } // 내부 구현 코드 생략
public void someMethodC(){ ... } // 내부 구현 코드 생략
}
- 위와 같이 특정 클래스에 메서드가 있을때 mocking 하게 되면 아래처럼 된다.
public class Service {
public void someMethodA(){ } // 내부 구현 코드 없어짐
public void someMethodB(){ } // 내부 구현 코드 없어짐
public void someMethodC(){ } // 내부 구현 코드 없어짐
}
- 내부 구현이 사라지기 때문에 테스트 대상에서 해당 메서드를 호출하는 경우 return 값을 지정해주어야 함
- ex) when(Service.someMethodA()).thenReturn(someValue);
- Service의 someMethodA()를 호출했을 때 someValue를 반환한다.
- ex) when(Service.someMethodA()).thenReturn(someValue);
- mock 객체의 stubbing을 하지 않을 시 Mockito는 해당 메서드 호출에 대한 기본 전략 적용
- void 메서드의 경우 아무 동작도, 아무 반환도 하지 않음
- primitive 타입 반환 메서드의 경우 0 반환
- int일 경우 0, double, float일 경우 0.0, boolean일 경우 false
- 객체 반환 메서드의 경우 null 반환
- Collection 혹은 Optional 등의 객체일 경우 Empty 반환
- 예를 들어 List일 경우 emptyList 반환
- 더 자세한 전략 정보는 org.mockito.Answers.java 클래스 참고
mocking 방법
public class SomeClass {
private SomeDependency someDependency;
}
// 테스트 대상은 SomeClass
// 위와 같은 클래스가 있을 때 테스트코드에서 아래와 같이 mocking 가능
/*
* 1. anootation 사용
* - annotation을 통해 mocking
* - @Mock을 통해 Mock 객체를 선언하고, @InjectMocks를 통해 해당 클래스애 의존성 주입
* */
@ExtendWith(MockitoExtension.class)
public class SomeClassTest {
@Mock
private SomeDependency someDependency;
@InjectMocks
private SomeClass someClass;
}
/*
* 2. 명시적인 방법
* - 명시적으로 mock 객체를 생성 하여 필드 인스턴스에 주입
* - 명시적인 mock 객체를 주입하여 new를 통해 객체 생성 후 필드 인스턴스에 주입
* - @BeforeEach를 통해 각 메서드 실행 시 독립적인 테스트 가능
* */
public class SomeClassTest {
private SomeDependency someDependency;
private SomeClass someClass;
@BeforeEach
void setUp() {
someDependency = mock(SomeDependency.class);
someClass = new SomeClass(someDependency);
}
}
spy
- 실제 인스턴스 객체
- 실제 인스턴스 객체이기 때문에 테스트 대상의 메서드를 재정의(stubbing) 하지 않으면 실제 동작이 발생
- 테스트 대상의 메서드를 재정의 할 경우 재정의한 동작을 수행
- 일반적으로 doAnswer().when() 혹은 doReturn().when() 사용
- when().thenReturn() 메서드를 사용할 수 있으나, 예외가 발생할 수 있어 doReturn().when() 사용
Spy 객체 변화 예시
public class Service {
public void someMethodA(){ ... } // 내부 구현 코드 생략
public void someMethodB(){ ... } // 내부 구현 코드 생략
public void someMethodC(){ ... } // 내부 구현 코드 생략
}
- 위와 같이 특정 클래스에 메서드가 있을때 spy 객체로 선언하게 되면 기존 클래스와 변함없이 작동한다.
public class Service {
public void someMethodA(){ ... } // 내부 구현 코드 생략
public void someMethodB(){ ... } // 내부 구현 코드 생략
public void someMethodC(){ ... } // 내부 구현 코드 생략
}
- 기존 내부 구현이 그대로 존재하고 재정의하여 동작을 변경하는 경우에 해당 메서드가 재정의한 동작에 따라 값을 반환
- 단, 코드 자체가 변경되는 것이 아닌, Mockito 라이브러리가 실행 시점을 파악하여 해당 메서드 실행 시점에 인터셉트하여 재정의한 동작을 실행
spy 방법
public class SomeClass {
private SomeDependency someDependency;
}
// 테스트 대상은 SomeClass
// 위와 같은 클래스가 있을 때 테스트코드에서 아래와 같이 mocking 가능
/*
* 1. anootation 사용
* - annotation을 통해 mocking
* - @Mock을 통해 Mock 객체를 선언하고, @InjectMocks를 통해 해당 클래스애 의존성 주입
* */
@ExtendWith(MockitoExtension.class)
public class SomeClassTest {
@Mock
private SomeDependency someDependency;
@Spy
@InjectMocks
private SomeClass someClass;
/*
* 2. anootation 사용 및 명시적인 방법
* - annotation을 통해 mocking
* - @Mock을 통해 Mock 객체를 선언하고, @InjectMocks를 통해 해당 클래스애 의존성 주입
* */
@ExtendWith(MockitoExtension.class)
public class SomeClassTest {
@Mock
private SomeDependency someDependency;
private SomeClass someClass;
@BeforeEach
void setUp() {
someClass = spy(new SomeClass(someDependency));
}
}
}
/*
*32. 명시적인 방법
* - 명시적으로 mock 객체를 생성 하여 필드 인스턴스에 주입
* - 명시적인 mock 객체를 주입하여 new를 통해 객체 생성 후 필드 인스턴스에 주입
* - @BeforeEach를 통해 각 메서드 실행 시 독립적인 테스트 가능
* */
public class SomeClassTest {
private SomeDependency someDependency;
private SomeClass someClass;
@BeforeEach
void setUp() {
someDependency = mock(SomeDependency.class);
someClass = new SomeClass(someDependency);
}
}
- 위 선언된 spy 객체는 모두 stubbing을 하지 않으면 모두 실제 인스턴스와 같은 동작을 수행
- spy 객체에 A(), B(), C() 메서드가 있고, A() 메서드만 stubbing 하면 A() 메서드에 대한 부분만 재정의 동작 수행
MockBean & SpyBean
- 기본 컨셉은 mock & spy와 동일
- 단,
@SpringBootTest
를 사용할 때 SpringBootApplication이 관리하는 IoC 컨테이너에 Bean을 등록하여 사용하게 해줌- 혹은
@WebMvcTest
에도 적용 - 핵심은 SpringBootApplication이 기동되는 테스트 진행 시 해당 어노테이션을 사용하여 SpringBean으로 등록하여 사용할 수 있다는 것
- 스프링이 관리하기 때문에 의존성이 필요한 경우 의존성이 자동으로 주입
- 혹은
- 기본 컨셉은 mock & spy와 동일하기 때문에 stubbing을 통해 재정의 가능
MockWebServer
- Spring에서 외부 API에 의존성을 가지는 경우 사용할 수 있는 라이브러리
- 프로덕션 코드에서 Spring WebFlux에서 제공하는 WebClient를 사용하는 경우 테스트 코드 작성에 도움을 주는 라이브러리
- 실제 비즈니스 로직에서 외부 API를 호출하여 데이터를 사용하는 경우 해당 API 호출을 테스트할 수 있음
public class SomeClassTest {
private MockWebServer mockWebServer;
@BeforeEach
void setUp() {
mockWebServer = new MockWebServer();
mockWebServer.start(/*{외부 API 서버 IP:port 정보}*/)
}
@AfterEach
void tearDown() throws IOException {
// FIRST 원칙 중 Independent를 고려하여 독립적인 테스트를 위해 사용
luxiaWebClient.shutdown();
marinerWebClient.shutdown();
}
}
- 주의 사항은 Queue를 통해 데이터를 반환하기 때문에 실제 외부 API 호출 순서에 따라 데이터를 세팅하는 순서가 맞아야 한다.
- 테스트 대상 메서드가 비즈니스 로직 수행 중 특정 API를 세 번 호출하는 경우 코드 작성 순서도 API에서 반환을 기대하는 값과 동일하게 작성해야 함
void someMethod() {
// API 호출하지 않는 일반 비즈니스 로직
// 첫 번째 API 호출
// API 호출하지 않는 일반 비즈니스 로직
// 두 번째 API 호출
// API 호출하지 않는 일반 비즈니스 로직
// 세 번째 API 호출
}
// 위와 같은 메서드가 테스트 대상이라고 가정할 때
void someMethodTest() {
String firstApiCallValue = "";
String secondApiCallValue = "";
String thirdApiCallValue = "";
// 기타 작업
// 첫 번째 API 호출에 따른 기대값 stubbing
mockWebServer.enqueue(new MockResponse()
.setResponseCode(200)
.setBody(firstApiCallValue)
.addHeader("Content-Type", "application/json"));
// 두 번째 API 호출에 따른 기대값 stubbing
mockWebServer.enqueue(new MockResponse()
.setResponseCode(200)
.setBody(secondApiCallValue)
.addHeader("Content-Type", "application/json"));
// 세 번째 API 호출에 따른 기대값 stubbing
mockWebServer.enqueue(new MockResponse()
.setResponseCode(200)
.setBody(secondApiCallValue)
.addHeader("Content-Type", "application/json"));
}
- body나 header 혹은 HTTPresponseCode등의 값들을 원하는 값으로 설정할 수 있다.
728x90
'BE > Spring' 카테고리의 다른 글
스프링 핵심 원리 - 고급편 > 템플릿 메서드 패턴 (적용 1, 적용 2, 정의) (0) | 2024.05.10 |
---|---|
스프링 핵심 원리 - 고급편 > 템플릿 메서드 패턴 (시작, 예제 1, 예제 2) (0) | 2024.05.10 |
스프링 핵심 원리 - 고급편_쓰레드 로컬(주의사항) (0) | 2024.03.17 |
스프링 핵심 원리 - 고급편_쓰레드 로컬(쓰레드 로컬 동기화 개발, 적용) (1) | 2024.03.17 |
스프링 핵심 원리 - 고급편_쓰레드 로컬(쓰레드 로컬 소개, 예제 코드) (0) | 2024.03.17 |
Comments