Serenity BDD con Spring e JBehave

1. Introduzione

In precedenza, abbiamo introdotto il framework Serenity BDD.

In questo articolo, introdurremo come integrare Serenity BDD con Spring.

2. Dipendenza da Maven

Per abilitare Serenity nel nostro progetto Spring, dobbiamo aggiungere serenity-core e serenity-spring al pom.xml :

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

Abbiamo anche bisogno di configurare il plugin serenity-maven , che è importante per generare rapporti di test Serenity:

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

3. Spring Integration

Il test di integrazione di primavera deve essere @RunWith SpringJUnit4ClassRunner . Ma non possiamo utilizzare il test runner direttamente con Serenity, poiché i test Serenity devono essere eseguiti da SerenityRunner .

Per i test con Serenity, possiamo usare SpringIntegrationMethodRule e SpringIntegrationClassRule per abilitare l'iniezione.

Baseremo il nostro test su uno scenario semplice: dato un numero, quando si aggiunge un altro numero, restituisce la somma.

3.1. SpringIntegrationMethodRule

SpringIntegrationMethodRule è una MethodRule applicata ai metodi di test. Il contesto Spring verrà creato prima di @Before e dopo @BeforeClass .

Supponiamo di avere una proprietà da iniettare nei nostri fagioli:

 4 

Ora aggiungiamo SpringIntegrationMethodRule per abilitare l'iniezione di valore nel nostro 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(); } }

Supporta anche le annotazioni a livello di metodo del test della molla . Se qualche metodo di test sporca il contesto del test, possiamo contrassegnare @DirtiesContext su di esso:

@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(); } }

Nell'esempio sopra, quando invochiamo adderServiceSteps.whenAccumulate () , il campo del numero di base del @Service iniettato in adderServiceSteps verrà modificato:

@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); } }

Nello specifico, assegniamo la somma al numero 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; } }

Nel primo test _0_givenNumber_whenAddAndAccumulate_thenSummedUp , il numero di base viene modificato, rendendo il contesto sporco. Quando proviamo ad aggiungere un altro numero, non otterremo una somma prevista.

Si noti che anche se abbiamo contrassegnato il primo test con @DirtiesContext , il secondo test è ancora interessato: dopo l'aggiunta, la somma è ancora errata. Perché?

Ora, durante l'elaborazione del livello di metodo @DirtiesContext , l'integrazione Spring di Serenity ricostruisce solo il contesto di test per l'istanza di test corrente. Il contesto di dipendenza sottostante in @Steps non verrà ricostruito.

Per aggirare questo problema, possiamo iniettare @Service nella nostra attuale istanza di test e rendere il servizio come una dipendenza esplicita di @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; } // ... }

Oppure possiamo inserire il passaggio di inizializzazione della condizione nella sezione @Before per evitare un contesto sporco. Ma questo tipo di soluzione potrebbe non essere disponibile in alcune situazioni complesse.

@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

Per abilitare le annotazioni a livello di classe, dovremmo usare SpringIntegrationClassRule . Supponiamo di avere le seguenti classi di test; ognuno sporca il contesto:

@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(); } }

In questo esempio, tutte le iniezioni implicite verranno ricostruite per il livello di classe @DirtiesContext .

3.3. SpringIntegrationSerenityRunner

Esiste una comoda classe SpringIntegrationSerenityRunner che aggiunge automaticamente entrambe le regole di integrazione sopra. Possiamo eseguire i test sopra con questo runner per evitare di specificare il metodo o le regole del test di classe nel nostro 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. Integrazione SpringMVC

Nei casi in cui abbiamo solo bisogno di testare i componenti SpringMVC con Serenity, possiamo semplicemente utilizzare RestAssuredMockMvc per stare tranquilli invece dell'integrazione serenità-primavera .

4.1. Dipendenza da Maven

Dobbiamo aggiungere la sicura dipendenza spring-mock-mvc al pom.xml :

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

4.2. RestAssuredMockMvc in azione

Proviamo ora il seguente controller:

@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; } }

Possiamo trarre vantaggio dalle utility MVC- mocking di RestAssuredMockMvc in questo modo:

@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(); } }

Quindi la parte del resto non è diversa da come usiamo tranquilli :

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 e Spring

Il supporto per l'integrazione di Serenity Spring funziona perfettamente con JBehave. Scriviamo il nostro scenario di test come una storia di 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

Possiamo implementare le logiche in un @Service ed esporre le azioni tramite 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); } }

Ora possiamo costruire il test Serenity-JBehave con l'aiuto di RestAssuredMockMvc come segue:

@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(); } }

Possiamo contrassegnare SerenityStory solo con @ContextConfiguration , quindi l' inserimento di Spring viene abilitato automaticamente. Funziona allo stesso modo di @ContextConfiguration su @Steps .

6. Riepilogo

In questo articolo abbiamo spiegato come integrare Serenity BDD con Spring. L'integrazione non è del tutto perfetta, ma ci sta sicuramente arrivando.

Come sempre, l'implementazione completa può essere trovata nel progetto GitHub.