Spring Test

김띵규·2025년 2월 22일

테스트를 작성하는 이유는?

테스트 코드를 작성하는 이유가 뭐라고 생각하시나요?
블레이버스 해커톤에서 팀원으로 만난 현직 개발자분께 받은 질문이다. 내가 생각하기에는 프로젝트에서 필요한 기능을 구현했을 때 테스트 코드를 통해서 비즈니스 로직의 동작이 제대로 이루어지는지 확인할 수 있기 때문이었다. 내 대답을 들은 팀원분은 본인의 생각도 이야기 해주셨는데 다음과 같은 이야기였다.

"테스트 코드 작성의 큰 목표는 방금 말한 대로 비즈니스 로직의 동작을 확인하기 위함이다. 만약 공통 로직에 수정이 필요해졌지만 테스트 코드가 없다면 코드 수정 후에 일일이 API 호출을 통해서 확인해야 한다. 엮여있는 코드가 많다면 훨씬 번거로운 작업이 될 것이다. 그렇기에 테스트 코드를 작성하는 것에 필요한 시간이 불필요한 것 같더라도 전체적인 프로세스를 생각할 때 더 효율적이다."

"추가로 테스트 코드를 작성하는 건 협업의 이유도 있다. 서비스의 비즈니스 로직을 구현하는 개발자는 본인이 어떤 의도로 작성한 코드인지 명확하게 알고 있지만 다른 개발자는 그렇지 않다. 하지만 직접 작성한 코드의 유지보수를 언제까지 직접 담당할 수 있겠나. 모종의 이유로 담당자가 바뀌게 되는 경우가 부지기수다. 그런 경우에 후임 개발자는 사전에 작성된 테스트 코드를 보고 최초 코드 작성자의 개발 의도를 파악할 수 있고, 코드 수정 후에도 일관성을 유지할 수 있다는 장점이 있다."

이런 내용의 조언을 들을 수 있었다. 막연히 테스트 코드를 작성하면 좋다고는 생각하지만 귀찮다고만 생각하고 있었기에 정곡을 찔렸고, 해커톤 기간 중에 테스트 코드를 작성하면서 진행해보자는 생각을 하게 되었다.


Spring Boot Test

@SpringBootTest, @Transactional을 사용하면 테스트를 진행하고 DB의 상태를 테스트 진행 전으로 롤백할 수 있다. 보통 이런 방식으로 진행하는 것은 통합 테스트(Integration Test)이다. 통합 테스트의 경우 Spring을 통해서 빈을 띄우는 과정이 필요하기에 상대적으로 시간이 오래 걸리는 편이다. 하지만 간단한 validation 테스트 같은 경우에도 동일한 방식으로 테스트를 진행하는 것은 비효율적이기 때문에 단위 테스트(Unit Test)를 따로 진행하게 된다. 뿐만 아니라 DB에 접근하는 사용자가 여러 명인 경우에 DB에 데이터를 넣고 롤백하는 등의 과정을 수행하는 동안 충돌이 발생할 수 있다는 위험도 있다.
이번 포스트에서는 단위 테스트에 대해서 중점적으로 다루어 보겠다.


MOCKITO

Mockito는 테스트에 필요한 Mock 객체를 생성, 검증, 스터빙(Creation, Verification, Stubbing)해주는 라이브러리이다. 하지만 이번 해커톤 테스트 코드 작성에서는 Mockito 라이브러리를 사용하지 않았기 때문에 해당 라이브러리를 사용하고 싶다면 직접 찾아보도록 하자.

❓ 해커톤에서는 이렇게 편한 라이브러리를 왜 사용하지 않았을까?
Mockito 라이브러리를 사용해서 테스트 코드를 작성하면 라이브러리에 의존성을 가지게 된다. 만약 이렇게 테스트 코드를 작성했는데 Mockito 라이브러리의 버전이 업그레이드 되었을 때 사용한 메서드가 Deprecated 된다면 테스트 코드에도 필연적으로 수정이 필요해진다. 하지만 지속적으로 수정하지 않는다면 버전 업그레이드를 따라갈 수 없고 영원히 레거시 코드로 남게 된다. 그렇기에 Mockito 라이버러리를 사용하지 않고 직접 Mock 객체를 만들어서 사용했다.

Mock

Mock 객체를 직접 만들어서 사용했다고 하는데 Mock 객체는 뭐길래 라이브러리를 사용해서 만들거나 직접 만들어야 하는 것일까? Mock 객체는 단어의 뜻처럼 '가짜 객체, 거짓 객체'를 말한다. 작성할 테스트 코드에서는 DB가 아니지만 DB처럼 역할을 수행하는 객체로 예를 들 수 있다.

이번 해커톤에서 테스트 코드를 작성하기 위해서 우선 MockRepository 클래스를 선언했다. Mock 객체를 직접 만들기 위해서 클래스도 직접 선언해야 하는데 이번 해커톤에서 JPA Repository를 상속 받아서 사용했기 때문에 @Override 할 메서드가 꽤나 많다. 하지만 테스트를 위해서 모든 메서드를 구현할 필요는 없기 때문에 테스트 과정에서 필요한 메서드만 구현할 MyMockRepository로 한 번 더 상속을 받는다. 이렇게 하는 이유는 MockRepository에서 모든 메서드를 오버라이딩하기 때문에 MyMockRepository에서는 필요한 메서드만 오버라이딩 할 수 있어서 코드를 확인하기 간편하기 때문이다. 물론 MockRepository만 사용해도 문제는 없다.

MyMockRepository에서는 테스트에 사용할 코드를 작성해주면 된다. 나의 경우에는 테스트 클래스에 static class 형태로 선언해서 구현했다. 이렇게 했을 때 코드를 동시에 볼 수 있다는 점은 좋았지만 테스트 클래스의 코드 길이가 길어지기 때문에 테스트할 메서드가 많다면 따로 분리하는 것이 좋을 것 같다.

	@Test
    void Id로_디자이너_조회() {
        // given
        Designer designer = Designer.builder()
                .id(1L)
                .name("사용자")
                .region(Region.서울전체)
                .address("서울시 중앙대학교")
                .profile("imageUrl")
                .description("테스트 코드 작성 재밌네")
                .offlinePrice(40000)
                .onlinePrice(30000)
                .meetingType(MeetingType.BOTH)
                .portfolio1("example portfolio1")
                .portfolio2("example portfolio2")
                .favoriteCount(0)
                .designerMajors(Collections.emptyList())
                .build();
        designerRepository.save(designer);

        //when
        Designer result = designerService.getDesignerById(1L);

        //then
        assertNotNull(result);
        assertEquals(1L, result.getId());
        assertEquals("사용자", result.getName());
        assertEquals(Region.서울전체, result.getRegion());
        assertEquals("서울시 중앙대학교", result.getAddress());
        assertEquals("imageUrl", result.getProfile());
        assertEquals("테스트 코드 작성 재밌네", result.getDescription());
        assertEquals(40000, result.getOfflinePrice());
        assertEquals(30000, result.getOnlinePrice());
        assertEquals(MeetingType.BOTH, result.getMeetingType());
        assertEquals("example portfolio1", result.getPortfolio1());
        assertEquals("example portfolio2", result.getPortfolio2());
        assertEquals(0, result.getFavoriteCount());
    }

이렇게 given-when-then 형식으로 테스트 코드를 작성할 수 있다. Spring 서버 자체를 띄우지 않기 때문에 기존 소스 코드에서는 save를 호출할 때 직접 DB에 접근해서 저장하고 다시 확인하는 것에 비해서 훨씬 더 빠르게 테스트가 완료되는 것을 확인할 수 있었다.

	@Test
    void 존재하지_않는_디자이너_id로_조회() {
        // given
        Long unExists = 999L;

        // when-then
        assertThrows(DesignerError.class, () -> designerService.getDesignerById(unExists));
    }

이렇게 디자이너 ID로 조회할 때 디자이너가 존재하지 않는 경우에는 적절한 예외 처리가 되는 것도 테스트 코드로 확인할 수 있다.


마무리

  • 테스트 코드 작성의 목적이 비즈니스 로직의 동작 확인도 있지만 협업 과정에서의 의도 전달 역할도 수행할 수 있다는 점을 배우게 된 것이 뜻 깊음.
  • 직접 Mock 객체를 만들어서 테스트 코드를 작성해본 결과 단위 테스트의 구조를 확실히 이해할 수 있었음.
    • 이번에 함께 백엔드를 담당한 동기의 코드를 참고하기 전까지는 Mock 객체를 사용해서 유닛 테스트를 진행하는 것을 어떻게 해야 하는지 잘 몰랐기에 해커톤에서 테스트 코드 작성을 배우고 성장할 수 있었음.
profile
🦥 개발자의 일상과 배움을 나누는 공간, 함께 성장하는 Velog입니다. 🦥

0개의 댓글