Contents
1. Mockito 소개
모키토는 Mock을 지원하는 프레임워크로, 진짜 객체처럼 동작하지만 그 동작을 프로그래머가 컨트롤 할 수 있다. 즉, Mock을 쉽게 만들고 관리, 검증하는 프레임워크가 Mockito이다.
- EasyMock, JMock도 있지만 Mockito가 가장 많이 사용된다.
https://www.jetbrains.com/lp/devecosystem-2021/java/
Mockito 활용
application에서 database, 외부 API, 다른 자바 api를 계속 사용하면, 매번 외부 시스템을 접근해서 사용할 수 없기 때문에
이를 Mock으로 만들고
-> 어떻게 동작할지를 Mockito를 사용해서 코딩하고
-> 실제 외부 시스템을 이용했을 때 어떻게 작동할지 가정하고
-> 이를 검증, 체크할 수 있다.
단위테스트 (Unit test)에 대한 고찰
Mockito를 쓰려면 단위테스트에 대한 고찰을 할 수 밖에 없는데, 마틴 파울러의 '유닛 테스트'의 정의에 언급 되어 있다.
https://martinfowler.com/bliki/UnitTest.html
bliki: UnitTest
Unit Tests are focused on small parts of a code-base, defined in regular programming tools, and fast. There is disagreement on whether units should be solitary or sociable.
martinfowler.com
모든 의존성을 끊고 단위테스트를 강하게 해야한다고 하는 사람도 있지만, 이 부분에 집착할 필요는 없다고 한다.
- 단위를 행동의 단위로 생각하고, 다른 부분도 같이 테스트되어도 된다..
- 팀 내에서 단위테스트의 단위를 정하면 좋고, 어느 정도까지 Mock을 사용할 지 결정.
굉장히 중요한 논의다. 현재 우리 팀은 Unit테스트를 강하게 할 필요가 있고, 그런 의미에서 Test 적용을 이번 분기 목표로 잡았을 정도다. 다른 팀원들이 Unit테스트를 너무 안 하다보니 계속 다른 파트에 피해를 주고, 우리 파트 내에서도 수정을 하면 기존 기능이 안 되는 사례들이 발생했다(...) 같이 개발하고 적용까지 하는 내 입장에서 환장할 따름...
이런 경우에는 단위테스트를 강하게 하는 편이 좋다고 생각한다. 기존 회사들에서는 테스트가 기본이었기 때문에 이런 고찰을 해본 적이 없었다. 회사마다 다른 환경에 놓여있다보니 적절히 커뮤니케이션하며 협의하는 것이 중요하다.
2. Mockito 사용
- Mock을 만드는 방법
- Mock stubbing : Mock이 어떻게 동작해야 하는지 관리하는 방법
- Mock의 행동을 검증하는 방법
으로 진행하면 된다.
Mockito 레퍼런스
- https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html
Mockito - mockito-core 5.3.1 javadoc
Latest version of org.mockito:mockito-core https://javadoc.io/doc/org.mockito/mockito-core Current version 5.3.1 https://javadoc.io/doc/org.mockito/mockito-core/5.3.1 package-list path (used for javadoc generation -link option) https://javadoc.io/doc/org.m
javadoc.io
1) Mockito 추가
스프링 부트 2.2버젼 이상은 기본적으로 spring-boot-start-test에 자동 추가되어있다.
스프링 부트 쓰지 않는다면, 의존성 직접 추가한다. mockito-core와 mockito-junit-jupiter 동시에 필요
- mockito-core : 모키토 기본 기능
- mockito-junit-jupiter : JUnit test에서 모키토를 사용할 수 있도록 하는 확장 구현체
(현 시점에서 5.3.1까지 나왔으나 스프링부트 3.3에서 4.8.1을 지원해서 해당 버젼으로 적음)
- Maven
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>4.8.1</version>
<scope>test</scope>
</dependency>
- Gradle
testImplementation 'org.mockito:mockito-core:4.8.1
testImplementation 'org.mockito:mockito-junit-jupiter:4.8.1'
2) Mock 객체 생성
우선 Robot과 그 안에 들어가는 Part로 구성된 객체들을 가정해서 생성해본다.
- Robot 클래스 : Part만 일단 추가. (나중엔 List형태가 되겠지만)
public class Robot {
private String name = "unused";
private RobotStatus status = RobotStatus.AWAITING;
private int timeout;
private Part part;
- Robot 서비스 : 기등록한 파츠를 로봇에 등록한 후 조립된 새 로봇을 만든다는 가정.
public class RobotService {
private final PartService partService;
private final RobotRepository repository;
public RobotService(PartService partService, RobotRepository repository) {
// assert를 사용해서 null 체크
assert partService != null;
assert repository != null;
this.partService = partService;
this.repository = repository;
}
public Robot createNewRobot(Long partId, Robot robot){
Optional<Part> part = partService.findById(partId);
robot.setPart(part.orElseThrow(()-> new IllegalArgumentException("Parts doesn't exist for id :" + partId)));
return repository.save(robot);
}
}
- Robot Repository : 기본
public interface RobotRepository extends JpaRepository<Robot, Long> {
}
- Part와 PartService
: 아직 어떤 파츠가 들어올지 모르고, 어떤 로직으로 구현해야할지 모르므로 둘다 interface로 둔다.
public interface Part {
}
public interface PartService {
Optional<Part> findById(Long partId);
}
이 때 테스트를 위해서 Part와 PartService를 구현할 수 없으니, Mock을 이용해서 임시 객체를 생성할 수 있다.
(1) Mockito.mock() : 메소드로 만드는 방법
: mock 메소드를 이용해서 구현할 수 있다.
public class RobotServiceTest {
@Test
void createRobotService(){
// 1. mock 메소드를 이용해서 구현
PartService partService = mock(PartService.class);
RobotRepository robotRepository = mock(RobotRepository.class);
RobotService robotService = new RobotService(partService, robotRepository);
assertNotNull(robotService, "Robot Service cannot be null");
}
}
(2) @Mock 애노테이션으로 만드는 방법
- 클래스에 @ExtendWith(MockitoExtension.class)를 싸주고, @Mock 어노테이션으로 해당 객체들을 표시한다.
@ExtendWith(MockitoExtension.class)
public class RobotServiceTest {
@Mock
PartService partService;
@Mock
RobotRepository robotRepository;
@Test
void createRobotService(){
// 1. mock 메소드를 이용해서 구현
// RobotRepository robotRepository = mock(RobotRepository.class);
// PartService partService = mock(PartService.class);
RobotService robotService = new RobotService(partService, robotRepository);
assertNotNull(robotService, "Robot Service cannot be null");
}
}
여러 클래스를 사용할 때는 어노테이션 방식이 확실히 편리하다.
(3) 메소드 파라미터에 어노테이션 사용
class에 @ExtendWith(MockitoExtension.class)를 붙이고, 파라미터로 @Mock 객체를 받는다.
개인적으로 가장 선호하는 방식이다.
@Test
void createRobotService(@Mock PartService partService, @Mock RobotRepository robotRepository){
RobotService robotService = new RobotService(partService, robotRepository);
assertNotNull(robotService, "Robot Service cannot be null");
}
3) Mock Stubbing (동작 관리)
Stubbing이 별 것은 아니고 그저 특정 값을 셋팅해서 결과값을 조절하는 방법이다.
(1) 행동
우선 모든 Mock 객체는 기본적으로 아래처럼 행동한다.
- Null을 리턴.
- Optional 타입은 Optional.empty 리턴 - Primitive 타입 : 기본 Primitive 값.
- boolean -> false, inteager -> 0 - 콜렉션 : 빈 콜렉션.
- Void 메소드 : 예외x, 아무 일도 발생x
public interface PartService {
Optional<Part> findById(Long partId);
void validate(Long partId);
}
void를 반환하는 메소드를 만들고, 테스트에 실행해보면 아무런 일도 일어나지 않는다.
// 2. Mock Stubbing
@Test
void createRobotService(@Mock PartService partService, @Mock RobotRepository robotRepository){
partService.validate(1L); // void를 반환하는 메소드는 아무 일도 일어나지 않음. 예외x
}
(2) Argument matchers
- Mock 개체에 내가 설정한 값에 대해서만 해당 행동을 보이도록 하는 것. 자료값만 맞으면 같은 결과값을 낸다.
- when().thenReturn : 값 반환
- when().thenThrow() : 예외 반환
- doThrow().when() : 예외 반환
@Test
void createRobotService(@Mock PartService partService, @Mock RobotRepository robotRepository){
RobotService robotService = new RobotService(partService, robotRepository);
Part part = new Part(1L, "wheel");
when(partService.findById(1L)).thenReturn(Optional.of(part));
Robot robot = new Robot(1000, "robot1");
robotService.createNewRobot(1L, robot);
}
여기서 내가 partId로 1L을 넣든, 2L을 넣는 것은 상관이 없지만, 1L을 넣었을 때만 원하는 값을 반환한다.
- 예외를 던지게도 할 수 있고, Void 메소드 특정 매개변수를 받거나 호출된 경우에 예외를 줄 수도 있다.
when(partService.findById(2L)).thenThrow(new RuntimeException()); // 예외 발생
doThrow(new IllegalArgumentException()).when(partService).validate(3L); // void 값이 호출 되었을 떄, 예외 발생
예외가 발생할 경우 assertThrows를 이용해서 확인할 수 있다 .
@Test
void createRobotService(@Mock PartService partService, @Mock RobotRepository robotRepository){
// partService.validate(1L); // void를 반환하는 메소드는 아무 일도 일어나지 않음. 예외x
RobotService robotService = new RobotService(partService, robotRepository);
Part part = new Part(1L, "wheel");
when(partService.findById(1L)).thenReturn(Optional.of(part));
doThrow(new IllegalArgumentException()).when(partService).validate(1L); // void 값이 호출 되었을 떄, 예외 발생
assertThrows(IllegalArgumentException.class, ()-> {
partService.validate(1L);
});
}
- 동일 매개변수로 여러번 메소드 호출 시, 결과를 다르게 할 수도 있다.
@Test
void createRobotService2(@Mock PartService partService){
Part part = new Part(1L, "wheel");
// 1번 정상, 2번 예외, 3번 빈값이 출력되도록 함.
when(partService.findById(any()))
.thenReturn(Optional.of(part))
.thenThrow(new RuntimeException())
.thenReturn(Optional.empty());
assertEquals("wheel", partService.findById(1L).get().getCode());
assertThrows(RuntimeException.class, () -> partService.findById(2L));
assertEquals(Optional.empty(), partService.findById(3L));
}
테스트 2, 3번의 2L, 3L은 아무 값이나 넣어도 상관 없다.
아래처럼 when 문을 사용해서 원하는 값이 나오도록 하여 Mock 테스트를 할 수 있다.
// 2-3 Mock Stubbing
@Test
void createRobotService3(@Mock PartService partService, @Mock RobotRepository robotRepository){
RobotService robotService = new RobotService(partService, robotRepository);
Robot robot = new Robot (1500, "test robot");
Part part = new Part(1L, "LiDAR");
// 해당 stubbing이 되어있어야지만 테스트가 성공함
when(partService.findById(1L)).thenReturn(Optional.of(part));
when(robotRepository.save(robot)).thenReturn(robot);
// 로봇에 파츠 셋팅
robotService.createNewRobot(1L, robot);
assertEquals(part, robot.getPart());
}
4) Mock 객체 확인 (행동 검증) - verifying
Mock 객체가 어떻게 사용이 됐는지 확인할 수 있다.
- 특정 메소드가 특정 매개변수로 몇번 호출 되었는지 (Verifying exact number of invocations)
- 최소 한번은 호출 됐는지
- 전혀 호출되지 않았는지 - 어떤 순서대로 호출했는지 (Verification in order)
- 특정 시간 이내에 호출됐는지 (Verification with timeout)
- 특정 시점 이후에 아무 일도 벌어지지 않았는지 (Finding redundant invocations)
public Robot createNewRobot(Long partId, Robot robot){
Optional<Part> part = partService.findById(partId);
robot.setPart(part.orElseThrow(()-> new IllegalArgumentException("Parts doesn't exist for id :" + partId)));
robot = repository.save(robot);
partService.notify(robot); // verify 테스트용
partService.notify(part.get()); // verify 테스트용
partService.findById(partId); // verify 테스트용
return robot;
}
로봇 생성 메소드에 테스트용 3줄을 추가해준다.
verify를 써서 verify를 써서 몇 번이상 호출되었는지, 호출되지 않았는지 확인할 수 있다.
void verifyMock(@Mock PartService partService, @Mock RobotRepository robotRepository){
RobotService robotService = new RobotService(partService, robotRepository);
Robot robot = new Robot (1500, "test robot");
Part part = new Part(1L, "LiDAR");
// 해당 stubbing이 되어있어야지만 테스트가 성공함
when(partService.findById(1L)).thenReturn(Optional.of(part));
when(robotRepository.save(robot)).thenReturn(robot);
// 로봇에 파츠 셋팅
robotService.createNewRobot(1L, robot);
assertEquals(part, robot.getPart());
// 3. 객체 검증 : createNewRobot에서 partService가 쓰인 횟수, 순서 등등
verify(partService, times(1)).notify(robot); // 1번 이상 호출했는지
verify(partService, times(1)).notify(part); // 한번도 불리지 않은 것
// verify(partService, times(1)).validate(1L); // 한번도 불리지 않은 것
verify(partService, never()).validate(any()); // 전혀 호출되지 않아야함
}
또 notify가 3번 불렸는데, 1번만 verify한 후 verifyNoMoreInteractions(서비스명)를 써서 이후에 인터랙션이 일어났는지 아닌지도 확인할 수도 있다.
verify(partService, times(1)).notify(robot); // 1번 이상 호출했는지
verifyNoMoreInteractions(partService); // 더는 partService가 호출되면 안됨 - 그러나 호출 된 것이 있어서 테스트 실패
뒤에 2번 더 인터렉션이 있기 떄문에 테스트가 실패한다.
또한 InOrder를 써서 메소드 호출 순서대로 확인도 가능하다
// 객체 검증 순서 확인 - 불린 순서대로 verify (순서 바꾸면 에러)
InOrder inOrder = inOrder(partService);
inOrder.verify(partService).notify(robot);
inOrder.verify(partService).notify(part);
만약 순서를 바꾸면 테스트에 실패한다.
특정 시간 내에 수행되었는지도 확인할 수있다
verify(partService, timeout(1).times(1)).notify(robot);
3. Mock BDD 스타일 API
BDD는 Behaviorl driven development로, 어플리케이션을 행동 중심으로 구성하는 방법이다. (TDD에서 창안)
- 행동에 대한 스펙은 아래와 같다, 스펙을 창안하고 진행한다.
- Title
- Narrative(As a / I want / so that)
- As a : 역할
- I want : 테스트 내용
- Acceptance criteria
- Given : 주어진 정보, 상황들
- When : 실행 시점
- Then : 결과 확인 (verifying)
- Mockito는 BddMockito 클래스를 사용한다.
@ExtendWith(MockitoExtension.class)
public class BddTest {
// RobotServiceMockTest의 verifyMock과 비교해보면서
@Test
void createRobotTest(@Mock PartService partService, @Mock RobotRepository robotRepository){
// Given
RobotService robotService = new RobotService(partService, robotRepository);
Part part = new Part(1L, "wheel");
Robot robot = new Robot(1000, "robot1");
// Mockito의 when은 사실상 주어진 정보(Given)이라 표현이 좀 맞지 않는다.
// 따라서 when을 사용하는 대신 Given을 쓰는 것이 보기 좋다.
given(partService.findById(1L)).willReturn(Optional.of(part));
given(robotRepository.save(robot)).willReturn(robot);
// when(partService.findById(1L)).thenReturn(Optional.of(part));
// when(robotRepository.save(robot)).thenReturn(robot);
/* When */
// 로봇에 파츠 셋팅
robotService.createNewRobot(1L, robot);
// Verify -> Then
then(partService).should(times(1)).notify(robot);
// then(partService).shouldHaveNoMoreInteractions(); // 더이상 인터렉션이 없는지 = verifyNoMoreInteractions(partService);
}
}
이 때 Mockito와 BDD에서 조금 맞지 않는 부분이 있는데
- Mockito의 when
Mockito의 when은 사실상 주어진 정보이기 때문에, 실행일 이야기 하는 when에는 맞지 않는다.
따라서 given().willReturn()문으로 바꿔준다.
- Mockito의 verify
verify의 검증 부분은 then으로 써주면 명확하다.
- 참고
- https://javadoc.io/static/org.mockito/mockito-core/3.2.0/org/mockito/BDDMockito.html
- https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#BDD_behavior_verification
출처 : '더 자바, 애플리케이션을 테스트하는 다양한 방법'(백기선), 자료&16~23강
'Java & Spring' 카테고리의 다른 글
[Java Test] 6. ArchUnit 아키텍처 테스트 (0) | 2023.05.26 |
---|---|
[Java Test] 5. Chaos Monkey 운영 이슈 테스트 (0) | 2023.05.25 |
[Java Test] 4. Jmeter 성능테스트 (0) | 2023.05.25 |
[Java Test] 1. JUnit5 (4) properties, 확장, 마이그레이션 (0) | 2023.05.22 |
[Java Test] 1. JUnit5 (3) 테스트 인스턴스 & 순서 지정 (0) | 2023.05.19 |
[Java Test] 1. JUnit5 (2) 테스트 필터링, 테스트 반복 (1) | 2023.05.19 |
[Java Test] 1. JUnit5 (1) (0) | 2023.05.18 |
[DDD] 도메인 주도 개발 - (3) 리포지토리 & 모델 구현 (0) | 2023.02.21 |