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é.