Tests BDD avec Cucumber Java

cucumber-logoCe tutorial explique comment écrire et coder des tests BDD (Behavior Driven Development) grâce à Cucumber pour Java, permettant des cas de test plus fonctionnels, maintenables par des MOA, afin de partager la responsabilité de la qualité et gagner en productivité.

Environnement

Cucumber Java, c’est quoi ? C’est un outil qui permet de tester. Il automatise les tests d’acceptation par un formalise axé sur le style de développement BDD et sur un vocabulaire précis (Given, When, Then …Etant donnée, Quand, Alors …Arrange, Act, Assert).

bdd
https://github.com/cucumber/cucumber/wiki/Feature-Introduction
Le besoin se situe entre le TDD/TU et le test d’acceptance, au coeur du fonctionnel.

screen-shot-2012-11-05-at-12-44-55-am

Installation de Cucumber…

http://cukes.info/install-cucumber-jvm.html

pom.xml (Maven)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>com.damienfremont.blog</groupId>
	<artifactId>20150129-test-cucumber_java</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>jar</packaging>
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<jdk.version>1.7</jdk.version>
	</properties>
	<dependencies>

		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.12</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.assertj</groupId>
			<artifactId>assertj-core</artifactId>
			<version>1.7.1</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>info.cukes</groupId>
			<artifactId>cucumber-junit</artifactId>
			<version>1.2.2</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>info.cukes</groupId>
			<artifactId>cucumber-java</artifactId>
			<version>1.2.2</version>
			<scope>test</scope>
		</dependency>

	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<configuration>
					<source>${jdk.version}</source>
					<target>${jdk.version}</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

…et du plugin eclipse pour la coloration syntaxique et le formatage de votre texte (optionnal)

http://cukes.info/cucumber-eclipse/

ScreenShot003

Code

Les tests vont porter sur un entrepôt de donnée (une simple DAO) autour de l’entité Personne.

Les étapes décrites sur le site de Cucumber sont reprises ici, mais pour du Java et avec un peu plus de Français.

ScreenShot002

http://cukes.info/

1. Describe behaviour in plain text

person-repository.feature
Feature: Entrepôt de données 'Personne'

  # EXEMPLE SIMPLE
  Scenario: Création
    Given L'entrepôt contient N Personnes
    When Je crée une Personne
    Then J'obtiens l'ID de la Personne créée et l'entrepôt contient plus de N Personnes

  # EXEMPLE SPECIFIQUE
  Scenario: Suppression
    Given L'entrepôt contient la Personnes Anakin Skywalker
    When Je supprime la Personne 1
    Then L'entrepôt contient moins de N Personnes

  # EXEMPLE AVEC SUBSTITUTION (SCENARIO OUTLINES + EXAMPLES)
  Scenario Outline: Lecture
    Given L'entrepôt contient N Personnes
    When Je recupère la Personne <id>
    Then J'obtiens la Personne d'identifiant <id> contenant les données <prenom>, <nom>, <naissance>

    Examples:
      | id | prenom | nom         | naissance |
      | 1  | Anakin | Skywalker   | 41.9 BBY  |
      | 2  | Luke   | Skywalker   | 19 BBY    |
      | 3  | Leia   | Organa Solo | 19 BBY    |

  # EXEMPLE AVEC DATA TABLES
  Scenario Outline: Modification
    Given L'entrepôt contient les Personnes suivantes
      | id | prenom | nom         | naissance |
      | 3  | Leia   | Organa Solo | 19 BBY    |
    When Je modifie la Personne <id> avec <nom>
    Then J'obtiens la Personne d'identifiant <id> contenant les données <prenom>, <nom>, <naissance>

    Examples:
      | id | prenom | nom                   | naissance |
      | 3  | Leia   | Organa Solo Skywalker | 19 BBY    |

Pour les détails de syntaxe, voir le site officiel.
http://cukes.info/step-definitions.html

2. Write a step definition in Java

Il faut absolument s’aider du feedback console de Cucumber (sinon c’est fastidieux et on rate quelques subtilités sur les caractères à échapper dans les annotations @Given, @When, @Then).
Pour cela il suffit de lancer vos scénarios et vous obtiendrez les squelettes de méthodes manquants.

RunBDDTest.java (le lanceur de tests)

@RunWith(Cucumber.class)
public class RunBDDTest {

}

ScreenShot002

ScreenShot006

Sortie Console :

6 Scenarios ([33m6 undefined[0m)
18 Steps ([33m18 undefined[0m)
0m0.000s

You can implement missing steps with the snippets below:

@Given("^L'entrepôt contient N Personnes$")
public void l_entrepôt_contient_N_Personnes() throws Throwable {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

@When("^Je crée une Personne$")
public void je_crée_une_Personne() throws Throwable {
    // Write code here that turns the phrase above into concrete actions
    throw new PendingException();
}

...

ATTENTION : le code généré n’est pas optimisé, surtout pour les tags <Examples>.
Avec un step écrit comme ceci…

When Je modifie la Personne <id> avec <nom> 

Examples:
  | numero | prenom | nom                   | dateDeNaissance |
  | 2      | Luke   | Maitre Jedi Skywalker | 19 BBY          |
  | 3      | Leia   | Organa Solo Skywalker | 19 BBY          |

…et pour cela de généré…

	@When("^Je modifie la Personne (\\d+) avec Organa Solo Skywalker$")
	public void je_modifie_la_Personne_avec_Organa_Solo_Skywalker(int arg1) throws Throwable {
	    // Write code here that turns the phrase above into concrete actions
	    throw new PendingException();
	}

	@When("^Je modifie la Personne (\\d+) avec Maitre Jedi Skywalker$")
	public void je_modifie_la_Personne_avec_Maitre_Jedi_Skywalker(int arg1) throws Throwable {
	    // Write code here that turns the phrase above into concrete actions
	    throw new PendingException();
	}

…il est possible de seulement écrire ça (réutilisable) :

@When("^Je modifie la Personne (\\d+) avec (.*)$")
public void je_modifie_la_Personne_avec_(int id, String nom) throws Throwable {
	// Write code here that turns the phrase above into concrete actions
	throw new PendingException();
}

Ce qui donne au final StepDefinitions.java

public class StepDefinitions {

	// CE QUI EST A TESTER

	private PersonRepository personRepositoryToTest = new PersonRepository();

	// DONNEES COMMUNES ENTRE STEPS

	private long givenPersonSize;
	private PersonModel whenPersonId;
	private PersonModel whenPerson;

	// # EXEMPLE SIMPLE

	@Given("^L'entrepôt contient N Personnes$")
	public void l_entrepôt_contient_N_Personnes() throws Throwable {
		// L'entrepôt contient N Personnes
		givenPersonSize = personRepositoryToTest.count();
		assertThat(givenPersonSize).isPositive();
	}

	@When("^Je crée une Personne$")
	public void je_crée_une_Personne() throws Throwable {
		// Je crée une Personne
		PersonModel person = new PersonModel();
		whenPersonId = personRepositoryToTest.create(person);
	}

	@Then("^J'obtiens l'ID de la Personne créée et l'entrepôt contient plus de N Personnes$")
	public void j_obtiens_l_ID_de_la_Personne_créée_et_l_entrepôt_contient_plus_de_N_Personnes()
			throws Throwable {
		// J'obtiens l'ID de la Personne créée
		assertThat(whenPersonId).isNotNull();
		// l'entrepôt contient N+X Personnes
		long thenPersonCount = personRepositoryToTest.count();
		assertThat(thenPersonCount).isGreaterThan(givenPersonSize);
	}

	// # EXEMPLE SPECIFIQUE AVEC ARGUMENTS

	@Given("^L'entrepôt contient la Personnes Anakin Skywalker$")
	public void l_entrepôt_contient_la_Personnes_Anakin_Skywalker()
			throws Throwable {
		givenPersonSize = personRepositoryToTest.count();
		// L'entrepôt contient la Personnes Anakin Skywalker
		PersonModel p = personRepositoryToTest.read(1);
		assertThat(p.getPrenom()).isEqualTo("Anakin");
	}

	@When("^Je supprime la Personne (\\d+)$")
	public void je_supprime_la_Personne(int arg1) throws Throwable {
		// Je supprime la Personne
		personRepositoryToTest.delete(arg1);
	}

	@Then("^L'entrepôt contient moins de N Personnes$")
	public void l_entrepôt_contient_moins_de_N_Personnes() throws Throwable {
		// L'entrepôt contient N-X Personnes
		assertThat(personRepositoryToTest.count()).isLessThan(givenPersonSize);
	}

	// # EXEMPLE AVEC SUBSTITUTION (SCENARIO OUTLINES + EXAMPLES)

	@When("^Je recupère la Personne (\\d+)$")
	public void je_recupère_la_Personne(int arg1) throws Throwable {
		// Je recupère la Personne
		whenPerson = personRepositoryToTest.read(arg1);
	}

	@Then("^J'obtiens la Personne d'identifiant (\\d+) contenant les données (.*), (.*), (.*)$")
	public void j_obtiens_la_Personne_d_identifiant_contenant_les_données(
			int arg1, String prenom, String nom, String naissance)
			throws Throwable {
		// J'obtiens la Personne d'identifiant
		assertThat(whenPerson).isNotNull();
		assertThat(whenPerson.getId()).isEqualTo(arg1);
		// avec les données
		assertThat(whenPerson.getPrenom()).isEqualTo(prenom);
		assertThat(whenPerson.getNom()).isEqualTo(nom);
		assertThat(whenPerson.getNaissance()).isEqualTo(naissance);
	}

	// # EXEMPLE AVEC DATA TABLES

	@Given("^L'entrepôt contient les Personnes suivantes$")
	public void l_entrepôt_contient_les_Personnes_suivantes(DataTable expected)
			throws Throwable {
		givenPersonSize = personRepositoryToTest.count();
		// L'entrepôt contient les Personnes suivantes
		List<PersonModel> actual = personRepositoryToTest.readAll();
		for (final PersonModel exp : expected.asList(PersonModel.class)) {
			assertThat(actual).haveExactly(1, new Condition<PersonModel>() {

				@Override
				public boolean matches(PersonModel act) {
					return act.getId().equals(exp.getId()) //
							&& act.getPrenom().equals(exp.getPrenom()) //
							&& act.getNom().equals(exp.getNom()) //
							&& act.getNaissance().equals(exp.getNaissance());
				}
			});
		}
	}

	@When("^Je modifie la Personne (\\d+) avec (.*)$")
	public void je_modifie_la_Personne_avec_(int id, String nom)
			throws Throwable {
		PersonModel p = personRepositoryToTest.read(id);
		p.setNom(nom);
		whenPerson = personRepositoryToTest.update(p);
	}

}

3. Run and watch it fail

RunBDDTest.java (le lanceur de tests)

@RunWith(Cucumber.class)
public class RunBDDTest {

}

ScreenShot002

ScreenShot007

4. Write code to make the step pass

PersonModel.java

public class PersonModel implements Serializable {

	private static final long serialVersionUID = 6879685199191377814L;

	private Integer id;
	private String prenom;
	private String nom;
	private String naissance;

	public PersonModel() {
	}

	public PersonModel(Integer id, String prenom, String nom, String naissance) {
		super();
		this.id = id;
		this.prenom = prenom;
		this.nom = nom;
		this.naissance = naissance;
	}

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getPrenom() {
		return prenom;
	}

	public void setPrenom(String prenom) {
		this.prenom = prenom;
	}

	public String getNom() {
		return nom;
	}

	public void setNom(String nom) {
		this.nom = nom;
	}

	public String getNaissance() {
		return naissance;
	}

	public void setNaissance(String naissance) {
		this.naissance = naissance;
	}

}

PersonRepository.java

public class PersonRepository {

	/* DATAS */

	private static Map<Integer, PersonModel> datas = null;

	public PersonRepository() {
		datas = new HashMap<>();
		datas.put(1, new PersonModel(1, "Anakin", "Skywalker", "41.9 BBY"));
		datas.put(2, new PersonModel(2, "Luke", "Skywalker", "19 BBY"));
		datas.put(3, new PersonModel(3, "Leia", "Organa Solo", "19 BBY"));
	}

	public long count() {
		return datas.size();
	}

	/* CRUD METHODS */

	public PersonModel create(PersonModel entity) {
		int randomNum = new Random().nextInt();
		entity.setId(randomNum);
		datas.put(entity.getId(), entity);
		return entity;
	}

	public PersonModel read(Integer id) {
		return datas.get(id);
	}

	public List<PersonModel> readAll() {
		return new ArrayList<PersonModel>(datas.values());
	}

	public PersonModel update(PersonModel entity) {
		return datas.put(entity.getId(), entity);
	}

	public void delete(Integer id) {
		datas.remove(id);
	}

}

5. Run again and see the step pass

ScreenShot005

6. Repeat 2-5 until green like a cuke

(idem)

Conclusion

Cucumber est un outils puissant, voir indispensable pour garantir une approche agile sur vos projet et leur bonne maintenance. Mais il faut garder à l’esprit que l’intérêt de ce genre de techno n’est clairement pas du coté des développeurs, mais des MOA. On n’écharpera pas aux commentaires du genre “c’est pas plus rapide à coder”, “ça me limite”.

Les plus :

  • un document commun avec votre MOA, dont elle doit avoir la responsabilité
  • gros gain de productivité sur les tests des cas aux limites d’un scénario
  • analyse de la couverture de code par des plugins comme Emma qui fonctionne toujours

Les moins :

  • un peu laborieux car travail très manuel (peu de compilation) mais la productivité est au rendez-vous
  • Pas d’erreur si un step n’est pas trouvé, juste un skip du test.
  • Pour un projet qui bouge beaucoup, Cucumber-JS ou Ruby semblent plus indiqués (voir problème de duplication de code ci-dessous sur un langage compilé comme Java), même si cela oblige à manipuler 2 contextes d’exécution (JS + la techno de votre projet, Java par exemple).
  • Feedbacks un peu léger pour les débutants sur les erreurs de syntax dans un fichier feature. Et le plugin Cucumber-Eclipse n’apporte rien de plus à ce niveau là.
Exemple : Caused by: gherkin.parser.ParseError: Parse error at com/damienfremont/test.feature:1. Found scenario_outline when expecting one of: comment, feature, tag. (Current getState: root).
  • Par contre le pendant Cucumber-Java est lourd à utiliser car il implique de la duplication de code par rapport à Cucumber-JS lors de l’étape de définition des steps.

Cucumber-Java (il faut taper un nom de méthode en plus)

 @Given("^I have entered (.*) into the calculator$")
 public void i_have_entered_into_the_calculator() throws Throwable {
 // express the regexp above with the code you wish you had
 }

Cucumber-JS

 Given( /^I have entered (.*) into the calculator$/, function(callback) {
 // express the regexp above with the code you wish you had
 callback.pending();
 });

Cucumber-RUBY

 Given /^I have entered (.*) into the calculator$/ do |arg1|
 pending # express the regexp above with the code you wish you had
 end

Attention, ce genre de test n’est pas fait pour des tests unitaires de briques logiciels. A privilégier plutôt pour des suites de tests fonctionnels, à rédiger en binôme avec vos responsables fonctionnels / MOA / PO.

Pour aller plus loin

Il faudrait ajouter du reporting plus propre pour la MOA avec Maven Surefire Report Plugin ou une page de rapport de server de build type Bamboo ou Jenkins.

Et un éditeur tel que Gherkin ou sa version collaborative pour permettre à des non-devs d’écrirent ces scénarios.

syntax-highlighting_thumb

https://github.com/cucumber/gherkin-editor

Source

https://github.com/damienfremont/blog/tree/master/20150129-test-cucumber_java

References

http://cukes.info/

http://cukes.info/install-cucumber-jvm.html

http://cukes.info/step-definitions.html

http://www.goodercode.com/wp/using-cucumber-tests-with-maven-and-java/

https://thomassundberg.wordpress.com/2014/05/29/cucumber-jvm-hello-world/

http://cukes.info/cucumber-eclipse/

2 thoughts on “Tests BDD avec Cucumber Java

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s