Spring Batch

SpringBatch에서 ItemReader를 Mock객체로 교체하기

향로 (기억보단 기록을) 2017. 10. 17. 08:34
반응형

안녕하세요? 이번 시간엔 SpringBatch에서 ItemReader를 Mock객체로 교체하는 예제를 진행해보려고 합니다.
모든 코드는 Github에 있기 때문에 함께 보시면 더 이해하기 쉬우실 것 같습니다.
(공부한 내용을 정리하는 Github와 세미나+책 후기를 정리하는 Github, 이 모든 내용을 담고 있는 블로그가 있습니다. )

발단

스프링 배치로 테스트 코드를 짜다보면 정말 하기 싫은것이 ItemReader로 읽어올 데이터를 만드는 것입니다.
특히나 실제 업무에서 쓰다보면 수많은 필수값들과 엔티티 관계들로 인해 만들어야할 데이터가 많을 경우 더 그렇습니다.
이렇게 테스트 환경 구축이 힘들때는 역시나! Mock을 활용하면 좋다고 생각했습니다.
ItemReader를 어떻게 Mocking할지 고민하다 팀내 주임님의 코드로 힌트를 얻어 Mocking 할수있게되어 정리하게 되었습니다.

본문

기본 프로젝트 구성

스프링부트배치+Spock+Lombok으로 구성할 예정입니다.

build.gradle

apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

dependencies {
    compile('org.springframework.boot:spring-boot-starter-batch')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    runtime('com.h2database:h2')
    compileOnly('org.projectlombok:lombok')
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.springframework.batch:spring-batch-test')
    testCompile('org.spockframework:spock-core:1.1-groovy-2.4')
    testCompile('org.spockframework:spock-spring:1.1-groovy-2.4')
}

스프링배치를 사용하기 위해 Application.java@EnableBatchProcessing을 선언합니다.

@SpringBootApplication
@EnableBatchProcessing
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

사용할 엔티티 클래스들과 레파지토리는 다음과 같습니다.

@Entity
@Getter
@NoArgsConstructor
public class Customer {
    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private String email;

    public Customer(Long id) {
        this.id = id;
    }

    public Customer(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

public interface CustomerRepository extends JpaRepository<Customer, Long> {
}

@Entity
@Getter
@NoArgsConstructor
public class CustomerBackup {

    @Id
    @GeneratedValue
    private Long id;

    private Long customerId;

    public CustomerBackup(Long customerId) {
        this.customerId = customerId;
    }
}

public interface CustomerBackupRepository extends JpaRepository<CustomerBackup, Long> {
}

기본적인 클래스 생성은 끝났습니다.
다음은 테스트 환경을 구축하겠습니다.

테스트 환경

src/test/groovy 혹은 src/test/java에 본인의 패키지를 생성하고 테스트 환경 설정 파일을 생성합니다.
여기서는 TestJobConfiguration란 이름으로 생성하였습니다.

@EnableBatchProcessing
@Configuration
@EnableAutoConfiguration
@ComponentScan
public class TestJobConfiguration {

    @Bean
    public JobLauncherTestUtils jobLauncherTestUtils() {
        return new JobLauncherTestUtils();
    }
}

테스트환경설정

테스트 환경에서 실행되는 쿼리를 보고 싶다면 src/test/resources/application.yml에 다음과 같이 설정값을 추가합니다.

spring:
  jpa:
    show-sql: true

자 테스트 환경도 이제 끝났습니다.
본격적으로 배치 코드를 작성해보겠습니다.

스프링 배치 코드

저희가 만들 스프링 배치 기능은 간단합니다.
조건에 맞는 Customer를 조회해 CustomerBackup으로 복사하는 것입니다.
클래스명은 CustomerCopyBatchConfiguration로 하겠습니다.

import static com.jojoldu.mockreader.batch.CustomerCopyBatchConfiguration.JOB_NAME;

@AllArgsConstructor
@Configuration
@ConditionalOnProperty(name = "job.name", havingValue = JOB_NAME)
public class CustomerCopyBatchConfiguration {
    public static final String JOB_NAME = "customerCopyJob";
    private static final String STEP_NAME = "customerCopyStep";
    private static final int CHUNK_SIZE = 100;

    private JobBuilderFactory jobBuilderFactory;
    private StepBuilderFactory stepBuilderFactory;
    private EntityManagerFactory emf;

    /**
     * Mock 객체로 교체할 수 있게 Autowired 받도록 수정
     */
    private JpaPagingItemReader<Customer> itemReader;

    @Bean
    public Job job() {
        return jobBuilderFactory.get(JOB_NAME)
                .start(step())
                .build();
    }

    @Bean
    public Step step() {
        return stepBuilderFactory.get(STEP_NAME)
                .<Customer, CustomerBackup>chunk(CHUNK_SIZE)
                .reader(itemReader) // 인잭션 받은 itemReader를 사용한다
                .processor(processor())
                .writer(writer())
                .build();
    }

    @Bean
    @StepScope
    public JpaPagingItemReader<Customer> reader(
            @Value("#{jobParameters[mailHost]}") String mailHost) {
        Map<String, Object> paramMap = new HashMap<>();
        paramMap.put("mailHost", "%@"+mailHost);

        JpaPagingItemReader<Customer> reader = new JpaPagingItemReader<>();
        reader.setQueryString("SELECT c FROM Customer c where c.email like :mailHost");
        reader.setParameterValues(paramMap);
        reader.setPageSize(CHUNK_SIZE);
        reader.setEntityManagerFactory(emf);

        return reader;
    }

    @Bean
    public ItemProcessor<Customer, CustomerBackup> processor() {
        return item -> new CustomerBackup(item.getId());
    }

    @Bean
    public JpaItemWriter<CustomerBackup> writer() {
        final JpaItemWriter<CustomerBackup> itemWriter = new JpaItemWriter<>();
        itemWriter.setEntityManagerFactory(emf);
        return itemWriter;
    }
}

여기서 눈여겨 보실점은 바로 ItemReader를 받는 부분입니다.

배치코드

(itemReader@AllArgsConstructor으로 생성자 인잭션을 받았습니다.
쉽게 얘기해서 @Autowired가 된 상태라고 보시면 됩니다.)

기존에 processor나 writer와 달리 reader는 Bean으로 입력받도록 합니다.
이렇게 하면 실제 구동 환경에선 @Bean으로 지정된 reader 메소드의 결과값을 받아 쓰지만,
테스트 환경에선 언제든지 가짜 Bean으로 교체할 수 있는 구성이 된것입니다.

반대로 processor나 writer를 Mocking 하고 싶다면 이들을 Bean으로 입력받도록 하면 되겠죠?

자 그럼 실제로 이 코드가 잘 수행되는지 테스트 해보겠습니다.

테스트 코드

제일 먼저 시도해볼 것은 실제로 배치 코드가 잘 돌아가는 것인지 테스트하는 것입니다.

@TestPropertySource(properties = "job.name=customerCopyJob")
@SpringBootTest
class CustomerCopyBatchRealTest extends Specification {

    @Autowired
    JobLauncherTestUtils jobLauncherTestUtils

    @Autowired
    CustomerRepository customerRepository

    def "[Real] gmail.com을 가진 Customer 데이터를 CustomerBackup으로 복사한다" () {
        given:
        customerRepository.save(new Customer("jojoldu", "jojoldu@gmail.com"))
        customerRepository.save(new Customer("jojoldu1", "jojoldu@naver.com"))

        JobParametersBuilder builder = new JobParametersBuilder()
        builder.addString("mailHost", "gmail.com")

        when:
        JobExecution jobExecution = jobLauncherTestUtils.launchJob(builder.toJobParameters())

        then:
        jobExecution.status == BatchStatus.COMPLETED
    }
}

Spock으로 작성한 배치 테스트 코드입니다.
Junit과 크게 다르지 않아서 보시는데 어려움은 없으실것 같습니다.
테스트 코드를 실행해보면!

테스트통과1

이렇게 Bean으로 ItemReader를 잘 받아서 수행했음을 확인하였습니다.
자! 그럼 대망의 Mock Bean으로 ItemReader를 교체한 테스트 코드를 작성하겠습니다.

@TestPropertySource(properties = "job.name=customerCopyJob")
@SpringBootTest
class CustomerCopyBatchMockTest extends Specification {

    @Autowired
    JobLauncherTestUtils jobLauncherTestUtils

    @Autowired
    CustomerBackupRepository customerBackupRepository

    @MockBean(name = "itemReader")
    JpaPagingItemReader<Customer> mockItemReader

    def "[Mock] gmail.com을 가진 Customer 데이터를 CustomerBackup으로 복사한다" () {
        given:
        given(mockItemReader.read())
                .willReturn(new Customer(1L), new Customer(2L), null)

        JobParametersBuilder builder = new JobParametersBuilder()
        builder.addString("mailHost", "gmail.com")

        when:
        JobExecution jobExecution = jobLauncherTestUtils.launchJob(builder.toJobParameters())

        then:
        List<CustomerBackup> customerBackups = customerBackupRepository.findAll()
        jobExecution.status == BatchStatus.COMPLETED
        customerBackups.size() == 2
    }
}

좀전에 작성한 테스트 코드와 거의 흡사한 형태입니다.
(참고로 given().willReturn은 모두 Spock이 아닌 Spring-starter-test에 있는 의존성들입니다.
Junit에서도 동일하게 사용하실수 있습니다.)

여기서 주의하실 점은 .willReturn에서 반환하는 값들입니다.

목배치코드

여기선 배열을 리턴하는 것이 아닙니다.
Mocking 메소드 (여기서는 mockItemReader.read()) 가 수행될때마다 반환될 값들을 순차적으로 작성한 것입니다.
그래서 처음 read가 수행되면 new Customer(1L)가, 두번째 수행될땐 new Customer(2L), 마지막으로 3번째로 수행될땐 null가 반환되도록 한것입니다.
배열로 작성하면 배열 자체가 리턴되니 오류가 발생합니다.
read 메소드는 더이상 읽지 않겠다의 판단을 마지막으로 읽은 값이 null이냐 아니냐로 판단하기 때문입니다.

자 그럼 Mock 테스트도 한번 실행해보겠습니다.

테스트통과2

보시는 것처럼 Customer를 조회하는 부분은 전혀 없지만 (사진속 select는 검증을 위한 CustomerBackup조회 쿼리입니다.) 테스트 코드가 통과됨을 알 수 있습니다.
만약 MockBean이 아닌 실제 Bean을 사용했다면 읽을 데이터가 없어 2개의 Insert가 발생할수 없었겠죠?
정말 쉽게 ItemReader를 Mocking 할수 있게 되었습니다.

마무리

스프링부트 1.4부터 정말 테스트하기가 좋은 환경이 된것 같습니다.
더 다양한 환경에서 테스트와 Mocking 하는 사례를 계속 공유하겠습니다.
끝까지 읽어주셔서 감사합니다!

반응형