[4주차 발자국] 보다 더 나은 테스트를 위해
이번 주차는 시간이 많이 부족했던 관계로, 강의를 통한 느낀점 위주로 작성하겠습니다.
인프런 ‘Readable Code: 읽기 좋은 코드를 작성하는 사고법’을 수강한 후, 작성한 내용입니다.
📌 강의 내용
Presentation Layer
기존에 프로젝트에서 Presentation Layer 테스트는 건너뛴 경우가 많았다. 대부분 Business Layer까지만 테스트를 작성했고 사실 어떻게 Presentation Layer 테스트를 작성해야할지 잘 몰라서 안했던 것도 컸다,
이번 강의를 통해 MockMVC을 사용하여 Presentation Layer 테스트에 대해 익힐 수 있었고, 프로젝트에도 적용해봐야겠다.
Mock을 마주하는 자세
Mock은 주로 테스트 하는 대상에 대해 집중하기 위해 사용한다. 예를 들어, 외부 클라이언트에 의존하는 기능이 있을 때 실제 이 기능을 사용하면서까지 테스트할 필요는 없다. 이러한 상황에서 Mock을 사용하게 된다.
우리는 주로 테스트를 작성할 때 BDD 스타일로 작성하게 되는데, Mock을 사용하게 되면 given 절에 when().thenReturn()을 사용하게 된다. 이러한 상황에 BDDMockito를 사용하게 되면 given 절에서 given().thenReturn() 형식으로 훨씬 자연스럽게 구성할 수 있다.
Mock을 사용하는 관점에서 Classicist와 Mockist가 존재한다. 간단하게 말하면, Classicst는 Mock 사용을 최소하하여 꼭 필요한 경우에만 사용하고, Mockist는 따로따로 다 테스트를 할 수 있기에 보장된 기능은 Mocking 처리를 통해 빠르게 테스트를 한다는 입장이다.
두 관점에 대해 확실한 정답은 없다. 소프트웨어의 안정성을 고려하면 Classicst의 입장의 손을 들 수 있다. 프로덕션 코드에서 런타임 시점에 일어날 일을 정확하게 Stubbing 하는 것은 확실하지 않고 모르는 일이다.
더 나은 테스트를 작성하기 위한 구체적 조언
테스트도 하나의 코드다. 테스트를 작성할 때에도 클린 코드에서 배웠던 내용을 상기시키면서 작성해보자. 또한 테스트 코드에서 테스트하기 어려운 영역이 있다면, 이 것이 리팩토링 또는 영역 분리의 신호는 아닌지 생각하자.
또한 @BeforeEach 등으로 테스트마다 중복되는 코드를 분리할 수도 있는데, 중복된다고 모두 분리하는 것은 아니어야 한다. 중복이라고 느껴져서 분리하면 테스트에 대한 이해에 걸림돌이 될 수 있다. 예를 들어, 댓글에 관한 테스트인데 댓글 생성을 @BeforeEach로 분리하면, 테스트를 이해하기에 어려울 수 있다.
테스트를 확인하기 위해 모든 테스트를 한꺼번에 돌리면, 스프링이 여러 번 띄워지는 경우를 확인할 수 있다. 이는 테스트 마다 설정이 달라서 그런 것인데, 테스트 환경을 통합하여 스프링 서버가 띄워지는 횟수를 줄여 더 빠르게 테스트하고 확인할 수 있게 하자.
private 메서드 테스트는 어떻게 하나요?
private 메서드 테스트는 할 필요는 없고 해서도 안된다.
만약 그런걸 느낀다면, 객체를 분리할 시점인지 고민해보자!
테스트에서만 필요한 메서드가 생겼는데, 프로덕션 코드에서는 필요없다면..?
만들어도 되지만, 보수적으로 접근하자.
어떤 객체가 마땅히 가져도 될만한 행위이고, 미래에도 충분히 사용될 수 있는 성격의 메서드 정도는 가능하다.
Appendix
새로운 라이브러리를 사용하다보면, 기능에 익숙하지 않다. 보통 검색을 통해 해당 라이브러리를 익히거나 하는데 이떄 테스트를 활용할 수 있다. 테스트 코드를 통해 라이브러리에 대한 행동을 정의하고 검증하는 과정을 통해 구체적인 동작과 기능을 학습할 수 있다.
테스트 코드로 API 문서를 작성할 수 있다. Spring REST Docs인데, 사실 전에도 많이 들어봤던 이름이다. 하지만 Swagger가 간단하고 편하게 작성할 수 있으므로 자주 사용했는데, Swagger를 사용해보면 프로덕션 코드가 지저분해진다. Presentation Layer에 Swagger 관련 코드가 붙으면서 지저분해지면서 단점을 크게 느낀 경험이 있다. 다음엔 Spring REST Docs를 꼭 활용하자.
📌 미션
레이어 아키텍처의 테스트
영속성 레이어
영속성 레이어?
DB에 접근하는 계층
비즈니스 로직이 들어가지 않은 순수하게 데이터에 대한 처리 및 조회를 수행
영속성 레이어의 테스트
무엇을 확인해야할까?
원하는 데이터에 정확히 접근하는지
쿼리가 길어졌을 때, 내가 원하는 데이터에 맞게 작성되었는지
어떻게 테스트를 해야할까?
영속성 계층이 의존하는 계층이 대부분 상황에서 없기 때문에 단위 테스트 형식으로 진행
비즈니스 레이어
비즈니스 레이어?
비즈니스 로직이 전개되는 계층
영속성 레이어가 사용된다.
도메인 개념이 적용
트랜잭션 개념 적용
비즈니스 레이어의 테스트
하나의 트랜잭션을 보장하는지 확인
비즈니스 로직이 정확히 수행되는지 확인
여러 케이스에 대해 테스트하자.
프레젠테이션 레이어
프레젠테이션 레이어란?
외부 세계와 가장 가까운 계층
요청과 관련한 데이터를 받는다.
요청에 대한 데이터를 전달한다.
프레젠테이션 레이어의 테스트
요청에서 건너온 값들에 대한 검증
도메인 규칙을 제외한 간단한 검증
상황에 대한 정확한 응답이 반환되는지 확인
@Mock, @MockBean, @Spy, @SpyBean, @InjectMocks
Mock과 Spy의 차이
Mock: 행위에 대한 기대를 명세하고, 그에 따라 동작하도록 만들어진 객체
Spy: Stub이면서 호출된 내용을 기록하여 보여줄 수 있는 객체
일부는 실제 객체처럼 동작시키고 일부만 Stubbing할 수도 있다.
Stub이란?
테스트에서 요청한 것에 대해 미리 준비한 결과를 제공하는 객체. 그 외에는 응답하지 않는다.
여기서 그러면 ‘Mock이랑 Stub이랑 무슨 차이지? 같은 내용같은데?’라는 의문이 들 수 있다.
stub은 상태 검증, mock은 행위 검증과 관련되어 있다.
public interface MailService {
public void send (Message msg);
}
public class MailServiceStub implements MailService {
private List<Message> messages = new ArrayList<Message>();
public void send (Message msg) {
messages.add(msg);
}
public int numberSent() {
return messages.size();
}
}
// 테스트
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
MailServiceStub mailer = new MailServiceStub();
order.setMailer(mailer);
order.fill(warehouse);
assertEquals(1, mailer.numberSent());
}
Stub은 메일을 몇 번 보냈는지를 나타내는 상태를 검증한다.
// 테스트
public void testOrderSendsMailIfUnfilled() {
Order order = new Order(TALISKER, 51);
Mock warehouse = mock(Warehouse.class);
Mock mailer = mock(MailService.class);
order.setMailer((MailService) mailer.proxy());
mailer.expects(once()).method("send");
warehouse.expects(once()).method("hasInventory")
.withAnyArguments()
.will(returnValue(false));
order.fill((Warehouse) warehouse.proxy());
}
}
Mock은 행위를 중심으로 검증한다.
@Mock, @MockBean
@Mock
어노테이션이 붙은 객체를 Mock 객체로 생성
@MockBean
어노테이션이 붙은 객체를 Mock 객체로 생성하고, Spring Context에 등록된 빈을 Mock 객체로 대체한다.
@Spy, SpyBean
@Spy
어노테이션이 붙은 객체를 실제 객체 기반으로 만들지만, 일부 기능만 Stubbing 할 때 사용
@SpyBean
해당 객체를 Spy 객체로 생성하고, Spring Context에 등록된 빈을 Spy 객체로 대체한다.
@InjectMocks
@InjectMocks 어노테이션이 붙은 객체가 필요로하는 필드 중 @Mock으로 생성된 객체를 주입해준다.
BDD 스타일로 테스트 코드 배치하기
아래 3개의 테스트가 있다.
@BeforeEach
void setUp() {
❓
}
@DisplayName("사용자가 댓글을 작성할 수 있다.")
@Test
void writeComment() {
1-1. 사용자 생성에 필요한 내용 준비
1-2. 사용자 생성
1-3. 게시물 생성에 필요한 내용 준비
1-4. 게시물 생성
1-5. 댓글 생성에 필요한 내용 준비
1-6. 댓글 생성
// given
❓
// when
❓
// then
검증
}
@DisplayName("사용자가 댓글을 수정할 수 있다.")
@Test
void updateComment() {
2-1. 사용자 생성에 필요한 내용 준비
2-2. 사용자 생성
2-3. 게시물 생성에 필요한 내용 준비
2-4. 게시물 생성
2-5. 댓글 생성에 필요한 내용 준비
2-6. 댓글 생성
2-7. 댓글 수정
// given
❓
// when
❓
// then
검증
}
@DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
@Test
void cannotUpdateCommentWhenUserIsNotWriter() {
3-1. 사용자1 생성에 필요한 내용 준비
3-2. 사용자1 생성
3-3. 사용자2 생성에 필요한 내용 준비
3-4. 사용자2 생성
3-5. 사용자1의 게시물 생성에 필요한 내용 준비
3-6. 사용자1의 게시물 생성
3-7. 사용자1의 댓글 생성에 필요한 내용 준비
3-8. 사용자1의 댓글 생성
3-9. 사용자2가 사용자1의 댓글 수정 시도
// given
❓
// when
❓
// then
검증
}
내용을 살펴보고, 각 항목을 @BeforeEach, given절, when절에 배치한다면 어떻게 배치해야 할까?
(@BeforeEach에 올라간 내용은 공통 항목으로 합칠 수 있습니다. ex. 1-1과 2-1을 하나로 합쳐서 @BeforeEach에 배치)
✔ 게시판 게시물에 달리는 댓글을 담당하는 Service Test
✔ 댓글을 달기 위해서는 게시물과 사용자가 필요하다.
✔ 게시물을 올리기 위해서는 사용자가 필요하다.
직접 코드 배치해보기
@BeforeEach
void setUp() {
사용자 생성에 필요한 내용 준비
사용자 생성
사용자의 게시물 생성에 필요한 내용 준비
사용자의 게시물 생성
}
@DisplayName("사용자가 댓글을 작성할 수 있다.")
@Test
void writeComment() {
// given
댓글 생성에 필요한 내용 준비
// when
댓글 생성
// then
검증
}
@DisplayName("사용자가 댓글을 수정할 수 있다.")
@Test
void updateComment() {
// given
댓글 생성에 필요한 내용 준비
댓글 생성
// when
댓글 수정
// then
검증
}
@DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
@Test
void cannotUpdateCommentWhenUserIsNotWriter() {
// given
사용자2 생성에 필요한 내용 준비
사용자2 생성
사용자1의 댓글 생성에 필요한 내용 준비
사용자1의 댓글 생성
// when
사용자2가 사용자 1의 댓글 수정 시도
// then
검증
}
공통된 준비 작업을
@BeforeEach
로 분리댓글을 달기 위한 조건 작업
게시물을 작성할 사용자 생성
게시물 생성
given
검증하고자 하는 행동에 필요한 작업 수행
댓글 생성과 관련된 내용은 테스트마다 관리할 수 있도록 given 절에 배치
when
검증하고자 하는 행동
이번 중간 점검에서 Day 18에 대한 공통 피드백이 있었다.
핵심은 중복 제거가 아닌 도메인
사용자, 게시물은 간접적이므로 setUp()으로, 댓글은 직접적이므로 given절
@BeforeEach로의 분리는 중복 제거가 아니라 도메인이다. 중복 제거로 접근하면 좋지 못한 테스트로 이어질 수 있다.
댓글을 작성해보세요.