인프런 ‘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이랑 무슨 차이지? 같은 내용같은데?’라는 의문이 들 수 있다.
https://martinfowler.com/articles/mocksArentStubs.html
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로의 분리는 중복 제거가 아니라 도메인이다. 중복 제거로 접근하면 좋지 못한 테스트로 이어질 수 있다.
'인프런 워밍업 스터디' 카테고리의 다른 글
인프런 워밍업 스터디 3기 후기 (0) | 2025.04.08 |
---|---|
[3주차 발자국] 스프링과 테스트 코드 (0) | 2025.03.24 |
[인프런 워밍업 스터디] 2주차 발자국: Readable Code 적용기 (0) | 2025.03.17 |
[인프런 워밍업 스터디] 추상과 객체 지향의 구체화 (0) | 2025.03.09 |
[미션 Day 4] 코드 리팩토링 및 나만의 언어로 작성한 SOLID 원칙 (0) | 2025.03.07 |