问题
I'm trying to write the tests for a Spring Batch application, specifically on the interaction from the following reader when it gets records from a database implementing a simple RowMapper :
@Component
@StepScope
public class RecordItemReader extends JdbcCursorItemReader<FooDto> {
@Autowired
public RecordItemReader(DataSource dataSource) {
this.setDataSource(dataSource);
this.setSql(AN_SQL_QUERY);
this.setRowMapper(new RecordItemMapper());
}
}
Here is the step definition from the Batch configuration :
@Bean
public Step step(RecordItemReader recordItemReader,
BatchSkipListener skipListener,
RecordItemWriter writer,
RecordItemProcessor processor,
PlatformTransactionManager transactionManager) {
return stepBuilderFactory
.get("step1")
.transactionManager(transactionManager)
.reader(recordItemReader)
.faultTolerant()
.skip(ParseException.class)
.skip(UnexpectedInputException.class)
.skipPolicy(new AlwaysSkipItemSkipPolicy())
.listener(skipListener)
.processor(processor)
.writer(writer)
.build();
}
Everything works fine, except when I try to test using the following :
@SpringBatchTest
@EnableAutoConfiguration
class BatchSOTest extends Specification {
@Resource
JobLauncherTestUtils jobLauncherTestUtils
@Resource
JobRepositoryTestUtils jobRepositoryTestUtils
@Resource
RecordItemReader recordItemReader
def cleanup() {
jobRepositoryTestUtils.removeJobExecutions()
}
def "batch init perfectly"() {
given:
// this does not work :
(1.._) * recordItemReader.read() >> null
when:
def jobExecution = jobLauncherTestUtils.launchJob()
def jobInstance = jobExecution.getJobInstance()
def exitStatus = jobExecution.getExitStatus()
then:
jobInstance.getJobName() == "soJob"
exitStatus.getExitCode() == ExitStatus.SUCCESS.getExitCode()
}
}
I'm not able to Mock the reader properly, I tried various ways like updating reader's properties such as MaxRows
, but nothing seems to work.
What is the proper way to update the result of the Reader?
Or does it needs to be done another way to properly manipulate the records from the database during unit tests?
UPDATE: Ok so I tried a more structured way using a service inside the reader :
@Component
public class FooDtoItemReader extends AbstractItemStreamItemReader<FooDto> {
private List<FooDto> foos ;
private final FooService fooService;
@Autowired
public FooDtoItemReader(FooService fooService) {
this.fooService = fooService;
}
@Override
public void open(ExecutionContext executionContext) {
try {
foos = fooService.getFoos();
...
public interface FooService {
List<FooDto> getFoos();
}
@Service
public class FooServiceImpl implements FooService {
@Autowired
private FooDao fooDao;
@Override
public List<FooDto> getFoos() {
return fooDao.getFoos();
}
}
@Repository
public class FooDaoImpl extends JdbcDaoSupport implements FooDao {
@Autowired
DataSource dataSource;
@PostConstruct
private void initialize() {
setDataSource(dataSource);
}
@Override
public List<FooDto> getFoos() {
return getJdbcTemplate().query(SELECT_SQL, new FooMapper());
}
}
Here, I'm facing the problem where I cannot mock properly my Service :
I must be missing something with the test utils.
class BatchSOTest extends Specification {
@Resource
JobLauncherTestUtils jobLauncherTestUtils
@Resource
JobRepositoryTestUtils jobRepositoryTestUtils
FooService fooService = Mock(FooService);
FooDtoItemReader fooDtoItemReader = new FooDtoItemReader(fooService)
def cleanup() {
jobRepositoryTestUtils.removeJobExecutions()
}
def "batch init perfectly (second version)"() {
given:
fooDtoItemReader.open(Mock(ExecutionContext))
and:
// still not working from there :
(1.._) * fooService.getFoos() >> [createFooEntity(123, "Test")]
when:
def jobExecution = jobLauncherTestUtils.launchJob()
def jobInstance = jobExecution.getJobInstance()
def exitStatus = jobExecution.getExitStatus()
then:
jobInstance.getJobName() == "soJob"
exitStatus.getExitCode() == ExitStatus.SUCCESS.getExitCode()
}
But if I try to mock from there, it works :
class FooDtoItemReaderTest extends Specification {
FooService fooService = Mock(FooService);
FooDtoItemReader fooDtoItemReader = new FooDtoItemReader(fooService, 0)
def "open gets the foos and reader is initialized"() {
given: "Foos examples"
def foos = [
createFooEntity(123, "A"),
createFooEntity(456, "B")
]
when: "reader is initialized"
fooDtoItemReader.open(Mock(ExecutionContext))
then: "service get the expected foos"
1 * fooService.getFoos() >> foos
}
So what am I doing wrong ?
回答1:
When it comes to testing database interactions, I would not mock the reader. I would instead use an embedded database and populate it with test data. This can be achieved by adding the following bean to your test context:
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("/org/springframework/batch/core/schema-drop-h2.sql")
.addScript("/org/springframework/batch/core/schema-h2.sql")
.addScript("/schema.sql")
.addScript("/test-data.sql")
.build();
}
This example uses H2, but you can use derby or HSLQ or SQLite or any other embeddable database.
来源:https://stackoverflow.com/questions/62212196/how-to-mock-an-itemreader-in-a-spring-batch-application-using-spock-and-groovy