Serenity BDD avec Spring et JBehave

Serenity BDD avec Spring et JBehave

1. introduction

Auparavant, nous avonsintroduced the Serenity BDD framework.

Dans cet article, nous allons vous présenter comment intégrer Serenity BDD à Spring.

2. Dépendance Maven

Pour activer Serenity dans notre projet Spring, nous devons ajouterserenity-core etserenity-spring auxpom.xml:


    net.serenity-bdd
    serenity-core
    1.4.0
    test


    net.serenity-bdd
    serenity-spring
    1.4.0
    test

Nous devons également configurer lesserenity-maven-plugin, ce 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 Spring

Le test d'intégration de ressort doit être@RunWithSpringJUnit4ClassRunner. Mais nous ne pouvons pas utiliser le testeur directement avec Serenity, car les tests Serenity doivent être exécutés parSerenityRunner.

Pour les tests avec Serenity, nous pouvons utiliserSpringIntegrationMethodRule etSpringIntegrationClassRule pour activer l'injection.

Nous allons baser notre test sur un scénario simple: étant donné un nombre, lors de l’ajout d’un autre nombre, il renvoie la somme.

3.1. SpringIntegrationMethodRule

SpringIntegrationMethodRule est unMethodRule appliqué 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 haricots:


    4

Ajoutons maintenantSpringIntegrationMethodRule 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 despring test. Si une méthode de test salit le contexte de test, nous pouvons marquer@DirtiesContext dessus:

@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 invoquonsadderServiceSteps.whenAccumulate(), le champ du numéro de base des@Service injectés dansadderServiceSteps 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 affectons 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'obtenons pas la 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. Why?

Désormais, lors du traitement du niveau de 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 les@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 utiliserSpringIntegrationClassRule. Disons que nous avons les classes de test suivantes; chacun salies 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 pratiqueSpringIntegrationSerenityRunner qui ajoute automatiquement les deux règles d'intégration ci-dessus. Nous pouvons exécuter des tests ci-dessus avec ce programme afin d'éviter de spécifier les règles de test de la méthode ou de la 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 utiliserRestAssuredMockMvc dansrest-assured au lieu de l'intégrationserenity-spring.

4.1. Dépendance Maven

Nous devons ajouter la dépendancerest-assured spring-mock-mvc auxpom.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 de simulation MVC deRestAssuredMockMvc 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 utilisonsrest-assured:

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

La prise en charge de l'intégration Spring de Serenity fonctionne de manière transparente avecJBehave. É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 construire le test Serenity-JBehave à l'aide deRestAssuredMockMvc 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 marquerSerenityStory qu'avec@ContextConfiguration, alors l'injection de ressort est activée automatiquement. Cela fonctionne de la même manière que les@ContextConfiguration sur@Steps.

6. Sommaire

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 surthe GitHub project.