Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
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
Tags
more
Archives
Today
Total
관리 메뉴

우당탕탕 개발일지

[Spring] JUnit + Mockito 테스트 코드 작성 본문

Spring

[Spring] JUnit + Mockito 테스트 코드 작성

YUDENG 2024. 9. 27. 02:24

최근 개발 중인 프로젝트에서 Postman을 통해 API를 호출하여 테스트를 진행했지만, 유저 데이터를 수작업으로 입력하다 보니 입력 실수나 불완전한 데이터로 인해 테스트 결과가 일관되지 않거나 예상과 다르게 나오는 경우가 발생하였다. 위와 같은 문제를 해결하기 위해 테스트 코드를 도입하게 되었다.

 

단위 테스트(Unit Test)란, 하나의 모듈을 기준으로 독립적으로 진행되는 가장 작은 단위의 테스트이다. 여기서 모듈은 하나의 기능 또는 메서드이다. 단위 테스트는 해당 부분만 독립적으로 테스트하기 때문에 문제점을 빠르게 확인할 수 있다. 이러한 이유로 최근에는  TTD (Test-Driven-Development) 라는 테스트 주도 개발이 많이 사용되고 있다. 일반적으로 개발 흐름이 코드를 작성한 후 테스트를 진행하는 것이라면, TDD는 먼저 테스트 케이스를 작성하고 이를 통과할 수 있는 코드를 작성하는 방식이다. 테스트 케이스를 작성하는 시간이 오래 걸리는 만큼, 버그를 줄일 수 있고 요구사항 변동에도 유연하게 대처가 가능하다.

 

JUnit

자바를 위한 테스팅 프레임워크이다. 오랜 기간 동안 많은 개발자들에 의해 사용되어 왔기 때문에 안정성과 신뢰성이 검증된 프레임워크이다.

 

JUnit의 가장 기본적인 규칙은 다음과 같다.

  • 메서드가 public으로 선언되어야 한다.
  • @Test라는 어노테이션을 메서드 앞에 붙여준다.

 

JUnit 동작 방식

 

1. 테스트 클래스 로드

2. 테스트 인스턴스 생성

3. BeforeEach 실행

4. 테스트 메서드 실행

5. AfterEach 실행

6. 결과 기록

7. 테스트 종료

 

@Test 어노테이션으로 메서드를 호출할 때마다 새 인스턴스를 생성함으로써 독립적으로 테스트가 가능하다. 테스트가 종료되면 실행했던 객체는 삭제된다. 또한, @BeforeEach@AfterEach 어노테이션을 사용하면 각각의 테스트 메서드마다 적용되어 테스트 전후 처리가 가능하다.

JUnit 테스트 작성 과정

JUnit 테스트를 작성하는 데 있어 두 가지 패턴이 있다.

 

(1) Given-When-Then

  • Given: 테스트에 필요한 초기 조건 설정
  • When: 테스트에서 실행할 동작 정의
  • Then: 예상되는 결과를 검증

 

(2) Arrange-Act-Assert (AAA)

  • Arrange: 테스트에 필요한 모든 준비 작업 수행
  • Act: 테스트 대상 메서드를 호출하거나 동작 수행
  • Assert: 결과가 예상한 대로인지 확인

 

표현 방식만 다를 뿐, 테스트 코드에서 수행하는 작업은 동일하다. Given-When-Then 방식은 BDD(Behavior Driven Development)에서 사용되고, 비즈니스 논리에 더 중점을 둔다. Arrange-Act-Assert 방식은 앞서 말한 TDD 관점에서 사용된다. 앞으로 소개할 테스트 코드는 TDD 관점에 맞게 AAA 패턴으로 작성하였다.


Mockito

단위 테스트를 위한 자바 모킹 프레임워크이다. JUnit에서 가짜 객체인 Mock 객체를 생성해주고 관리하고 검증할 수 있도록 지원해주는 프레임워크이다. 스프링 부트 2.2 버전 이상부터는 프로젝트 생성시 자동으로 의존성이 추가된다고 한다.

 

Mockito 동작 방식

 

1. Mock 객체 생성

2. Stub 설정

3. 동작 검증 (Verify)

4. 예외 처리

 

JUnit과 Mockito를 함께 사용하면 직접적인 DB 호출 없이 Mock Object라는 가짜 객체를 구성해 테스트를 진행할 수 있다. 가짜 객체이기 때문에 실제 객체처럼 동작하지만, DB에 영향을 끼치지 않는다. 

Stub 설정

Mock 객체의 특정 메서드가 호출되었을 때, 어떤 동작을 할 지 설정한다.

when(categoryRepository.findById(1L)).thenReturn(java.util.Optional.of(category));

 

categoryRepository . findById () 메서드는 호출되었을 때, 실제 DB 호출 대신 미리 정의한 category 객체를 반환한다.

 

 

동작 검증

verify() 메서드를 사용하면 메서드가 의도대로 호출이 되었는지 검증할 수 있다.

verify(budgetRepository, times(1)).save(existingBudget);

 

budgetRepository.save() 메서드가 정확히 한 번 호출되었는지 검증한다.


테스트 코드 작성

 

1. 라이브러리 추가

dependencies {
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

 

 

2. 테스트 클래스 작성

public class BudgetServiceTest {
    @InjectMocks
    private BudgetService budgetService;

    @Mock
    private BudgetRepository budgetRepository;

    @Mock
    private CategoryRepository categoryRepository;

    @Mock
    private CustomUserDetails customUserDetails;
    
    @Mock
    private User user;

    @BeforeEach
    public void setUp() {
        MockitoAnnotations.openMocks(this);
        when(customUserDetails.getUser()).thenReturn(user);
        System.out.println("Test Before");
    }

    @AfterEach
    public void after(){
        System.out.println("Test After");
    }
    
}

 

Spring Boot에서 테스트 코드는 src/test/java 패키지에 작성한다. @InjectMocks 어노테이션은 의존성 주입을 위해 사용된다. 주입되는 객체들은 Mockito의 @Mock을 통해 Mock 객체로 주입이 된다.

 

3. 테스트 시나리오 작성

 

작성할 테스트 시나리오는 다음과 같다.

 

(1) 예산 설정 기능 (setBudget)

- 유저가 예산 설정을 요청할 때 기존 데이터가 존재하면, 해당 데이터를 업데이트 하는지 확인

- 기존 데이터가 없는 경우 새로운 데이터를 생성하는지 확인

 

(2) 예산 추천 기능 (suggestBudget)

- 유저들이 입력한 데이터들을 바탕으로 카테고리 별 평균을 계산하는 로직이 잘 동작하는지 확인

- 값이 10% 이하인 카테고리들이 "기타"로 묶여 값을 반환하는지 확인

 

[ setBudget ]

    @Test
    public void testSetBudget_updatesBudget(){
        // Arrange
        Category category = new Category(1L, "식비");
        SetBudgetRequest request = new SetBudgetRequest(1L, 50000L, LocalDate.now());
        Budget existingBudget = new Budget(1L, 40000L, LocalDate.now(), user, category);

        when(categoryRepository.findById(1L)).thenReturn(java.util.Optional.of(category));
        when(budgetRepository.findByUserAndCategory(user, category)).thenReturn(null);

        // Act
        budgetService.setBudget(customUserDetails, request);

        // Assert
        verify(budgetRepository, times(1)).save(existingBudget);
        assertEquals(50000L, existingBudget.getAmount());
    }

    @Test
    public void testSetBudget_createsBudget() {
        // Arrange
        Category category = new Category(1L, "식비");
        SetBudgetRequest request = new SetBudgetRequest(1L, 50000L, LocalDate.now());

        when(categoryRepository.findById(request.getCategory())).thenReturn(Optional.of(category));
        when(budgetRepository.findByUserAndCategory(user, category)).thenReturn(null);

        // Act
        budgetService.setBudget(customUserDetails, request);

        // Assert
        ArgumentCaptor<Budget> budgetCaptor = ArgumentCaptor.forClass(Budget.class);
        verify(budgetRepository).save(budgetCaptor.capture());

        Budget savedBudget = budgetCaptor.getValue();
        assertEquals(50000L, savedBudget.getAmount());
    }

 

 

[ suggestBudget ]

    @Test
    public void testSuggestBudget() {
        // Arrange
        SuggestBudgetRequest request = new SuggestBudgetRequest(1000000L); // 총 예산 설정

        // 가짜 데이터 설정
        List<Object[]> avgBudgets = new ArrayList<>();
        avgBudgets.add(new Object[]{"식비", 40.0});
        avgBudgets.add(new Object[]{"주거", 30.0});
        avgBudgets.add(new Object[]{"교통", 8.0});
        avgBudgets.add(new Object[]{"취미", 7.0});

        when(budgetRepository.findAverageBudgetByCategory()).thenReturn(avgBudgets);

        // Act
        List<BudgetResponse> responses = budgetService.suggestBudget(request);

        // Assert
        assertEquals(3, responses.size());

        for (BudgetResponse response : responses) {
            switch (response.getCategory()) {
                case "식비":
                    assertEquals(400000L, response.getAmount());
                    break;
                case "주거":
                    assertEquals(300000L, response.getAmount());
                    break;
                case "기타":
                    assertEquals(150000L, response.getAmount());
                    break;
            }
        }
    }

 

 

 

이런식으로 실패한 테스트와 성공한 테스트를 볼 수 있다.

728x90

'Spring' 카테고리의 다른 글

[Spring] 스케줄링+ Discord webhook  (1) 2024.10.06
[Spring] Redis를 활용한 토큰 관리 최적화  (2) 2024.09.26
[Spring] API 공통 응답  (0) 2024.09.20
[Spring] Swagger - API 명세서 작성  (1) 2024.09.15
[Spring] JWT 서비스  (0) 2024.07.11