ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 테스트팁 - 테스트 하기 좋은 코드
    TDD 2024. 6. 26. 20:22
    test

     

     

     

     

    가끔 욕을 먹고 싶을 때가 있을 수도 있죠.
    가끔 지탄을 받고 꾸중을 들음으로써 자극을 받고 정신을 차리고 싶을 수도 있습니다.
    아니면 혹은 그냥 아무 이유 없이 갑자기 한심한 눈초리를 받고 싶을 때가 있을 수도 있겠죠.
    그럴 땐 주변에 있는 훌륭한 개발자를 잡아놓고 “저희는 테스트 코드 안 짜요.” 라고 한 마디 건네 보세요.
    아주 쉽게 원하는 것을 얻으실 수 있을 것입니다.
    - 버즈빌 이성원님 - 

     💡 이 게시글은 계속해서 변화하는 게시글입니다. 제가 테스트 코드를 작성하면서 느낀 것을 나열한 것으로 언제든지 방향을 바꿀 수도 있습니다.

     

    Application.class 에 추가적인 어노테이션 사용 금지

    @ComponentScan({"com.sample.product"})
    @EnableJpaAuditing
    @ConfigurationPropertiesScan
    @SpringBootApplication
    public class SampleApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SampleApplication.class, args);
        }
    }

    application에 @componentScan 을 사용할 경우 application 이 로드 시 scan이 동작하게 된다.

    만약 @WebMvcTest를 사용하여 controller 단위 테스트를 진행하게 된다면

    @WebMvcTest는 controller에 필요한 bean만 스캔하는데 Application 로드 시 ComponentScan이 동작하게 되면서 Controller 테스트와는 적합하지 않은 스캔이 발생한다.

     

    @EnableJpaAuditing 또한 마찬가지이다

    @WebMvcTest에서 Application을 로드 시 @EnableJpaAuditing로 인해 JPA 관련 bean을 스캔하려 하지만 존재하지 않아 문제가 발생한다.

     

    이경우

    @Configuration
    @ComponentScan({"com.sample.product"})
    public class GlobalComponentScan {}
    
    @Configuration
    @EnableJpaAuditing
    public class JpaAuditingConfiguration {}
    
    @ConfigurationPropertiesScan
    @SpringBootApplication
    public class SampleApplication {}

    스캔대상이 되지 않도록 분리하자.

    util 용도의 static method 사용 금지

    public class UuidGenerator {
        public static String generateUUID() {
            return UUID.randomUUID().toString();
        }
    }
    
    String id = UuidGenerator.generateUUID();

    애플리케이션 코드에서는 Utils이라는 편한 이름으로 많은 static 코드가 존재한다

    위의 코드를 포함한 코드는 가짜 구현체를 넣을 수도 없고 mocking 할 수 없기에 테스트하기가 힘들어진다

    💡 Mokito 에는 mockStatic라는 static를 mocking 할 수 있는 방법을 제공하지만
    이에 대해 안티패턴으로 권장하지 않는다.

     

    /**
    지정한 시계에서 현재 날짜-시간을 가져옵니다.
    이것은 현재 날짜-시간을 얻기 위해 지정된 클록을 질의할 것입니다. 
    이 방법을 사용하면 테스트를 위해 대체 클록을 사용할 수 있습니다.
    대체 클록은 의존성 주입을 사용하여 도입될 수 있습니다.
    **/
    public static LocalDateTime now(Clock clock) {
        Objects.requireNonNull(clock, "clock");
        final Instant now = clock.instant();  // called once
        ZoneOffset offset = clock.getZone().getRules().getOffset(now);
        return ofEpochSecond(now.getEpochSecond(), now.getNano(), offset);
    }
    public static LocalDateTime now() {
        return now(Clock.systemDefaultZone());
    }

     

    흔히 static로 사용되는 LocalDateTime.now() 메서드는 테스트를 하기 위한 방법을 제시하고 있다.

    파라미터로 clock를 받는 메서드를 열어두었으니 이곳을 사용하라는 것

    @Service
    @RequiredArgsConstructor
    class TimeService {
       public void test() {
            LocalDateTime now = LocalDateTime.now();
        }
    }
    

    이렇게 사용하던 로직을

    @Configuration
    public class ClockConfig {
        @Bean
        public Clock clock() {
            return Clock.systemDefaultZone();
        }
    }
    
    @Service
    @RequiredArgsConstructor
    class TimeService {
    
        private final Clock clock;
    
        public void test() {
            LocalDateTime now = LocalDateTime.now(clock);
        }
    }

    위처럼 사용하는 것을 권장하는 것.

     

    토비의 스프링에서는 의존하고 있는 오프젝트를 DI를 사용하여 바꿔치기하는 것을 서비스 추상화라고 하며

    서비스 추상화는 다양한 장점이 있지만 원활한 테스트 만을 위해서도 충분히 가치가 있다고 한다.

     

    즉 굳이 추상화 안 해도 될 오브젝트지만 테스트만을 위해서 진행하는 것도 가치가 있다는 것

    static을 지우는 게 가장 좋지만 그럴 수 없다고 하면 해당 static 메서드를 호출하는 bean 객체를 만들어서 사용한다.

    public class UuidGenerator {
        public static String generateUUID() {
            return UUID.randomUUID().toString();
        }
    }
    
    @Component
    public class IdGenerator {
        public String uuid(){
            return UuidGenerator.generateUUID();
        }
    }

    테스트하기 어려운 코드는 레이어 가장 밖에 있어야 한다

    테스트하기 쉬운코드란 항상 같은 결과를 반환 하며 외부 상태를 변경하지 않는 코드를 말한다.

    그 의외에 외부 상태를 변경하거나 외부의 의존하는 모든 코드는 테스트 하기 어려운 코드이다.(데이터베이스 CRUD, 콘솔 출력 등 )

    레이어 가장 안쪽 코드에 테스트 하기 어려운 코드가 위치한다면 모든 레이어가 테스트 하기 어려워진다

    회원가입을 구현하는 경우

    • 이메일 검사, 비밀번호 검사하는 회원 가입 로직
    • 이메일과 패스워드가 유효한 경우 DB에 저장

    테스트하기 쉬운 코드와 어려운 코드를 분리한다.

    체크 과정과 저장 과정을 분리하여 처리한다.

    그렇지만 결국 위의 두 코드가 만나서 처리되는 과정이 필요한데

    그곳을 가장 밖에서(Boundary Layer) 처리한다

    테스트하기 쉬운 코드와 어려운 코드는 바운더리 레이어에서 만난다.

    회원가입의 두 기능은 레이어드라면 facade 레이어 에서 만나야 할 것이고

    핵사고날이라면 service 레이어 에서 만나야 할 것이다.

    그리고 이 둘이 만나는 테스트는 인수테스트로 진행한다.

    💡 일반적인 레이어드 아키텍처로는 테스트 코드를 작성하기 어렵다고 생각한다. 레이어드 아키텍처를 사용하야 한다면 controller - facade - service - repository로 구분하여 테스트하기 어려운 코드를 facade 레이어로 운영하자.

    Service코드끼리의 합성은 인터페이스로

    A service 구현체에서 B service 구현체를 사용할 때

    A service를 생성하려면 B Service 도 생성 해야 한다.

    이때 만약 A 가 B를 인터페이스가 아닌 구현체로 알고 있다면

    B를 생성하는 것도 굉장히 힘들어진다.( B도 다른 서비스들의 합성 일 테니까)

    만약 A가 B를 구현체가 아닌 인터페이스로 알고 있다면 인터페이스를 익명으로 구현하거나 테스트 더블 객체로 만들어서 사용하면 쉽게 A를 구현할 수 있다

    // NO
    class UserService {
    	private final MessageHandler handler;
    }
    class MessageHandler {}
    
    ---
    
    // OK
    class UserService {
    	private final MessageHandler handler;
    }
    
    class MessageResolver implements MessageHandler{}
    
    interface MessageHandler{}
    

    Repository에서 Entity를 찾지 못하는경우 옵셔널이 아닌 예외

    Service 레이어에서 Repository 레이어에 Entity 를 요청했을 때

    Entity를 찾지 못하는 경우 Repository에서 옵셔널을 내려주게 되면

    Service 레이어에서 옵셔널을 받았을 때에 대한 테스트 요소가 추가가 된다.

    Repository에서는 entity를 못 찾는 경우 예외를 전달하여 테스트 범위가 넘어가는 걸 방지하자

    'TDD' 카테고리의 다른 글

    테스트팁 - 좋은 테스트 코드  (0) 2024.06.26
    테스트팁 - 테스트 작성 방법  (0) 2024.06.26