인프런 워밍업 스터디 클럽 2기 백엔드(클린코드&테스트코드) 발자국 - 4주차

인프런 워밍업 스터디 클럽 2기 백엔드(클린코드&테스트코드) 발자국 - 4주차

인프런 워밍업 클럽 2기, 백엔드(클린코드&테스트코드) 과정에 참여하고 있습니다.

마지막 주차인 이번 4주차에는 Layered Architecture 의 각 레이어별 역할과 실무에 가까운 테스트 방법들을 배워보는 시간을 가졌습니다.

강의 링크: Readable Code: 읽기 좋은 코드를 작성하는 사고법

 


[학습 요약]

 

Layered Architecture

  • Persistence Layer

    • Data Access의 역할을 한다.

      • ~~Repository

    • 비즈니스 가공 로직이 포함되어서는 안된다.

      • Data에 대한 CRUD에만 집중한 레이어

  • Business Layer

    • 비즈니스 로직을 구현하는 역할

    • Persistence Layer와의 상호작용(Data를 읽고 쓰는 행위)을 통해 비즈니스 로직을 전개시킨다.

    • 트랜잭션을 보장해야 한다.

      • 작업단위에 대한 원자성

  • Presentation Layer

    • 외부 세계의 요청을 가장 먼저 받는 계층

    • 파라미터에 대한 최소한의 검증을 수행한다.

MockMvc

  • Mock: 가짜, 대역의 의미를 갖고 있다.

  • Mock(가짜) 객체를 사용해 스프링 MVC 동작을 재현할 수 있는 프레임워크

 

Test Double

테스트를 수행하기 위한 대역 역할을 하는 객체들을 칭하는 표현

  • Dummy

    • 아무것도 하지 않는 깡통 객체

  • Fake

    • 단순한 형태로 동일한 기능은 수행하나, 프로덕션에서 쓰기에는 부족한 객체 (ex. FakeRepository)

  • Stub

    • 테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체.

    • 그 외에는 응답하지 않는다.

  • Spy

    • Stub 이면서 호출된 내용을 기록하여 보여줄 수 있는 객체.

    • 일부는 실제 객체처럼 동작시키고 일부만 Stubbing 할 수 있다.

  • Mock

    • 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체.

Stub과 Mock의 차이

  • Stub은 상태 검증(State Verififation) 을 위해 사용

  • Mock은 행위 검증(Behavior Vefirification) 을 위해 사용

 

순수 Mockito로 검증해보기

@ExtendWith(MockitoExtension.class)
class MailServiceTest {

    @Spy
    private MailSendClient mailSendClient;

    @Mock
    private MailSendHistoryRepository mailSendHistoryRepository;

    @InjectMocks
    private MailService mailService;

    @DisplayName("메일 전송 테스트")
    @Test
    void sendMail() {
        // given

        // 1)
        doReturn(true)
            .when(mailSendClient)
            .sendEmail(anyString(), anyString(), anyString(), anyString());

        // 2)
        doNothing()
            .when(mailSendClient)
            .a();

        // when
        boolean result = mailService.sendMail("", "", "", "");

        // then
        assertThat(result).isTrue();
        verify(mailSendHistoryRepository, times(1)).save(any(MailSendHistory.class)); // 3)
    }
}
  • @Spy 객체는 Mockito 의 메서드가 아니라 Stubber 메서드를 이용해서 Stubbing 을 수행해야 한다.

  • 1) MailSendClient 의 .sendEmail() 메서드만 Stubbing 했고, 나머지는 실제 구현 코드를 사용한다.

  • 2) 이렇게 하면 .a() 도 Stubbing 했으므로, 동작을 안하게 됨.

  • 3) verify() 를 이용해서 Mock 객체의 여러가지 행위를 검증할 수 있다.

 

BDDMockito

Mockito 를 한번 더 감싸서, 동일한 기능을 제공하되 BDD 스타일로 작성된 메서드를 사용할 수 있도록 도와주는 라이브러리이다.

// Before

// given
Mockito.when(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))
	.thenReturn(true);


// After

// given
BDDMockito.given(mailSendClient.sendEmail(anyString(), anyString(), anyString(), anyString()))  
    .willReturn(true);

Mockito를 사용하게 되면, given 절에 when() 이라는 이름의 메서드를 사용해야 된다. 이는 부자연스럽게 느껴질 수 있으므로, BDDMockito 를 사용하면 동일하게 when() 동작을 수행하지만 이름은 given() 인 메서드를 사용할 수 있다.

 

 

더 나은 테스트를 작성하기 위한 구체적 조언

 

한 문단에 한 주제

  • 테스트 코드는 문서의 역할을 한다.

    • 그러니, 글쓰기를 한다는 마음 가짐으로 테스트를 작성하자.

 

완벽하게 제어하기

  • 현재시간, 랜덤값 등을 외부에서 주입받도록 DI 구조로 리팩토링 하여, given 데이터를 완벽하게 제어할 수 있도록 한다.

  • 이메일 발송과 같은 외부 세계와 소통하는 기능은 Mocking 을 통해 제어할 수 있도록 한다.

 

테스트 환경의 독립성을 보장하자

  • 한 테스트 메서드에서 두가지 이상의 기능을 테스트하지 말자

    • 논리적인 사고가 한번 더 필요해지므로, 가독성에 안좋은 영향을 준다.

    • when/then 절의 assert 구문을 호출하기 전에 given 에서 예외가 발생할 수도 있다.

  • given절에 사용할 데이터를 만들때는 가급적이면 순수한 생성자 또는 Builder 를 통해 생성하는 것이 좋다.

    • 생성 과정에 검증이 포함되어 있는 Factory 메서드 패턴을 사용하면, 생성 과정에서 예외가 발생할 수도 있음.

 

테스트 간 독립성을 보장하자

  • 테스트간에는 순서라는 개념이 없어야 한다.

  • 각각 독립적으로, 언제 어떤 순서로 어떻게 수행되든 항상 같은 결과를 내야만 한다.

 

한 눈에 들어오는 Test Fixture 구성하기

  • Fixture: 고정물, 고정되어 있는 물체

  • 테스트를 위해 원하는 상태고정시킨 일련의 객체

  • Fixture를 생성하는 코드를 메서드로 분리시킨 다면,

    • 이 테스트에서 필요한 파라메터만 넘길 수 있도록 메서드 내부에서 기본값을 하드코딩으로 설정해주는 방법이 가독성 향상에 좋다.

@BeforeEach, @BeforeAll

  • 중복 코드를 줄이기 위해 사용하는 기능이지만, 각 테스트간 결합이 생기게 만든다는 맹점이 존재함.

  • 각 테스트 입장에서 봤을 때, 아래 두가지 항목을 만족하면 BeforeEach 절에 사용해도 괜찮다.

    • 아예 몰라도 테스트 내용을 이해하는 데에 문제가 없는가?

    • 수정해도 모든 테스트에 영향을 주지 않는가?

data.sql

  • 테스트 클래스 코드가 아닌, 쿼리로 데이터를 미리 셋업하도록 처리하는 방식은 데이터를 셋업하는 코드의 파편화로 인해, 유지보수 포인트가 증가하고 추후 중복코드를 만들어낼 수도 있는 등 프로젝트의 복잡도만 높이는 행위이므로 지양하는 것이 좋다.

 

Test Fixture 클렌징

  • .deleteAll() vs .deleteAllInBatch()

    • `.deleteAll()` : 지울 테이블을 먼저 select 하고, 데이터 건수만큼 delete 쿼리의 where 절에 key값을 포함한 쿼리가 요청됨.

    • `.deleteAllInBatch()` : 냅다 delete All 쿼리를 요청해버림.

    • 그럼 .deleteAll()은 왜 사용??

      • .deleteAll() 은 모든 연관관계를 먼저 찾은 다음 삭제를 해주기 때문에, 외래키 같은 제약 조건을 고려해서 삭제해준다.

      • 어지간하면 .deleteAllInBatch()를 사용하자.

 


[후기] 

이로써 총 4주간의 워밍업 클럽 일정이 마무리 되었습니다. 지금까지는 인프런 강의를 구매만 해놓고 수강을 미뤄놓기만 해왔는데, 우연한 기회에 알게된 워밍업 클럽 덕분에 밀도있게 학습을 할 수 있는 경험을 겪어본 것이 가장 좋았던 점이었습니다.

실무에서도 테스트를 적극적으로 작성하지 않는 상황이 정말 많아, 테스트를 어떻게 하면 더 잘 작성할 수 있는지에 대한 의문점이 항상 있었습니다. 그런 저에게 실용적인 테스트를 작성할 수 있게 실무에 가까운 예제를 알려준 이번 강의가 정말 많은 도움이 되었습니다.

좋은 강의를 준비해주신 박우빈 강사님께 감사드리고, 워밍업 클럽이라는 시스템을 마련하고 운영해주신 인프런 운영진분께도 감사드립니다.

댓글을 작성해보세요.

채널톡 아이콘