Study/Next Step / / 2024. 2. 17. 15:21

[Next Step] ATDD, 클린 코드 with Spring 8기 2주차 리뷰

🎈 인수테스트 격리하기

인수 테스트를 격리하는 방법은 아래와 같다.

  1. @Transactional 어노테이션 활용
  2. @DirtiesContext 어노테이션 활용
  3. @Sql 혹은 쿼리 수행
  4. 코드 상으로 테이블 truncate

나의 경우에는 3번째 방법 중 쿼리를 사용하고 @interface로 선언하여 클래스단에 어노테이션을 붙이면 쉽게 사용할 수 있도록 하였다. 이 방법은 DB마다 동작하지 않을 수 있지만, JPA에서 @Embeddable과 같이 중간 테이블이 생기는 경우도 초기화를 쉽게 할 수 있어서 이 방법을 선택했다. 

 

@Transactional

  • RANDOM_PORT나 DEFINED_PORT 사용 할 경우 실제 서블릿 환경이 제공됨
  • HTTP Client와 Server는 다른 스레드에서 동작
  • 따라서 @Transactional을 통한 roll back은 동작하지 않음

 

@DirtiesContext

  • 효과적인 테스트 수행을 위해 스프링에서는 context caching 기능 지원
  • context caching의 조건이 빈이 오염(변경)되지 않은 경우
  • @DirtiesContext를 활용하여 (빈을 오염시켜) 캐시 기능을 사용하지 않게 설정 가능
  • 매번 Context를 새로 구성하다보니 시간이 많이 걸림

 

@Sql 혹은 쿼리 수행

  • 테스트가 수행될 때 마다 테이블들을 truncate 시키는 쿼리 수행
  • 컨텍스트를 다시 띄우는 것 보다 낮은 비용
  • 테이블이 추가될 때 마다 해당 쿼리 수정이 필요

 

코드 상으로 테이블 truncate

  • JPA 사용시 EntityManager를 이용하여 테이블 이름 조회
  • 아닌 경우 DataSource를 이용하여 테이블 이름 조회
  • 각 테이블을 truncate 시켜주는 쿼리 수행
  • 테이블 상태에 의존하지 않는 초기화 환경 구축 가능

 

😊 인수 테스트 리팩터링

테스트를 리팩터링 할 때는 고려해야 할 사항이 있다.

 

테스트의 의도를 명확히 드러내야 한다.

이를 통해 가독성을 챙길 수 있다.

가독성이 좋지 않은 테스트는 방치될 가능성이 있고, 변경 사항에 대해 수정하기 어렵다. 그리고 가독성이 좋으면 해당 기능의 스팩을 나타낼 수 있다. 이는 프로덕션 코드도 동일하다.

 

테스트 코드 중복을 제거해라.

테스트 코드도 코드다. 코드 작성시 중복 내용이 많이 발생하여 가독성과 유지보수를 위해 중복제거를 해야한다.

메소드를 분리하고, CRUD를 추상화하고, Cucumber나 JBehave와 같은 BDD 도구를 사용하여 중복 제거가 가능하다.

다른 테스트에서 재사용 될 가능성이 높은 코드들은 별도의 클래스로 분리하여 static 선언하여 쉽게 다른곳에서 사용이 가능하여 일관성있는 코드를 사용할 수 있다.

그리고 CRUD를 추상화하여 의도를 드러낼 수 있다. 메서드를 한글명으로 래핑하여 읽기 쉬운 테스트를 만들 수 있다.

 

❓ 모든 요구사항을 인수 테스트로 작성해야 할까?

이는 절대 아니다. 비효율적이며, 테스트의 수가 많은게 중요한게 아니다.

성공하는 해피 케이스에 대한 검증 위주로 작성하고, 예외 케이스에 대한 검증은 비용이 낮은 테스트로 작성하자.

 

🎁 TDD와 단위 테스트

단위 테스트는 작은 코드 조각을 검증한다. 빠르게 수행이 가능하고 격리된 방식으로 처리가 가능하다.

단위 테스트를 작성할때는 협력 객체가 있는 경우가 있다.

통합과 고립이라는 특징을 통해 협력 객체를 실제 객체로 사용하는지, Mock 객체로 사용하는지에 따라서 테스트 구현이 달라진다.

 

🎭 Solitary Tests

Mock 객체를 사용하기 전에 Test Double과, Stub에 대해 알아야 하는데,

Test Double -  실 객체 대신 사용되는 모든 종류의 객체에 대한 일반용어, 즉 실제 객체를 가짜로 대체

Stub - Test Double의 동작을 사전에 정의하는 행위

 

일반적으로 3가지 방법을 통해 Mock 객체를 생성하여 Subbing 할 수 있다.

  1. Mockito 활용
  2. MockitoExtension 활용
  3. Spring을 활용한 Stubbing

 

🔎 통합 vs 고립

협력 객체를 실제 객체로 해야할까? 가짜 객체로 해야할까?

실제 객체로 사용하면 협력 객체의 행위를 협력 객체 스스로 정의한다. 그리고 협력 객체의 상세 구현에 대해 알 필요가 없지만, 협력 객체의 정상 동작 여부에 영향을 받는다.

하지만 가짜 객체로 한다면  협력 객체의 행위를 테스트가 정의한다. 테스트 대상을 검증할 때 외부 요인에 대해 철저리 격리되어 있지만 상세 구현을 알아야 한다는 차이가 있다. 조금 더 편하게 테스트가 가능하지만, 상세 구현에 의존하는 테스트가 될 수 있어 이를 고려해서 선택해야 한다.

내 생각으로, 테스트가 가능하다면 실 객체를 사용하여 실제 동작이 변경되면 테스트를 수정하여 모든 테스트가 살아있는 테스트가 되어야 한다고 생각한다. 만약, 동작을 예측할 수 있는 클래스인데 mock 객체를 사용한다면, 해당 클래스가 변경되었을때도 가짜 객체를 통해 테스트를 한다면 실패해야 할 테스트가 성공하는 일이 발생할 수 있다고 생각하기 때문이다. 하지만, 랜덤한 값을 출력하는 클래스거나 외부와 통신을 통해 예측할 수 없는 값을 가져오는 경우라면 mock을 활용하는게 맞다고 생각한다.