현재 이제 프로젝트의 서비스 요구 사항, 기능, DB, Api들을 얼추 작성한 상태고 이제 개발을 들어가야 합니다.
개발을 들어가기 앞서, 요번 프로젝트에서는 TDD를 도입해 보기로 서버 개발자 분들과 야심 찬? 목표를 세워봤습니다.
사실 저 나름대로 TDD에 관해 얼추 알고 있지..라고 생각했었는데 아직 자기 객관화가 한참은 부족하다는 점만 이해했습니다.
TDD(Test Driven Development) 란 코드를 작성하기 이전에 테스트를 먼저 작성하고, 그 테스트를 통과하는 코드를 작성함으로써 테스트된 동작하는 코드를 얻는 개발 방법론입니다. (뭐 코드를 작성하기 이전에 테스트를 해..??)
혼자만의 다짐이었다면, 도망가볼까 생각하겠지만 이미 팀원들과 결정이 났으니까 열심히 TDD를 공부해 보겠습니다. 신난다
TDD
왜 할까요? 꽤 많은 장점이 검색하면 나옵니다. 그런 거는 금방 까먹을게 분명하니까 그럼 여기에는 TDD를 해보면서 직접 느낀 장점을 적어보기로 하죠.
1. test가 확실하고 빠르고 반복가능하다.
물론 아직 CRUD의 C만 TDD로 작성한 상태에서 느낀 장점이지만, 코드로 코드를 검증하는 과정 자체가 굉장히 안정감?을 줍니다. 정신머리가 왔다 갔다 하는 저에게 확실하게 남아있는 테스트 코드는 꽤나 안정적인 기분을 느끼게 해 줬습니다. 또한 이전 API의 검증은 포스트맨 혹은.. 무식하지만 프론트 개발을 통해서 직접 호출하며 해왔는데, 이에 비해 굉장히 빠르고, 더 작은 단위에서 더 확실하게 테스트를 해볼 수 있었습니다.
예를 들어 특정 이벤트 게시물을 생성하는 API구현을 한다고 가정했을 때 이전에는 API를 전부 구현한 다음 포스트맨을 통해서 테스트를 했습니다. 하지만 큰 범위의 테스트는 에러가 발생했을 때 당연히도 큰 범위의 디버깅을 포함합니다. 하지만 현재 테스트의 단위가 굉장히 줄었고, 추후 개발이 더 진행됨에 따라 분명 시간 절약에 도움을 줄 것 같습니다.
2.
추후 느끼면 추가 작성.
개발과정
TDD는 개발은 ''망나니 개발자' 블로그를 참고하여 진행하였습니다. 정말 너무 꼼꼼하신 글 감사합니다. (여기)
제가 맡은 개발 부분은 Board(게시글)의 타입 중 하나인 Event 게시물의 CRUD를 구현하는 것입니다.
(현재 환경은 Java 17, Spring 3.x 를 사용하고 있습니다.)
Repo 계층
Repo 계층의 테스트 코드를 작성하기 전, 어떤 데이터 베이스를 test 환경에서 사용을 해야 하나 고민이 있었습니다.
1. H2 (In memory)
제가 참조한 레퍼런스에서 사용하고 계신 데이터베이스였습니다. 빠르고, 독립적이며, 세팅하기 쉽다는 장점이 있습니다.
단점으로는 기능이 실제 RDB에 비해 제한적일 수 있고 살~짝 작동방식이 다를 수 있다고 합니다.
2. realDataBas
실제 데이터 베이스를 test환경에서 사용하는 방법입니다. 실제 데이터베이스를 사용하다 보니 굉장히 테스트의 신빙성이 올라가겠지만, H2에 비해 굉장히 느리고 데이터 베이스의 관리를 해줘야 합니다.
3. mockDateBase
처음 봤을 때는 얘는 도대체 뭘까라고 생각했지만, 가장 중요한 Service 계층에서 정말 많이 사용하게 되는 데이터베이스 형태였습니다. 근데 얘를 데이터 베이스..라고 부르는 게 맞는지 애매하네요.
-gpt 질문 원본-
1. In-Memory Database (H2)
Pros:
- Fast: Since it's in-memory, read/write operations are very quick.
- Isolated: Tests do not affect each other because the database state is reset between tests.
- Easy to Set Up: Spring Boot has excellent support for H2, and it can be easily configured with a few properties.
Cons:
- Limited Features: H2 might not support all the features your production database does.
- Different Behavior: The behavior of H2 might differ slightly from your production database, leading to some edge cases not being caught.
2. Mock Database
Pros:
- Unit Test Isolation: Helps in writing true unit tests by mocking out the database layer.
- Speed: Very fast as it doesn’t involve actual database operations.
Cons:
- Complexity: Requires writing extensive mock setups.
- Not Realistic: Might not catch issues related to real database interactions (e.g., SQL syntax, data integrity).
3. Real Database
Pros:
- Realistic: Tests interact with the same database system as production, catching more realistic scenarios.
- Feature-Complete: Utilizes all the features of your production database.
Cons:
- Setup Complexity: Requires managing database state, including setting up and tearing down data.
- Speed: Slower than in-memory databases, especially if running in CI/CD pipelines.
- Isolation Issues: Tests might interfere with each other if the database state is not managed properly.
Recommendation
Development Phase:
- Unit Tests: Use mocking to isolate unit tests from the database.
- Integration Tests: Use an in-memory database like H2 for quick and isolated integration tests.
Pre-Production/Staging Phase:
- Use a real database similar to your production environment for end-to-end tests to catch any discrepancies.
Repository 계층 테스트 시 가장 중요한 점은 테스트를 여러 번 실행하거나 각각의 테스트 메서드가 서로 영향을 주지 않아야 한다는 점을 바탕으로 저도 Repo 계층에서 H2를 가지고 테스트 코드를 가지고 작성했습니다.
@DataJpaTest
@TestPropertySource(locations = "classpath:application-test.yml")
public class BoardRepositoryTest {
@Autowired
private BoardRepository boardRepository;
@Test
@DisplayName("이벤트 저장")
void saveEvent(){
//when
final Board board = Board.builder()
.member(member())
.title("test")
.contents("contents")
//TODO: codeTable로 변경
.type("EVENT")
.build();
//given
final Board result = boardRepository.save(board);
//then
assertThat(result.getTitle()).isEqualTo("test");
assertThat(result.getContents()).isEqualTo("contents");
assertThat(result.getType()).isEqualTo("EVENT");
assertThat(result.getMember().getName()).isEqualTo("test");
}
그런데 대부분의 Entity들이
'생성 시간'과 '수정 시간'에 대한 데이터도 가지고 있어 저도 위 둘에 대한 테스트 코드를 작성해 보고 싶었습니다. TDD가 처음이라 눈에 보이는 건 다 테스트해보고 싶은 열정이 보이시죠?
하지만 result 객체의 createTime과 upatedTime은 모두 null 값을 가지고 있었습니다. (충격)
이것저것 해보다.. 우선 문제의 범인을 줄이기 위해서, 테스트 환경의 데이터베이스를 실제 RDB로 변경하여 테스트를 해봤습니다. 실제 RDB로 변경하니까 테스트가 정상적으로 통과가 되었습니다. 그럼 분명 H2 쪽에 어떤 부분에서 문제가 생긴 것 같은데 정말 죄송스럽지만 아직 해결하지 못했습니다. 하지만 아직 포기하지 않았다는 점을 기억해주십쇼..
Service 계층
핵심 비즈니스 로직이 존재하는 Service 계층입니다.
여기서는 다른 클래스에게 의존성을 갖고 있는(Repo) 클래스를 테스트해야 합니다. 의존성을 테스트 환경에서 어떻게 다뤄야 하나 자료 조사 중 제 눈에 들어온 것은 mockito와 Stub 이였습니다. 정말 둘의 차이에 대해 이해가 너어무 안돼서 꽤나 많은 생각을 했습니다.
'mock은 행동을 검증하고, Stub은 상태를 검증합니다.'라는 설명이 mock 객체를 이용하여 Service 계층 테스트 코드를 작성하고 보니 이해가 50% 정도 된 것 같습니다.
mock 객체 사용코드는 아래와 같습니다.
이 코드에서 MemberRepository는 Mock 객체를 통해 findById 메서드에 어떠한 UUID값을 넣어도 특정 에러를 발생하도록 구현되어 있습니다. 당연히 위 과정이 findById의 메서드의 호출 결과에 대한 검증이 되지 않습니다. 당연하게도 위 코드는 Service 계층에 대한 테스트 코드로 findById에 대한 메서드가 적절히 작동하는지에 대해 관심이 없습니다.
위 코드를 보면 행동 검증에 대해 좀 더 확실하게 이해를 해볼 수 있습니다. 이 코드는
1. boardService.save 메서드가 적절한 값을 받았을 때, 적절한 형태의 값을 반환해 주는지
2. boardService.save 메서드가 실행되면서, boardRepo.save 와 memberRepo.findById가 1번씩 잘 실행되는지
위 두 가지를 검증하고 있습니다. boardRepo.save 와 memberRepo.findById가 어떤 값을 반환하는지에 대한 검증이 아닌 boardRepo.save 와 memberRepo.findById가 실행(행동) 되었는지에 대한 검증입니다.
https://martinfowler.com/articles/mocksArentStubs.html
Mocks Aren't Stubs
Explaining the difference between Mock Objects and Stubs (together with other forms of Test Double). Also the difference between classical and mockist styles of unit testing.
martinfowler.com
Stub에 대해서는 아직 이해가 한참 부족하여.. 글을 작성하지는 못하지만 상태를 검증하는 Stub도 가까운 시일 내 만나게 될 것 같습니다.
Controller 계층
사실 테스트 코드를 작성 시 몇 가지 규칙 중 '실패하는 경우 먼저 테스트 코드를 작성한다'에 대해 사실 마음속으로 불신을 갖고 있었습니다. 그래서 자연스럽게 성공 케이스를 먼저 작성했는데 그 이유를 Controller 계층의 테스트 코드를 작성하며 알 수 있었습니다.
우선 Controller 계층의 테스트 환경에 대해 보고 위 주제에 대해 계속 이야기해보겠습니다.
저는 래퍼런스의 방법 + Controller 계층의 테스트 시 주로 사용되는 @WebMvcTest 어노테이션을 사용해보고 싶었습니다.
@WebMvcTest는 기본적으로 컨트롤러와 관련된 빈들만 로드하여 빠르고, MockMvc 객체를 자동으로 설정해 줍니다. 이런 기가 막힌 어노테이션이 있다니 바로 사용해 봤는데
“A component required a bean named 'entityManagerFactory' that could not be found.”
테스트가 실패합니다..? 제가 조사한 바로 이는 @WebMvcTest가 컨트롤러와 관련된 빈만 로드하기 때문에 발생하며 이를 해결하기 위해 기본적으로 JPA 관련 빈, 즉 EntityManagerFactory나 JpaRepositories와 같은 모두 MockBean으로 등록해야 한다고 합니다.
MockMvc의 StandaloneSetup만 기억하게 될 것 같아서, 두 가지 방법에 대한 장단점을 찾아봤습니다.
- 간단한 컨트롤러 테스트: MockMvc의 standaloneSetup을 사용하여 간단한 컨트롤러 테스트를 수행하는 것은 매우 효율적입니다.
- 복잡한 통합 테스트: 전체 애플리케이션 컨텍스트를 포함한 통합 테스트가 필요하다면 @SpringBootTest와 @AutoConfigureMockMvc를 사용하는 것이 좋습니다.
- 컨트롤러와 의존성 주입 간의 균형: 테스트하고자 하는 컨트롤러의 복잡성과 테스트의 목적에 따라 @WebMvcTest와 MockMvc.standaloneSetup 중 적절한 방법을 선택해야 합니다.
현재는 Controller의 단위 테스트를 작성 중이니 mockMvc의 standaloneSetUp을 사용하는 것이 적합해 보입니다.(휴)
그럼 다시 위에서 한 이야기로 돌아가기 전에, 이번 포스팅은 여기까지만 쓰면 안 될까요?
Controller 계층에서 고민하고 배운 점이 많기도 하고.. 그리고 커피를 다 마시기도 하고.. 새로운 포스팅에서 시작을 하고 싶어 졌습니다. 그럼 이만
'어차피 공부는 해야한다. > Spring' 카테고리의 다른 글
[Spring] TDD 도입기- 4. IOS Push 알람 기능 구현 (3) | 2024.08.28 |
---|---|
[Spring] TDD 도입기 - 3 H2가 밉다. (0) | 2024.07.12 |
[Spring] TDD 도입기 - 2. 아직은 잘 모르겠어요 (0) | 2024.06.28 |
리눅스(linux) 기본 배우기 (1) | 2023.11.19 |