Serenity BDD avec Spring et JBehave

1. Introduction

Auparavant, nous avons introduit le framework Serenity BDD.

Dans cet article, nous présenterons comment intégrer Serenity BDD à Spring.

2. Dépendance de Maven

Pour activer Serenity dans notre projet Spring, nous devons ajouter serenity-core et serenity-spring au pom.xml :

 net.serenity-bdd serenity-core 1.4.0 test   net.serenity-bdd serenity-spring 1.4.0 test 

Nous devons également configurer le plugin serenity-maven-plugin , qui est important pour générer des rapports de test Serenity:

 net.serenity-bdd.maven.plugins serenity-maven-plugin 1.4.0   serenity-reports post-integration-test  aggregate    

3. Intégration de printemps

Le test d'intégration Spring doit @RunWith SpringJUnit4ClassRunner . Mais nous ne pouvons pas utiliser le testeur directement avec Serenity, car les tests Serenity doivent être exécutés par SerenityRunner .

Pour les tests avec Serenity, nous pouvons utiliser SpringIntegrationMethodRule et SpringIntegrationClassRule pour activer l'injection.

Nous baserons notre test sur un scénario simple: étant donné un nombre, lors de l'ajout d'un autre nombre, puis renvoie la somme.

3.1. SpringIntegrationMethodRule

SpringIntegrationMethodRule est une MethodRule appliquée aux méthodes de test. Le contexte Spring sera construit avant @Before et après @BeforeClass .

Supposons que nous ayons une propriété à injecter dans nos beans:

 4 

Ajoutons maintenant SpringIntegrationMethodRule pour activer l'injection de valeur dans notre test:

@RunWith(SerenityRunner.class) @ContextConfiguration(locations = "classpath:adder-beans.xml") public class AdderMethodRuleIntegrationTest { @Rule public SpringIntegrationMethodRule springMethodIntegration = new SpringIntegrationMethodRule(); @Steps private AdderSteps adderSteps; @Value("#{props['adder']}") private int adder; @Test public void givenNumber_whenAdd_thenSummedUp() { adderSteps.givenNumber(); adderSteps.whenAdd(adder); adderSteps.thenSummedUp(); } }

Il prend également en charge les annotations au niveau de la méthode du test de ressort . Si une méthode de test salit le contexte de test, nous pouvons y marquer @DirtiesContext :

@RunWith(SerenityRunner.class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @ContextConfiguration(classes = AdderService.class) public class AdderMethodDirtiesContextIntegrationTest { @Steps private AdderServiceSteps adderServiceSteps; @Rule public SpringIntegrationMethodRule springIntegration = new SpringIntegrationMethodRule(); @DirtiesContext @Test public void _0_givenNumber_whenAddAndAccumulate_thenSummedUp() { adderServiceSteps.givenBaseAndAdder(randomInt(), randomInt()); adderServiceSteps.whenAccumulate(); adderServiceSteps.summedUp(); adderServiceSteps.whenAdd(); adderServiceSteps.sumWrong(); } @Test public void _1_givenNumber_whenAdd_thenSumWrong() { adderServiceSteps.whenAdd(); adderServiceSteps.sumWrong(); } }

Dans l'exemple ci-dessus, lorsque nous appelons adderServiceSteps.whenAccumulate () , le champ du numéro de base du @Service injecté dans adderServiceSteps sera modifié:

@ContextConfiguration(classes = AdderService.class) public class AdderServiceSteps { @Autowired private AdderService adderService; private int givenNumber; private int base; private int sum; public void givenBaseAndAdder(int base, int adder) { this.base = base; adderService.baseNum(base); this.givenNumber = adder; } public void whenAdd() { sum = adderService.add(givenNumber); } public void summedUp() { assertEquals(base + givenNumber, sum); } public void sumWrong() { assertNotEquals(base + givenNumber, sum); } public void whenAccumulate() { sum = adderService.accumulate(givenNumber); } }

Plus précisément, nous attribuons la somme au nombre de base:

@Service public class AdderService { private int num; public void baseNum(int base) { this.num = base; } public int currentBase() { return num; } public int add(int adder) { return this.num + adder; } public int accumulate(int adder) { return this.num += adder; } }

Dans le premier test _0_givenNumber_whenAddAndAccumulate_thenSummedUp , le numéro de base est modifié, rendant le contexte sale. Lorsque nous essayons d'ajouter un autre nombre, nous n'obtiendrons pas une somme attendue.

Notez que même si nous avons marqué le premier test avec @DirtiesContext , le deuxième test est toujours affecté: après l'ajout, la somme est toujours erronée. Pourquoi?

Désormais, lors du traitement au niveau de la méthode @DirtiesContext , l'intégration Spring de Serenity ne reconstruit que le contexte de test pour l'instance de test actuelle. Le contexte de dépendance sous-jacent dans @Steps ne sera pas reconstruit.

Pour contourner ce problème, nous pouvons injecter le @Service dans notre instance de test actuelle et rendre le service comme une dépendance explicite de @Steps :

@RunWith(SerenityRunner.class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @ContextConfiguration(classes = AdderService.class) public class AdderMethodDirtiesContextDependencyWorkaroundIntegrationTest { private AdderConstructorDependencySteps adderSteps; @Autowired private AdderService adderService; @Before public void init() { adderSteps = new AdderConstructorDependencySteps(adderService); } //... }
public class AdderConstructorDependencySteps { private AdderService adderService; public AdderConstructorDependencySteps(AdderService adderService) { this.adderService = adderService; } // ... }

Ou nous pouvons mettre l'étape d'initialisation de la condition dans la section @Before pour éviter un contexte sale. Mais ce type de solution peut ne pas être disponible dans certaines situations complexes.

@RunWith(SerenityRunner.class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) @ContextConfiguration(classes = AdderService.class) public class AdderMethodDirtiesContextInitWorkaroundIntegrationTest { @Steps private AdderServiceSteps adderServiceSteps; @Before public void init() { adderServiceSteps.givenBaseAndAdder(randomInt(), randomInt()); } //... }

3.2. SpringIntegrationClassRule

Pour activer les annotations au niveau de la classe, nous devons utiliser SpringIntegrationClassRule . Disons que nous avons les classes de test suivantes; chacun salit le contexte:

@RunWith(SerenityRunner.class) @ContextConfiguration(classes = AdderService.class) public static abstract class Base { @Steps AdderServiceSteps adderServiceSteps; @ClassRule public static SpringIntegrationClassRule springIntegrationClassRule = new SpringIntegrationClassRule(); void whenAccumulate_thenSummedUp() { adderServiceSteps.whenAccumulate(); adderServiceSteps.summedUp(); } void whenAdd_thenSumWrong() { adderServiceSteps.whenAdd(); adderServiceSteps.sumWrong(); } void whenAdd_thenSummedUp() { adderServiceSteps.whenAdd(); adderServiceSteps.summedUp(); } }
@DirtiesContext(classMode = AFTER_CLASS) public static class DirtiesContextIntegrationTest extends Base { @Test public void givenNumber_whenAdd_thenSumWrong() { super.whenAdd_thenSummedUp(); adderServiceSteps.givenBaseAndAdder(randomInt(), randomInt()); super.whenAccumulate_thenSummedUp(); super.whenAdd_thenSumWrong(); } }
@DirtiesContext(classMode = AFTER_CLASS) public static class AnotherDirtiesContextIntegrationTest extends Base { @Test public void givenNumber_whenAdd_thenSumWrong() { super.whenAdd_thenSummedUp(); adderServiceSteps.givenBaseAndAdder(randomInt(), randomInt()); super.whenAccumulate_thenSummedUp(); super.whenAdd_thenSumWrong(); } }

Dans cet exemple, toutes les injections implicites seront reconstruites pour le niveau de classe @DirtiesContext .

3.3. SpringIntegrationSerenityRunner

Il existe une classe pratique SpringIntegrationSerenityRunner qui ajoute automatiquement les deux règles d'intégration ci-dessus. Nous pouvons exécuter les tests ci-dessus avec ce runner pour éviter de spécifier la méthode ou les règles de test de classe dans notre test:

@RunWith(SpringIntegrationSerenityRunner.class) @ContextConfiguration(locations = "classpath:adder-beans.xml") public class AdderSpringSerenityRunnerIntegrationTest { @Steps private AdderSteps adderSteps; @Value("#{props['adder']}") private int adder; @Test public void givenNumber_whenAdd_thenSummedUp() { adderSteps.givenNumber(); adderSteps.whenAdd(adder); adderSteps.thenSummedUp(); } }

4. Intégration SpringMVC

Dans les cas où nous n'avons besoin que de tester les composants SpringMVC avec Serenity, nous pouvons simplement utiliser RestAssuredMockMvc en toute tranquillité au lieu de l' intégration Serenity -Spring .

4.1. Dépendance de Maven

Nous devons ajouter la dépendance repos-assuré spring-mock-mvc au pom.xml :

 io.rest-assured spring-mock-mvc 3.0.3 test 

4.2. RestAssuredMockMvc en action

Testons maintenant le contrôleur suivant:

@RequestMapping(value = "/adder", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RestController public class PlainAdderController { private final int currentNumber = RandomUtils.nextInt(); @GetMapping("/current") public int currentNum() { return currentNumber; } @PostMapping public int add(@RequestParam int num) { return currentNumber + num; } }

Nous pouvons tirer parti des utilitaires moqueurs MVC de RestAssuredMockMvc comme ceci:

@RunWith(SerenityRunner.class) public class AdderMockMvcIntegrationTest { @Before public void init() { RestAssuredMockMvc.standaloneSetup(new PlainAdderController()); } @Steps AdderRestSteps steps; @Test public void givenNumber_whenAdd_thenSummedUp() throws Exception { steps.givenCurrentNumber(); steps.whenAddNumber(randomInt()); steps.thenSummedUp(); } }

Ensuite, le reste n'est pas différent de la façon dont nous utilisons rassurez-vous :

public class AdderRestSteps { private MockMvcResponse mockMvcResponse; private int currentNum; @Step("get the current number") public void givenCurrentNumber() throws UnsupportedEncodingException { currentNum = Integer.valueOf(given() .when() .get("/adder/current") .mvcResult() .getResponse() .getContentAsString()); } @Step("adding {0}") public void whenAddNumber(int num) { mockMvcResponse = given() .queryParam("num", num) .when() .post("/adder"); currentNum += num; } @Step("got the sum") public void thenSummedUp() { mockMvcResponse .then() .statusCode(200) .body(equalTo(currentNum + "")); } }

5. Serenity, JBehave et Spring

Le support d'intégration Spring de Serenity fonctionne de manière transparente avec JBehave. Écrivons notre scénario de test comme une histoire JBehave:

Scenario: A user can submit a number to adder and get the sum Given a number When I submit another number 5 to adder Then I get a sum of the numbers

Nous pouvons implémenter les logiques dans un @Service et exposer les actions via des API:

@RequestMapping(value = "/adder", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @RestController public class AdderController { private AdderService adderService; public AdderController(AdderService adderService) { this.adderService = adderService; } @GetMapping("/current") public int currentNum() { return adderService.currentBase(); } @PostMapping public int add(@RequestParam int num) { return adderService.add(num); } }

Nous pouvons maintenant créer le test Serenity-JBehave à l'aide de RestAssuredMockMvc comme suit:

@ContextConfiguration(classes = { AdderController.class, AdderService.class }) public class AdderIntegrationTest extends SerenityStory { @Autowired private AdderService adderService; @BeforeStory public void init() { RestAssuredMockMvc.standaloneSetup(new AdderController(adderService)); } }
public class AdderStory { @Steps AdderRestSteps restSteps; @Given("a number") public void givenANumber() throws Exception{ restSteps.givenCurrentNumber(); } @When("I submit another number $num to adder") public void whenISubmitToAdderWithNumber(int num){ restSteps.whenAddNumber(num); } @Then("I get a sum of the numbers") public void thenIGetTheSum(){ restSteps.thenSummedUp(); } }

Nous ne pouvons marquer SerenityStory qu'avec @ContextConfiguration , puis l'injection de printemps est activée automatiquement. Cela fonctionne de la même manière que la @ContextConfiguration sur @Steps .

6. Résumé

Dans cet article, nous avons expliqué comment intégrer Serenity BDD à Spring. L'intégration n'est pas tout à fait parfaite, mais elle y parvient définitivement.

Comme toujours, l'implémentation complète peut être trouvée sur le projet GitHub.