Coding/Server

[내일배움캠프] 테스트코드 - 이론

kangplay 2025. 4. 17. 15:08

1. 테스트 코드는 무엇이고, 왜 작성해야할까?

소프트웨어를 테스트한다는 것은 소프트웨어가 기대한대로 잘 동작하는지 확인하는 과정이다. 

아마 소프트웨어를 개발하고 테스트하지 않는 사람은 없을 것이다.

-> 이러한 테스트를 코드로써 하겠다는 것이다.

💪🏻 테스트 코드를 작성함으로써 얻어지는 이점
1. 테스트를 자주할 수 있게 된다.
2. 리팩토링에 심리적인 안정감을 준다.
3. 에러를 조기에 발견할 수 있다.

 

2. 테스트 코드 작성 전에 해야할 것

테스트 코드가 무조건 많은 것보다, 질 좋은 테스트를 작성하는 것에 중요하다.

이를 위해서는, "시나리오"를 잘 작성해야한다.

카카오 로그인이 잘되는지 테스트
1.~
2.~
3.~
(BDD를 코드가 아닌, 글로 작성!)

3. Mocking과 Stubbing: Test Double

Test Double이란 실제 객체의 역할을 흉내내는 테스트용 객체로, 종류는 다음과 같다.

  • Mock 객체: 테스트에서만 사용할 가짜 객체로 설정에 따라서, Dummy, Stub, Spy처럼 동작할 수 있다.
  • Stubbing: Test Double 객체의 메소드가 어떤 응답을 할지 결정해버리는 것
  • Spy: Test Double 객체의 메소드가 호출되었는지, 몇 번 호출 되었는지에 대한 정보를 기록하는 것
  • Fake: Stubbing은 단순 응답만 정해줬다면, Fake는 가짜 로직까지 정해주는 것
    • 예) 데이터 저장 로직을 DB가 아닌 다른 저장소를 만들어 저장 
Java 에서는 Mockito 등의 라이브러리를 활용해 Test Double을 구현할 수 있다.
=> Mock 객체 생성 후, 해당 Mock 객체를 이용해 Stubbing, Spy를 구현할 수 있다.
@ExtnedWith(MockitoExtension.class)
class SocialMemberServiceTest {
	@Mock
    private SocialMemberRepository socilMemberRepository;
    
    //@InjectMocks를 쓰지 않고, new SocialMemberService(socialMemberRepsitory);와 같이 Mock객체를 이용해 직접 생성해도 된다!
    @InjectMocks
    private SocialMemberService socialMemberService;
}​

4. Mock 객체를 이용해 여러가지 시나리오 테스트해보기

1. Stubbing을 이용해 Mock 객체의 동작 정의하기

when(memberRepository.findByEmail(any()).thenReturn(Optional.ofNullable(new Member(..));

2. Spy를 이용해 Mock 객체가 특정 동작을 수행했는지 검증하기

verify(memberRepository, times(1)).findByEmail(any());
//Return Value만 테스트하던 것에서 벗어날 수 있다!

3. Spy + Captor를 이용해 특정 동작을 수행할 때의 파라미터 검증하기

//2번에서 any()가 어떤 이메일인지까지 검증 가능
@Captor
ArgumentCaptor<String> emailCaptor;

verify(memberRepsitory, times(1)).findByEmail(emailCaptor.capture());
String email = emailCaptor.getValue();
//org.assertj.core.api.Assertions.assertThat(Junit에서 제공하는 assert 구문보다 AssertJ의 assert 구문 사용을 추천
assertThat(email).isEqualTo("slolee@naver.com");

4. Stubbing을 이용해 특정 메소드가 동작하지 않도록 만들기

//반환없이 void인 애
doNothing().when(memberRepsitory).findByEmail(any());

5. 지정된 메소드만 Stubbing 하고 나머지는 원본 코드 그대로 사용하기 

@Spy
private MemberRepository memberRepository;

5. Unit Test: 객체별 단위테스트 작성하기

👐 단위테스트 작성 우선순위
- Domain Entity -> Service -> Client -> POJO -> Repository(내가 직접 작성한 쿼리가 아닌 경우, @DataJpaTest를 활용한 SliceTest) -> Controller(상대적으로 많은 로직이 들어가지 않기 때문에 @WebMvcTest를 이용한 통합테스트나 E2E로 대체됨)
- 기본적인 우선 순위는 이렇지만 "예측하기 어렵고 불안한 코드"(반복문이 많거나 복잡한 계산 로직이 있는 경우)가 있으면 반드시 테스트 코드를 우선적으로 작성하는 게 좋다.

객체별 단위 테스트 작성 요령에 대해서 알아보자.

 

1. Domain Entity 등 POJO 

  • 일반적으로 다른 객체를 의존하고 있지 않기 때문에 별도의 Mocking은 필요없다.
  • 가장 가볍고 간단하게, 즉 Spring Boot 없이 순수한 단위 테스트로 구현할 수 있는 테스트다.

2. Service

  • Service는 다른 객체들을 의존하고 있기 때문에 해당 객체들에 대한 Mocking이 필요하다.
  • 만약 DB까지 붙여서 테스트하고 싶으면, 통합테스트를 작성하자.

비즈니스 로직을 설명하는만큼, 단위테스트가 반드시 필요하다. Spy를 활용해 메소드 호출 횟수 검증, 호출 시 파라미터 검증 등을 수행할 수 있다.

3. Client

  • Client는 외부 서비스를 호출하는 특징을 가졌기 때문에 Mock Server를 만들어 테스트해야한다.
  • 간단한 Mock Server인 경우 MockWebServer를 이용하고, 조금 복잡한 동작을 해야하는 Mock Server의 경우 WireMock을 이용할 수 있다.
  • 이를 통해 외부 서비스로부터의 응답 파싱, 외부 서버 예외 발생 시나리오 등을 테스트할 수 있다.

MockWebServer는 인메모리 데이터베이스이기 때문에, 각 테스트가 끝날 때마다 초기화해주는 작업이 필요하다.

6. JPA + H2 Database를 곁들인 통합테스트

  • 통합 테스트라고 다짜고짜 @SpringBootTest를 사용하는 것은 좋지 않다.
  • JPA에서 제공해주는 @DataJPaTest를 이용하자.
    • +) Spring Boot Test에서 Context를 재사용해서 제공하는 방법도 있다. 
  • 만약, MemberService.register()가 실행되면 DB에 회원정보가 잘 저장되는지 확인하고 싶다면?
    • Service - Repository - DB

7. 인수테스트 예시

8. 마무리하며

테스트코드에서 가장 중요한 것은 테스트 시나리오를 잘 작성하는 것이다. 테스트 코드를 보고 읽는 사람이 쉽게 시나리오를 파악할 수 있도록 하자.