[Spring] DDD에서 Repository Test 하기

0

Repository는 Jpa의 Repository를 이야기하는 것이 아니다.

데이터 액세스 계층(Data Access Layer)을 이야기하고자 한다.
(헥사고날 아키텍처로 이야기 하자면 Repository 는 Domain의 Driven Port이고 JpaRepositoryAdapter 이다.)

(JPA Entity를 도메인 모델로 사용하는 Service Layer의 아키텍처로 구성되어 있어도 응용할 수 있다.)

@Repository
public class ProductRepositoryImpl implements ProductRepository {

    private final ProductJpaRepository productJpaRepository;

    private final Function<ProductEntity, Product> from = entity -> new Product(entity.getId(), entity.getName(), entity.getPrice());

    public ProductRepositoryImpl(final ProductJpaRepository productJpaRepository) {
        this.productJpaRepository = productJpaRepository;
    }

    @Override
    @Transactional
    public Product save(final Product product) {
        ProductEntity entity = new ProductEntity(product.getId(), product.getName(), product.getPrice());

        ProductEntity savedEntity = productJpaRepository.save(entity);

        return from.apply(savedEntity);
    }
}

일단, Java로 웹 프로그래밍하면서 Spring 안쓸래? JPA 안쓸래? 하면 그것도 맞는 말이라고 생각하지만

외부 의존성을 타는 persistence 영역이나, ephemeral 영역은 포트 & 어댑터 패턴을 사용하는게 좋은 것 같다.

일단 JpaRepository를 주입받은 구현 클래스는 @DataJpaTest로 테스트를 할 수 없다.

Spring Data JPA 를 위한 슬라이스 테스트 환경을 자동 구성해주는 Annotation으로,

구현 클래스는 Application Context에 Bean으로 등록되지 않기 때문이다.

어떻게 해결해볼까?

Test Auto Configuration을 직접 구축하기

1. DataSourceAutoConfiguration

2. JdbcTemplateAutoConfiguration

3. HibernateJpaAutoConfiguration

4. TaskExecutionAutoConfiguration

5. JpaRepositoriesAutoConfiguration

@EnableTestAutoConfiguration 을 만들고 싶어서

org.springframework.boot.autoconfigure 를 뒤적거려봤다. 뭐가 참 많다.

일단... 까불지말고 SpringBoot에서 제공하는 Auto configuration을 이용하자

다시, 초심을 찾아 이걸 왜하고 있는지 생각해보면

도메인의 infrastructure interface를 구현한 클래스를 bean으로 등록하고 테스트를 하고 싶은 것이다.

@SpirngBootTest가 해주긴 하지만 다 퍼올려야 되니 느린게 문제다.

그럼 scope를 좁게 잡아보면 되지 않을까?

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class ProductRepositoryTests {

    @Autowired
    private ProductRepository productRepository;

    @Test
    @DisplayName("ProductRepository 빈이 등록되었을 것이다.")
    void productRepositoryBeanShouldBeRegistered() {
        Assertions.assertThat(productRepository).isNotNull();
    }
}

가장 먼저 해볼 수 있는건 Web 환경은 필요없다.

조금 테스트 해보자.

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class ProductRepositoryTests {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    @DisplayName("RepositoryTestConfiguration 이 외의 빈은 등록되지 않았을 것이다.")
    void shouldNotBeRegisteredBean() {
        Assertions.assertThatThrownBy(() -> applicationContext.getBean(SomethingComponent.class))
                .isInstanceOf(Exception.class);
    }

    @Test
    @DisplayName("ProductRepository 빈이 등록되었을 것이다.")
    void productRepositoryBeanShouldBeRegistered() {
        Assertions.assertThat(productRepository).isNotNull();
    }

    @Test
    @DisplayName("Product 를 성공적으로 저장할 수 있어야 한다.")
    void shouldBeProductIsStored() {

        // given
        Product product = new Product(null, "test", BigDecimal.ONE);

        // when
        Product saved = productRepository.save(product);

        // then
        Assertions.assertThat(saved.getId()).isNotNull();
    }

    @TestConfiguration
    static class RepositoryTestConfiguration {

        @Bean
        public ProductRepository productRepository(ProductJpaRepository productJpaRepository) {
            return new ProductRepositoryImpl(productJpaRepository);
        }
    }
}

일단 내가 원하지 않는 빈이 등록되어서 테스트가 깨졌다.
나는 infarastructure에 물려있는 Repository만 데이터베이스와 연동해서 실제 동작을 테스트해보고 싶을 뿐이다.

어떻게 해볼까?

@ComponentScan(
        resourcePattern = "**/*Repository*.class",
        includeFilters = @Filter(type = FilterType.ANNOTATION, classes = {Component.class, Repository.class})
)
@EnableAutoConfiguration(
        exclude = WebMvcAutoConfiguration.class
)
public class RepositoryTestConfiguration {}

TestConfiguration 클래스를 만들고
Component의 scope를 잡아준다.
나는 Repository의 네이밍을 가진 클래스 중에서
@Component, @Repository가 붙은 클래스만 스캔하기로 했다.

@ContextConfiguration(classes = RepositoryTestConfiguration.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DataJpaTest
class ProductRepositoryTests {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    @DisplayName("RepositoryTestConfiguration 이 외의 빈은 등록되지 않았을 것이다.")
    void shouldNotBeRegisteredBean() {
        Assertions.assertThatThrownBy(() -> applicationContext.getBean(SomethingComponent.class))
                .isInstanceOf(Exception.class);
    }

    @Test
    @DisplayName("ProductRepository 빈이 등록되었을 것이다.")
    void productRepositoryBeanShouldBeRegistered() {
        Assertions.assertThat(productRepository).isNotNull();
    }

    @Test
    @DisplayName("Product 를 성공적으로 저장할 수 있어야 한다.")
    void shouldBeProductIsStored() {

        // given
        Product product = new Product(null, "test", BigDecimal.ONE);

        // when
        Product saved = productRepository.save(product);

        // then
        Assertions.assertThat(saved.getId()).isNotNull();
    }
}

@ContextConfiguration(classes = RepositoryTestConfiguration.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DataJpaTest
class ProductRepositoryTests { 
	// .. 
}

Repository를 테스트할 때 마다 붙여야하는데 보일러플레이트 코드를 확장형 애너테이션으로 Repository 테스트 환경을 구성해볼 수도 있다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ContextConfiguration(classes = RepositoryTestConfiguration.class)
public @interface RepositoryTest {
}

@SpringJUnitConfig 이란게 있어서,
@ContextConfiguration,
@ExtendWith(SpringExtension.class)
가 묶인 합성 애너테이션이 존재하는데 이걸 이용하면 더 심플하게 작성할 수 있다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@SpringJUnitConfig(RepositoryTestConfiguration.class)
public @interface RepositoryTest {
}

@AutoConfigureTestDatabase의 replace 값을 none으로 세팅해주고 싶은데 무시된다.

아직 이 부분은 잘 몰라서 추측으로는 SpringBootTestContextBootstrapper가 없으니 부트스트래핑이 안되는거 같다.

Support ContextLoader config in @⁠SpringJUnit[Web]Config

슬라이스 테스트 관련된 애너테이션은 제공해주고 있으니, 원래 목적이었던 테스트에 필요한 컴포넌트를 빈으로 올리는데 집중하기로 했다.

Spring 6.1 부터 Context Loader를 지정할 수 있게 변경되니 이 때 적용보면서 머리 박아봐야지.

@RepositoryTest
@DataJpaTest
class ProductRepositoryTests { 
	// .. 
}

개발환경에서 실 디비를 물고 테스트를 해봐야하는 거라면 그것 또한 안 좋은 신호를 보내고 있는 것 일지도 모르겠다.

=======================================================================
2023-11-22
갑자기 생각나서 해봤는데

@DataJpaTest(
        includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Repository.class)
class MemberJpaRepositoryTest { }

이렇게 하면 된다. 쩝...

0개의 댓글