Ici TDD s’applique à un batch Spring Batch, un batch d’extraction est pris comme exemple pour appliquer la démarche.
Les sources complètes sont sur Github : springbatch-sample
Voici la spécification du batch d’extraction d’une base de données dans un fichier plat :
1 Job contient
- 1 validateur de présence du paramètre chemin du fichier en sortie
- 1 étape contient
- 1 reader jdbc
- 1 writer fichier plat
Le plan de test du batch est le suivant
- TDD reader jdbc et ensuite
- TDD writer fichier plat et ensuite
- TDD job extraction
Le SUT se présente sous la forme de classes java de configuration spring (aucun xml n’a été utilisé…)
Des classes génériques de configuration et des commodités de test telles que des Rules Junit et des templates de reader et writer sont fournies par le module springbatch-support.
On déroule le plan de test, d’abord le TDD du reader jdbc
package com.giovanetti.sample.batch.job; | |
import com.giovanetti.sample.batch.configuration.JobExtractionTestConfiguration; | |
import com.giovanetti.sample.batch.item.User; | |
import com.giovanetti.support.batch.rule.BatchProperties; | |
import com.giovanetti.support.batch.rule.DBUnitRule; | |
import com.giovanetti.support.batch.template.ItemReaderTemplate; | |
import org.junit.ClassRule; | |
import org.junit.Rule; | |
import org.junit.Test; | |
import org.junit.runner.RunWith; | |
import org.springframework.test.annotation.DirtiesContext; | |
import org.springframework.test.annotation.DirtiesContext.ClassMode; | |
import org.springframework.test.context.ContextConfiguration; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
import javax.inject.Inject; | |
import static com.giovanetti.sample.batch.item.ItemHelper.listOf2Users; | |
import static org.assertj.core.api.Assertions.assertThat; | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration(classes = {JobExtractionTestConfiguration.class}) | |
@DirtiesContext(classMode = ClassMode.AFTER_CLASS) | |
public class JdbcCursorItemReaderTest { | |
@ClassRule | |
public final static BatchProperties batchProperties = BatchProperties.getDefault(); | |
@Rule | |
@Inject | |
public DBUnitRule dbUnitRule; | |
@Inject | |
private ItemReaderTemplate<User> itemReader; | |
@Test | |
public void databaseInitialisationOK() { | |
assertThat(dbUnitRule.rowCountFrom("USER")).isEqualTo(2); | |
} | |
@Test | |
public void read() { | |
assertThat(itemReader.readAll()) | |
.hasSize(2) | |
.usingFieldByFieldElementComparator() | |
.containsAll(listOf2Users()); | |
} | |
} |
Quelques explications sur le test du reader jdbc
- JobExtractionTestConfiguration crée des beans spring utilitaires pour les tests et importe la classe à tester JobExtractionConfiguration
- La rule BatchProperties créée un fichier de properties temporaire pour configurer les datasources
- une datasource pour la base de données technique Spring Batch
- une datasource fonctionnelle pour lire les données à extraire
- La rule DBunit insère les données de test dans une base mémoire
- Le composant ItemReaderTemplate permet de récupérer tous les items de la base de données
On enchaîne sur le TDD du writer fichier plat
package com.giovanetti.sample.batch.job; | |
import com.giovanetti.sample.batch.configuration.JobExtractionTestConfiguration; | |
import com.giovanetti.sample.batch.item.User; | |
import com.giovanetti.support.batch.rule.BatchProperties; | |
import com.giovanetti.support.batch.template.ItemWriterTemplate; | |
import org.junit.ClassRule; | |
import org.junit.Test; | |
import org.junit.rules.TemporaryFolder; | |
import org.junit.runner.RunWith; | |
import org.springframework.batch.core.JobParametersBuilder; | |
import org.springframework.batch.core.StepExecution; | |
import org.springframework.batch.test.MetaDataInstanceFactory; | |
import org.springframework.batch.test.StepScopeTestExecutionListener; | |
import org.springframework.test.annotation.DirtiesContext; | |
import org.springframework.test.annotation.DirtiesContext.ClassMode; | |
import org.springframework.test.context.ContextConfiguration; | |
import org.springframework.test.context.TestExecutionListeners; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; | |
import org.springframework.test.context.support.DirtiesContextTestExecutionListener; | |
import javax.inject.Inject; | |
import java.io.File; | |
import java.io.IOException; | |
import java.nio.file.Files; | |
import static com.giovanetti.sample.batch.item.ItemHelper.listOf2Users; | |
import static org.assertj.core.api.Assertions.assertThat; | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration(classes = {JobExtractionTestConfiguration.class}) | |
@TestExecutionListeners( | |
{DependencyInjectionTestExecutionListener.class, StepScopeTestExecutionListener.class, DirtiesContextTestExecutionListener.class}) | |
@DirtiesContext(classMode = ClassMode.AFTER_CLASS) | |
public class FlatFileItemWriterTest { | |
public static StepExecution getStepExecution() throws IOException { | |
outputFile = outputRule.newFile(); | |
return MetaDataInstanceFactory.createStepExecution( | |
new JobParametersBuilder().addString(JobExtractionConfiguration.OUTPUT_FILE_PARAMETER, | |
outputFile.getPath()).toJobParameters()); | |
} | |
private static File outputFile; | |
@ClassRule | |
public final static TemporaryFolder outputRule = new TemporaryFolder(); | |
@ClassRule | |
public final static BatchProperties batchProperties = BatchProperties.getDefault(); | |
@Inject | |
private ItemWriterTemplate<User> itemWriter; | |
@Test | |
public void write() throws IOException { | |
// Act | |
itemWriter.write(listOf2Users()); | |
// Assert | |
assertThat(Files.readAllLines(outputFile.toPath())) | |
.hasSize(2) | |
.contains("1,prenom1,nom1", "2,prenom2,nom2"); | |
} | |
} |
- La méthode static getStepExecution est nécessaire pour injecter tardivement (late binding) le paramètre de chemin du fichier de sortie
- La rule TemporaryFolder permet de créer un fichier de sortie temporaire
- Le composant ItemWriterTemplate permet d’écrirer tous les items dans le fichier de sortie
Et on finit par le TDD du job extraction
package com.giovanetti.sample.batch.job; | |
import com.giovanetti.sample.batch.configuration.JobExtractionTestConfiguration; | |
import com.giovanetti.support.batch.ExternalConfiguration; | |
import com.giovanetti.support.batch.rule.BatchProperties; | |
import com.giovanetti.support.batch.rule.DBUnitRule; | |
import com.google.common.collect.Iterables; | |
import org.junit.ClassRule; | |
import org.junit.Rule; | |
import org.junit.Test; | |
import org.junit.rules.TemporaryFolder; | |
import org.junit.runner.RunWith; | |
import org.springframework.batch.core.*; | |
import org.springframework.batch.test.JobLauncherTestUtils; | |
import org.springframework.test.annotation.DirtiesContext; | |
import org.springframework.test.annotation.DirtiesContext.ClassMode; | |
import org.springframework.test.context.ContextConfiguration; | |
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | |
import javax.inject.Inject; | |
import static org.assertj.core.api.Assertions.assertThat; | |
@RunWith(SpringJUnit4ClassRunner.class) | |
@ContextConfiguration(classes = {JobExtractionTestConfiguration.class, JobLauncherTestUtils.class}) | |
@DirtiesContext(classMode = ClassMode.AFTER_CLASS) | |
public class JobExtractionTest { | |
@ClassRule | |
public final static TemporaryFolder outputFile = new TemporaryFolder(); | |
@ClassRule | |
public final static BatchProperties batchProperties = new BatchProperties().addTechnicalHsql() | |
.addFunctionalHsql() | |
.add(ExternalConfiguration.StepPropertyKeys.COMMIT_INTERVAL.toString(), "1"); | |
@Rule | |
@Inject | |
public DBUnitRule dbUnitRule; | |
@Inject | |
private JobLauncherTestUtils jobLauncherTestUtils; | |
@Test | |
public void jobExtraction() throws Exception { | |
// Act | |
JobExecution jobExecution = jobLauncherTestUtils.launchJob( | |
new JobParametersBuilder().addString(JobExtractionConfiguration.OUTPUT_FILE_PARAMETER, | |
outputFile.getRoot().getPath()).toJobParameters()); | |
// Assert | |
assertThat(jobExecution.getStatus()).isEqualTo(BatchStatus.COMPLETED); | |
StepExecution stepExecution = Iterables.getOnlyElement(jobExecution.getStepExecutions()); | |
assertThat(stepExecution.getReadCount()).isEqualTo(2); | |
assertThat(stepExecution.getWriteCount()).isEqualTo(2); | |
} | |
@Test(expected = JobParametersInvalidException.class) | |
public void jobExtraction_SiParametreInvalide_AlorsException() throws Exception { | |
jobLauncherTestUtils.launchJob(); | |
} | |
} |
Voilà, TDD permet de contrôler pas à pas et sans douleur la réalisation d’un batch Spring Batch.
Lorsque le dernier composant de test passe au vert alors le batch est conforme à la spécification, voir la solution complète sur Github : springbatch-sample.
Quant au code factorisé dans le module springbatch-support c’est la dernière étape de TDD (clean & refactor) qui l’a fait émergé.