Depuis plusieurs années les nouvelles fonctionnalités de Junit étaient surtout destinées à rattraper le retard pris sur son concurrent TestNg. Aujourd'hui les deux frameworks convergent au niveau de leur fonctionnalité.
Voici un tableau fait par Zeroturnaround résumant les différences
Je vais parler d'une fonctionnalité méconnue de Junit, les Rules qui n'ont pas d'équivalent dans TestNg.
Mutualiser des fonctionnalités communes
Comment faites vous pour mutualiser du comportement dans vos tests ? …. Je vous aide un peu... Il existe plusieurs solutions.
Tout d'abord au sein même d'une classe de tests, vous pouvez définir des méthodes qui seront lancées
La dernière solution un peu plus compliquée consiste à définir un Runner Junit. Cette solution est souvent adoptée par les solutions tierces (exemples SpringTest , JunitParams, Archillian..). Mais on ne peut pas définir plus d'un Runner par classe. Si vous devez en utiliser plusieurs vous n'aurez pas d'autres choix que de passer par une classe abstraite.
C'est dans cette optique que les Rules Junit ont été mises en place. Une Rule Junit permet d'ajouter du comportement avant ou après l'exécution de chacune de vos méthodes de tests. Une Rule est ensuite attachée à une classe de tests via l'annotation @Rule.
Rien de mieux que du code pour introduire le sujet, non? Je ne présenterai que des portions de code dans cet article mais l'intégralité des exemples est disponible sous Github.
Comment se passer de l'abstraction ?
Si vous êtes coutumier des tests d'intégration avec une base de données, je suppose que vous connaissez Dbunit. DbUnit demande par exemple de définir sa propre source de données. Mais vous ne le faites pas dans chacune de vos classes de test... On retrouve souvent une classe abstraite permettant de s'interfacer à Dbunit qui ressemble à ça
La classe de test peut être par exemple
Définir une Rule
Nous allons donc définir notre première Rule pour simplifier tout ça. Une Rule doit implémenter l'interface TestRule. Pour vous simplifier la vie vous pouvez étendre les classes concrètes fournies par Junit. Ici je vais étendre la classe ExternalRessource qui permet de rajouter du comportement avant ou après une méthode de test.
Comme vous pouvez le constater ce n'est pas plus compliquer que d'écrire la classe abstraite. Au niveau de la classe de tests nous pouvons supprimer l'héritage.
Comment se passer de Runner Junit?
Nous avons vu comment nous passer de l'abstraction. Allons plus loin avec le Runner. Le Runner Junit utilisé dans les tests est là pour initier un contexte Spring pour ensuite injecter via un autowire les éléments à tester.
En attendant que Spring intègre dans le projet spring-test une Rule vous pouvez la créer vous même
Notre classe de tests devient encore plus simple
Comment chainer des Rules ?
On pourrait maintenant se dire qu'une Rule pourrait dépendre d'une autre. Pour oublier un peu DbUnit je parlerai d'une solution alternative plus élégante à mon sens qui se nomme DbSetup. Si vous ne connaissez pas DbSetup je vous laisse lire la documentation en ligne qui contient tout ce qu'il y a savoir.
DbSetup utilise une datasource. Si vous faites du Spring cette dernière est déjà initialisée dans le contexte et il serait intéressant de la récupérer. Comment pourrait t-on écrire une Rule récupérant le context défini dans une autre? C'est là que rentre en jeu les RuleChain.
Passons à l'exemple la Rule DbSetup est la suivante
Et la classe de test...
D'autres exemples de Rules ?
Plusieurs Rules sont fournies par Junit : ErrorCollector, ExpectedException, TemporaryFolder, TestWatcher. On les retrouve souvent reprises dans les blogs fournissant des exemples d'implémentation des Rules. Ces Rules n'ont d'intérêt que dans un nombre de uses cases limités et je pense que c'est une des raisons pour laquelle les Rules restent encore trop inutilisées.
Je n'en parlerai pas ici mais quelques exemples sont disponibles dans mon projet exemple sous Github.
Je ne sais pas si je vous ai convaincu mais si vous souhaitez mutualiser du comportement entre des tests gardez à l'esprit que les Rules peuvent répondre à votre besoin.
Voici un tableau fait par Zeroturnaround résumant les différences
Je vais parler d'une fonctionnalité méconnue de Junit, les Rules qui n'ont pas d'équivalent dans TestNg.
Mutualiser des fonctionnalités communes
Comment faites vous pour mutualiser du comportement dans vos tests ? …. Je vous aide un peu... Il existe plusieurs solutions.
Tout d'abord au sein même d'une classe de tests, vous pouvez définir des méthodes qui seront lancées
- après l'intialisation de la classe ou juste avant sa destruction (@BeforeClass ou @AfterClass)
- après ou avant le lancement d'une méthode via @Before ou @After
La dernière solution un peu plus compliquée consiste à définir un Runner Junit. Cette solution est souvent adoptée par les solutions tierces (exemples SpringTest , JunitParams, Archillian..). Mais on ne peut pas définir plus d'un Runner par classe. Si vous devez en utiliser plusieurs vous n'aurez pas d'autres choix que de passer par une classe abstraite.
C'est dans cette optique que les Rules Junit ont été mises en place. Une Rule Junit permet d'ajouter du comportement avant ou après l'exécution de chacune de vos méthodes de tests. Une Rule est ensuite attachée à une classe de tests via l'annotation @Rule.
Rien de mieux que du code pour introduire le sujet, non? Je ne présenterai que des portions de code dans cet article mais l'intégralité des exemples est disponible sous Github.
Comment se passer de l'abstraction ?
Si vous êtes coutumier des tests d'intégration avec une base de données, je suppose que vous connaissez Dbunit. DbUnit demande par exemple de définir sa propre source de données. Mais vous ne le faites pas dans chacune de vos classes de test... On retrouve souvent une classe abstraite permettant de s'interfacer à Dbunit qui ressemble à ça
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {PersistenceConfig.class}) @TestExecutionListeners({DependencyInjectionTestExecutionListener.class, TransactionalTestExecutionListener.class}) @Transactional public abstract class AbstractDbunitRepositoryTest { private static Properties properties = new Properties(); protected IDatabaseTester databaseTester; protected static String databaseJdbcDriver; protected static String databaseUrl; protected static String databaseUsername; protected static String databasePassword; protected IDataSet dataSet; @BeforeClass public static void initProperties() throws IOException { if (databaseJdbcDriver == null) { properties.load(AbstractDbunitRepositoryTest.class.getResourceAsStream("/application.properties")); databaseJdbcDriver = properties.getProperty("db.driver"); databaseUrl = properties.getProperty("db.url"); databaseUsername = properties.getProperty("db.username"); databasePassword = properties.getProperty("db.password"); } } @Before public void importDataSet() throws Exception { IDataSet dataSet = readDataSet(); databaseTester = new JdbcDatabaseTester(databaseJdbcDriver, databaseUrl, databaseUsername, databasePassword); databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT); databaseTester.setDataSet(dataSet); databaseTester.onSetup(); } protected abstract IDataSet readDataSet(); }
La classe de test peut être par exemple
public class DbUnitTestWithAbstractTest extends AbstractDbunitRepositoryTest { @Autowired private CountryRepository countryRepository; protected IDataSet readDataSet() { try { return new FlatXmlDataSetBuilder().build(new File("src/test/resources/datasets/country.xml")); } catch (MalformedURLException | DataSetException e) { throw new RuntimeException(e); } } @Test public void shouldFindCountryWhenCodeIsKnown() { Country persistantCountry = countryRepository.findCountryByCode("FRA"); assertThat(persistantCountry.getName()).isEqualTo("France"); } }
Définir une Rule
Nous allons donc définir notre première Rule pour simplifier tout ça. Une Rule doit implémenter l'interface TestRule. Pour vous simplifier la vie vous pouvez étendre les classes concrètes fournies par Junit. Ici je vais étendre la classe ExternalRessource qui permet de rajouter du comportement avant ou après une méthode de test.
public class DbUnitRule extends ExternalResource { private Properties properties = new Properties(); protected IDatabaseTester databaseTester; protected IDataSet dataSet; protected String databaseJdbcDriver; protected String databaseUrl; protected String databaseUsername; protected String databasePassword; public DbUnitRule(IDataSet dataSet) { this.dataSet = dataSet; } @Override protected void before() throws Throwable { initProperties(); databaseTester = new JdbcDatabaseTester(databaseJdbcDriver, databaseUrl, databaseUsername, databasePassword); databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT); databaseTester.setDataSet(dataSet); databaseTester.onSetup(); } private void initProperties() throws IOException { if (databaseJdbcDriver == null) { properties.load(getClass().getResourceAsStream("/application.properties")); databaseJdbcDriver = properties.getProperty("db.driver"); databaseUrl = properties.getProperty("db.url"); databaseUsername = properties.getProperty("db.username"); databasePassword = properties.getProperty("db.password"); } } }
Comme vous pouvez le constater ce n'est pas plus compliquer que d'écrire la classe abstraite. Au niveau de la classe de tests nous pouvons supprimer l'héritage.
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {PersistenceConfig.class}) @TestExecutionListeners({DependencyInjectionTestExecutionListener.class}) public class DbuniCibleTest { @Autowired private CountryRepository countryRepository; @Rule public DbUnitRule rule = new DbUnitRule(readDataSet()); protected IDataSet readDataSet() { try { return new FlatXmlDataSetBuilder().build(new File("src/test/resources/datasets/country.xml")); } catch (MalformedURLException | DataSetException e) { throw new RuntimeException(e); } } @Test public void shouldFindCountryWhenCodeIsKnown() { Country persistantCountry = countryRepository.findCountryByCode("FRA"); assertThat(persistantCountry.getName()).isEqualTo("France"); } }
Comment se passer de Runner Junit?
Nous avons vu comment nous passer de l'abstraction. Allons plus loin avec le Runner. Le Runner Junit utilisé dans les tests est là pour initier un contexte Spring pour ensuite injecter via un autowire les éléments à tester.
En attendant que Spring intègre dans le projet spring-test une Rule vous pouvez la créer vous même
public class SpringContextRule extends ExternalResource{ /** Fichiers de conf spring */ private final Class[] configs; /** Classe de test sur laquelle on va lancer l'instrumentation */ private final Object classToAutowire; /** Contexte Spring */ private final AnnotationConfigApplicationContext context; public SpringContextRule(Object classToAutowire, Class ... configs) { this.classToAutowire = classToAutowire; this.configs = configs; this.context = new AnnotationConfigApplicationContext(); } @Override protected void before() throws Throwable { context.register(configs); context.refresh(); context.getAutowireCapableBeanFactory().autowireBean(classToAutowire); context.start(); } @Override protected void after() { context.close(); } public ApplicationContext getContext() { return context; } }
Notre classe de tests devient encore plus simple
public class DbuniCibleTest { @Autowired private CountryRepository countryRepository; @Rule public DbUnitRule rule = new DbUnitRule(readDataSet());
@Rule public SpringContextRule rule = new SpringContextRule(this, PersistenceConfig.class);
protected IDataSet readDataSet() { try { return new FlatXmlDataSetBuilder().build(new File("src/test/resources/datasets/country.xml")); } catch (MalformedURLException | DataSetException e) { throw new RuntimeException(e); } } @Test public void shouldFindCountryWhenCodeIsKnown() { Country persistantCountry = countryRepository.findCountryByCode("FRA"); assertThat(persistantCountry.getName()).isEqualTo("France"); } }
Comment chainer des Rules ?
On pourrait maintenant se dire qu'une Rule pourrait dépendre d'une autre. Pour oublier un peu DbUnit je parlerai d'une solution alternative plus élégante à mon sens qui se nomme DbSetup. Si vous ne connaissez pas DbSetup je vous laisse lire la documentation en ligne qui contient tout ce qu'il y a savoir.
DbSetup utilise une datasource. Si vous faites du Spring cette dernière est déjà initialisée dans le contexte et il serait intéressant de la récupérer. Comment pourrait t-on écrire une Rule récupérant le context défini dans une autre? C'est là que rentre en jeu les RuleChain.
Passons à l'exemple la Rule DbSetup est la suivante
public class DbSetupRule extends ExternalResource { protected Operation init; protected ApplicationContext applicationContext; public DbSetupRule(ApplicationContext applicationContext, Operation init) { this.applicationContext = applicationContext; this.init = init; } @Override protected void before() throws Throwable { DbSetup dbSetup = new DbSetup(new DataSourceDestination(applicationContext.getBean(DataSource.class)), init); dbSetup.launch(); } }
Et la classe de test...
public class CountryRepositoryDbSetupTest { private static Operation init = Operations.sequenceOf( deleteAllFrom("country"), insertInto("country") .columns("id", "code", "name") .values(1, "FRA", "France") .values(2, "USA", "United States") .build() ); public SpringContextRule springContextRule = new SpringContextRule(this, PersistenceConfig.class); @Rule public TestRule testRule = RuleChain.outerRule(springContextRule).around(new DbSetupRule(springContextRule.getContext(), init)); @Autowired private CountryRepository countryRepository; @Test public void shouldFindCountryWhenCodeIsKnown() { Country persistantCountry = countryRepository.findCountryByCode("FRA"); assertThat(persistantCountry.getName()).isEqualTo("France"); } }
D'autres exemples de Rules ?
Plusieurs Rules sont fournies par Junit : ErrorCollector, ExpectedException, TemporaryFolder, TestWatcher. On les retrouve souvent reprises dans les blogs fournissant des exemples d'implémentation des Rules. Ces Rules n'ont d'intérêt que dans un nombre de uses cases limités et je pense que c'est une des raisons pour laquelle les Rules restent encore trop inutilisées.
Je n'en parlerai pas ici mais quelques exemples sont disponibles dans mon projet exemple sous Github.
Je ne sais pas si je vous ai convaincu mais si vous souhaitez mutualiser du comportement entre des tests gardez à l'esprit que les Rules peuvent répondre à votre besoin.
Aucun commentaire:
Enregistrer un commentaire
Remarque : Seul un membre de ce blog est autorisé à enregistrer un commentaire.