스프링 부트 테스트코드 작성하기

Seunghyeon·2025년 5월 9일
post-thumbnail

테스트코드는 왜 작성하는거지?

그냥 서비스 하나 만들었으면 Postman으로 쭉 테스트 해보면 되는거 아닌가?

라고 생각했다.

하지만 테스트코드의 장점은 이 뿐 만이 아니다.

만약 이전에 완성했던 코드를 수정했을 때, 또 다시 모든 경우를 하나하나 직접 테스트 해봐야 한다.

Postman 테스트는 사람이 직접 하는거다 보니 실수가 생길 수 있다. 이런 경우를 대비하여
테스트코드를 작성해두고 발생할 수 있는 문제의 경우의 수를 코드로 작성해 두는 것이다.

JUnit5

  • JUnit 5는 Java 애플리케이션에서 단위 테스트(Unit Test)를 수행하기 위해 사용되는 테스트 프레임워크

모듈화 구조로 인한 유연성

  • JUnit 5는 크게 3개의 모듈로 구성됩니다:

    • JUnit Platform: 테스트 실행 및 결과 보고 (테스트 실행 환경)
    • JUnit Jupiter: JUnit 5의 핵심 모듈, 실제 테스트 작성 (테스트 시 사용되는 대부분의 모듈들)
      ex) @Test, @BeforeEach, @AfterEach, @Nested, @DisplayName
    • JUnit Vintage: JUnit 4와 호환성을 제공

    JUnit5을 통해 만들어진 테스트 환경속에서 테스트를 진행하게 된다.

AssertJ

AssertJ를 통해 내 테스트코드를 검증하자.

JUnit5 에도 Assertions 클래스가 제공 된다. 하지만 AssertJ를 따로 추가해서 사용하는것이 더 가독성이 좋다. AssertJ는 함수형 프로그래밍 패러다임으로 만들어졌기 때문이다.

  • JUnit5의 Assertions
    @Test
    void testEquals() {
        String actual = "Hello";
        String expected = "Hello";
        assertEquals(expected, actual);
    }
  • AssertJ
    @Test
    void testEquals() {
        String actual = "Hello";
        assertThat(actual).isEqualTo("Hello");
    }

무엇을 검사할지 명확하게 지정하고, 검사 방법까지 다음 메서드로 호출하기 때문에 차례대로 읽기가 쉽다.


Mockito

단위 테스트를 위해서 사용되는 mock 객체 라이브러리이다.
가짜 객체를 만들어주는 역할을 한다.

  1. 텅 빈 가짜 객체를 만들어 주고(의존성 주입) 테스트 할 코드를 실행하는데 문제가 없도록 만들어 준다.

  2. mock 객체 내부의 메서드를 사용해야 한다면 정의를 해줘야 사용이 가능하다.
    ('파라미터는 무엇이고' 그 파라미터가 들어왔을 때 , '어떤걸 반환한다 혹은 throw 한다')

  3. 해당 테스트에서 mock 객체를 만들어두고 사용되지 않는다면 이것또한 찾아준다.

  4. 메서드 호출 횟수를 검증할 수 있다.



		@Test
        public void 정상동작의_경우() {

			// given: Mock 객체 생성, 테스트 해볼 동작을 설정
            User mockAdminUser= User.builder()
                    .id(1L)
                    .authCode("adminUser authCode")
                    .role(User.Role.ROLE_ADMIN)
                    .email("admin@google.com")
                    .type(OAuth2Type.GOOGLE)
                    .nickname("운영자")
                    .password("admin password")
                    .registerDate(LocalDateTime.now())
                    .build();

            BoardNotice mockNotice = BoardNotice.builder()
                    .id(1L)
                    .title("notice Write title")
                    .content("notice Write content")
                    .views(123)
                    .type(BoardType.NOTICE)
                    .thumbnail("thumbnail url")
                    .status(BoardStatus.PUBLISHED)
                    .user(mockAdminUser)
                    .createdAt(LocalDateTime.now())
                    .publishedAt(LocalDateTime.now())
                    .updatedAt(null)
                    .deletedAt(null)
                    .imageToken("notice image token")
                    .build();

            
            Long noticeId = 1L;

            BoardAdminRequest.Notice request = new BoardAdminRequest.Notice();
            request.setTitle("title");
            request.setBoardType("NOTICE");
            request.setImageSrc(List.of("img1", "img2"));
            request.setThumbnail("thumbnail");
            request.setContent("content");
            request.setImageToken("imageToken");

            AuthUser mockAuthUser = new AuthUser(new UserDto(mockAdminUser.getId(), mockAdminUser.getEmail(), mockAdminUser.getNickname(), mockAdminUser.getAuthCode(), mockAdminUser.getRegisterDate(), mockAdminUser.getType(), mockAdminUser.getRole()));

            when(userRepository.findById(any(Long.class))).thenReturn(Optional.of(mockAdminUser));
            when(noticeBoardRepository.save(any(BoardNotice.class))).thenAnswer(invocation ->
            {
                BoardNotice boardNotice = invocation.getArgument(0);

                try {
                    Field idField = BoardNotice.class.getDeclaredField("id");
                    idField.setAccessible(true);
                    idField.set(boardNotice, noticeId);
                } catch (NoSuchFieldException | IllegalAccessException e) {
                    throw new RuntimeException("필드 수정 오류", e);
                }

                return boardNotice;
            });
            when(boardNoticeMapper.toNewNoticeId(any(Long.class)))
                    .thenReturn(new BoardAdminResponse.newNotice(mockNotice.getId()));

            // when: 실제 테스트 하고싶은 메서드
            BoardAdminResponse.newNotice result = boardAdminService.writeNotice000(request, mockAuthUser);

            // then: 테스트 메서드 동작 후 검증 단계
            assertThat(result).isNotNull();
            assertThat(result.getNoticeId()).isEqualTo(mockNotice.getId());

            verify(userRepository).findById(mockAuthUser.getUserDto().id());
            verify(boardImageService).checkAndSave(eq(request.getImageSrc()), eq(request.getImageToken()));
            verify(boardNoticeMapper).toNewNoticeId(mockNotice.getId());

        }

테스트 코드는 given, when, then 으로 나누어서 작성하는 방식을 따른다.

'given' 에서는 테스트 환경에 필요한 사전 구성 작업이다.
여기서 mock 객체에 대한 설정들을 해주고 mock 내부 메서드를 사용한다면 채워줘야 한다.

'when' 에서는 우리가 만들어놓은 가상의 환경에서 테스트 하고싶은 메서드를 실행한다.

'then' 에서는 테스트 후 결과를 검증하는 단계이다.
위 코드에서는 assertThat(AssertJ) 이랑 verify(Mockito)를 통해서 검증하고 있다.

assertThat은 값을 직접 확인하고, verify 는 메서드 호출(몇번 호출했는지, 어떤 인자가 들어갔는지)에 대한 검증을 한다.


05.15 추가)

테스트코드를 작성하면서 spy 객체라는것을 접하게 되었다.
(사용하지는 않았음.)

spy

java 에서 primitive 타입으로 int, long 이런것들이 있다.
이러한 값들을 하나의 객체로서 사용하기 위해서 우리는
Wrapper 클래스를 통해 각 primitive 값들을 감싸서 객체로 사용할 수 있게 된다.

여기서 마치 Wrapper 클래스와 비슷한 역할을 한다.
spy는 우리가 만든 '실제 객체'를 감싸서 'mock 객체' 로 만들어 주는 역할을 한다.
이러한 짓을 왜하는거지? 싶지만

실제 객체의 대부분의 기능을 사용하고 싶은데
하나의 기능만 mocking 해서 사용하고 싶을때


실제 객체의 부분적인 mocking 이 필요할 때 사용한다.


내가 spy를 사용하게 된 경위는
아직 테스트코드에 대해서 익숙하지 않아서였다.

mockito 에서 제공하는 verify 라는 메서드가 있다.

이는 'mock 객체'의 특정 메서드가

  1. 호출 되었는지?
  2. 몇 회 호출 되었는지?
  3. 호출 시 특정 파라미터로 호출 되었는지?

를 확인할 수 있다.


왜 안쓰는거야

verify 메서드를 실제 객체를 대상으로 사용하려고 했고 에러가 발생했다.

그래서 이를 해결하기 위해 단순히 spy로 감싸서 사용했지만

엔티티 즉 POJO 객체(순수한 도메인 로직)에는 spy를 사용하지 않는것이 원칙이다.

굳이 해당 객체를 spy로 감싸지 않고도 검증할 수 있기 때문이다.

-> 무조건! 꼭! 내가 verify를 통해서 메서드 호출 횟수와, 파라미터를 검증해야겠다~ 고 한다면
spy로 감싸서 확인할 수 있다.


하지만 테스트 코드에서 중요하다고 생각하는건 쓸데없는 모킹, 쓸데없는 검증 은 지양해야한다.

안그래도 많은 mocking 과정이 필요한데 검증하지 않아도 되는 검증까지 해버리는 코드가 들어간다면 코드를 읽는데도 어려울게 당연하기 때문

결론: 내가 이 코드에서 정확하게 이것을 검증해야 한다. 라는 것을 명확히 하고 테스트 코드를 작성할 필요가 있다.


테스트코드를 작성안하면 불안에 떠는 개발자들이 많다고 하더라...

나는 여태껏 테스트코드를 많이 안써봤지만 앞으로 꼼꼼하게 작성해봐야겠다.

끝!

profile
그냥 합니다.

0개의 댓글