[Spring Boot] JUnit과 Mockito

우리 모두 알다시피, 단위 테스트는 애플리케이션 개발에서 유지해야 할 주요한 중요한 관행 중 하나입니다. 이 글에서는 Java 애플리케이션의 단위 테스트 프레임워크인 JunitMockito의 Rest apis에 대해 살펴보겠습니다.

단위 테스트란 무엇인가요?

단위 테스트(Unit Testing)는 애자일 방법론의 연속 프로세스 중 하나로 알려져 있습니다. 단위 테스트는 개발자 수준에서 시작됩니다. 단위 테스트는 소프트웨어 애플리케이션의 개별 구성 요소를 테스트하는 소프트웨어 테스트의 한 유형입니다.

Rest API를 작성한다고 가정하면 각 API에 대해 테스트 케이스를 작성하고 메인 API에 커밋하기 전에 애플리케이션에서 테스트할 수 있습니다. 단위 테스트에서는 서비스를 모의 테스트하므로 매번 데이터베이스를 호출하지 않고 모의 데이터를 사용합니다.

JUnit이란 무엇인가요?

JUnit은 Java 프로그래밍 언어의 단위 테스트 프레임워크입니다. 테스트 중심 개발에서 중요한 역할을 하며 xUnit으로 알려진 단위 테스트 프레임워크 제품군으로 알려져 있습니다.

JUnit은 개발자에게 먼저 테스트한 다음 코딩하는 아이디어를 제공하므로 실제 API를 구현하기 전에 어떤 유형의 데이터가 필요한지 확인할 수 있습니다.

JUnit을 사용하면 프로그래머의 생산성이 향상되고 애플리케이션의 안정성이 유지됩니다.

JUnit은 테스트 케이스 작성을 위한 프레임워크이므로 테스트 방법을 식별하는 어노테이션을 제공하고 예상 결과를 테스트하기 위한 어설션을 제공합니다.

Mockito 프레임워크란 무엇인가요?

Mockito는 Java 애플리케이션의 단위 테스트에 사용되는 Java 기반 Mocking 프레임워크입니다. Mockito는 MIT 라이선스에 따라 오픈 소스 테스트 프레임워크로 출시되었습니다.

Mockito는 내부적으로 Java 리플렉션 API를 사용하여 모의 객체를 생성합니다. Mockito 프레임워크 사용의 주된 목적은 외부 종속성을 모의하고 테스트 코드에서 이를 사용하여 개발을 간소화하는 것입니다. 또한 JUnit 및 TestNG와 같은 다른 테스트 프레임워크와 함께 Mockito를 사용할 수도 있습니다.

Mocking 이란 무엇인가요?

Mocking은 객체의 모의 또는 복제 역할을 하는 객체를 개발하는 프로세스입니다. 즉, 실제 객체를 사용하는 대신 Mocking된 객체를 사용하게 됩니다. 모의 객체는 특정 또는 더미 입력과 출력을 제공합니다.

단계별 구현

Step 1

Spring initialize를 사용하여 Spring Boot 애플리케이션을 만들고 다음 종속성을 추가합니다.

  • Spring Web
  • Spring Data JPA
  • Lombok

Spring 부팅 프로젝트를 zip 파일로 생성하고 압축을 푼 다음 IDE로 가져옵니다.

Step 2

Spring 부팅 프로젝트에 아래 종속성을 추가했는지 확인합니다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>

Step 3: JPA 엔티티 생성

이제 다음 내용으로 Employee JPA 엔티티를 생성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import lombok.*;

import javax.persistence.*;

@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder

@Entity
@Table(name = "employees")
public class Employee {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;

@Column(name = "first_name", nullable = false)
private String firstName;

@Column(name = "last_name", nullable = false)
private String lastName;

@Column(nullable = false)
private String email;
}

상용구 코드를 줄이기 위해 Lombok 어노테이션을 사용하고 있다는 점에 유의하세요.

Step 4: Repository Layer 생성

이제 JpaRepository 인터페이스를 확장하는 EmployeeRepository를 생성합니다.

1
2
3
4
5
6
import net.jUnitApplication.springboot.model.Employee;
import org.springframework.data.jpa.repository.JpaRepository;

public interface EmployeeRepository extends JpaRepository<Employee, Long> {

}

Step 5: Service Layer 생성

CRUD 메서드를 사용하여 EmployeeService 인터페이스를 만듭니다.

1
2
3
4
5
6
7
8
9
10
11
12
import net.jUnitApplication.springboot.model.Employee;

import java.util.List;
import java.util.Optional;

public interface EmployeeService {
Employee saveEmployee(Employee employee);
List<Employee> getAllEmployees();
Optional<Employee> getEmployeeById(long id);
Employee updateEmployee(Employee updatedEmployee);
void deleteEmployee(long id);
}

이제 EmployeeService 인터페이스를 구현하는 EmployeeServiceImpl 클래스를 생성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import net.jUnitApplication.springboot.exception.ResourceNotFoundException;
import net.jUnitApplication.springboot.model.Employee;
import net.jUnitApplication.springboot.repository.EmployeeRepository;
import net.jUnitApplication.springboot.service.EmployeeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Optional;

@Service
public class EmployeeServiceImpl implements EmployeeService {

private EmployeeRepository employeeRepository;

public EmployeeServiceImpl(EmployeeRepository employeeRepository) {
this.employeeRepository = employeeRepository;
}

@Override
public Employee saveEmployee(Employee employee) {

Optional<Employee> savedEmployee = employeeRepository.findByEmail(employee.getEmail());
if(savedEmployee.isPresent()){
throw new ResourceNotFoundException("Employee already exist with given email:" + employee.getEmail());
}
return employeeRepository.save(employee);
}

@Override
public List<Employee> getAllEmployees() {
return employeeRepository.findAll();
}

@Override
public Optional<Employee> getEmployeeById(long id) {
return employeeRepository.findById(id);
}

@Override
public Employee updateEmployee(Employee updatedEmployee) {
return employeeRepository.save(updatedEmployee);
}

@Override
public void deleteEmployee(long id) {
employeeRepository.deleteById(id);
}
}

Step 6: JUnit 5와 Mockito를 사용하여 Service Layer 단위 테스트하기

EmployeeService에 대한 단위 테스트 케이스를 작성해 보겠습니다. Spring 기능을 사용하지 않고도 EmployeeService에 대한 단위 테스트를 작성할 수 있어야 합니다.

Mock을 사용하여 EmployeeRepository의 모의 인스턴스를 생성하고 모의 EmployeeRepository 인스턴스를 사용하여 EmployeeServiceImpl 인스턴스를 생성하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import net.jUnitApplication.springboot.exception.ResourceNotFoundException;
import net.jUnitApplication.springboot.model.Employee;
import net.jUnitApplication.springboot.repository.EmployeeRepository;
import net.jUnitApplication.springboot.service.impl.EmployeeServiceImpl;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.willDoNothing;
import static org.mockito.Mockito.*;

import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Collections;
import java.util.List;
import java.util.Optional;

@ExtendWith(MockitoExtension.class)
public class EmployeeServiceTests {

@Mock
private EmployeeRepository employeeRepository;

@InjectMocks
private EmployeeServiceImpl employeeService;

private Employee employee;

@BeforeEach
public void setup(){
employee = Employee.builder()
.id(1L)
.firstName("ko")
.lastName("su")
.email("hgko@gmail.com")
.build();
}

// JUnit test for saveEmployee method
@DisplayName("JUnit test for saveEmployee method")
@Test
public void givenEmployeeObject_whenSaveEmployee_thenReturnEmployeeObject(){
// given - precondition or setup
when(employeeRepository.findByEmail(employee.getEmail()))
.thenReturn(Optional.empty());

when(employeeRepository.save(employee)).thenReturn(employee);

System.out.println(employeeRepository);
System.out.println(employeeService);

// when - action or the behaviour that we are going test
Employee savedEmployee = employeeService.saveEmployee(employee);

System.out.println(savedEmployee);
// then - verify the output
assertThat(savedEmployee).isNotNull();
}
}

getAllEmployees 메서드에 대한 JUnit 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@DisplayName("JUnit test for getAllEmployees method")
@Test
public void givenEmployeesList_whenGetAllEmployees_thenReturnEmployeesList(){
// given - precondition or setup
Employee employee1 = Employee.builder()
.id(2L)
.firstName("ko")
.lastName("test")
.email("test@gmail.com")
.build();

when(employeeRepository.findAll()).thenReturn(List.of(employee,employee1));

// when - action or the behaviour that we are going test
List<Employee> employeeList = employeeService.getAllEmployees();

// then - verify the output
assertThat(employeeList).isNotNull();
assertThat(employeeList.size()).isEqualTo(2);
}

getEmployeeById 메서드에 대한 JUnit 테스트

1
2
3
4
5
6
7
8
9
10
11
12
@DisplayName("JUnit test for getEmployeeById method")
@Test
public void givenEmployeeId_whenGetEmployeeById_thenReturnEmployeeObject(){
// given
when(employeeRepository.findById(1L)).thenReturn(Optional.of(employee));

// when
Employee savedEmployee = employeeService.getEmployeeById(employee.getId()).get();

// then
assertThat(savedEmployee).isNotNull();
}

updateEmployee 메서드에 대한 JUnit 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@DisplayName("JUnit test for updateEmployee method")
@Test
public void givenEmployeeObject_whenUpdateEmployee_thenReturnUpdatedEmployee(){
// given - precondition or setup
when(employeeRepository.save(employee)).thenReturn(employee);
employee.setEmail("lee@gmail.com");
employee.setFirstName("Lee");
// when - action or the behaviour that we are going test
Employee updatedEmployee = employeeService.updateEmployee(employee);

// then - verify the output
assertThat(updatedEmployee.getEmail()).isEqualTo("lee@gmail.com");
assertThat(updatedEmployee.getFirstName()).isEqualTo("Lee");
}

deleteEmployee 메서드에 대한 JUnit 테스트

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@DisplayName("JUnit test for deleteEmployee method")
@Test
public void givenEmployeeId_whenDeleteEmployee_thenNothing(){
// given - precondition or setup
long employeeId = 1L;

doNothing().when(employeeRepository).deleteById(employeeId);

// when - action or the behaviour that we are going test
employeeService.deleteEmployee(employeeId);

// then - verify the output
verify(employeeRepository, times(1)).deleteById(employeeId);
}

위의 예제 코드는 Junit과 Mockito를 사용하여 REST API의 CRUD 동작 테스트를 제공합니다.

Share