Serenity BDD mit Spring und JBehave

1. Einleitung

Zuvor haben wir das Serenity BDD-Framework eingeführt.

In diesem Artikel stellen wir Ihnen vor, wie Sie Serenity BDD in Spring integrieren.

2. Maven-Abhängigkeit

Um Ruhe in unserem Frühlings - Projekt zu aktivieren, müssen wir hinzufügen Ruhe-Kern und Ruhe-Feder zum pom.xml :

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

Wir müssen auch das Serenity -Maven-Plugin konfigurieren , das für die Erstellung von Serenity-Testberichten wichtig ist:

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

3. Federintegration

Der Spring-Integrationstest muss @RunWith SpringJUnit4ClassRunner sein . Wir können den Testläufer jedoch nicht direkt mit Serenity verwenden, da Serenity-Tests von SerenityRunner ausgeführt werden müssen .

Für Tests mit Serenity können wir SpringIntegrationMethodRule und SpringIntegrationClassRule verwenden , um die Injektion zu aktivieren.

Wir werden unseren Test auf ein einfaches Szenario stützen: Wenn Sie eine Zahl angeben, geben Sie beim Hinzufügen einer weiteren Zahl die Summe zurück.

3.1. SpringIntegrationMethodRule

SpringIntegrationMethodRule ist eine MethodRule, die auf die Testmethoden angewendet wird. Der Spring-Kontext wird vor @Before und nach @BeforeClass erstellt .

Angenommen, wir haben eine Eigenschaft, die wir in unsere Bohnen injizieren können:

 4 

Fügen wir nun SpringIntegrationMethodRule hinzu , um die Wertinjektion in unserem Test zu aktivieren:

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

Es unterstützt auch Verfahren Ebene Annotationen Federtest . Wenn eine Testmethode den Testkontext verschmutzt, können wir @DirtiesContext darauf markieren :

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

Wenn wir im obigen Beispiel adderServiceSteps.whenAccumulate () aufrufen , wird das Basisnummernfeld des in adderServiceSteps injizierten @Service geändert:

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

Insbesondere weisen wir die Summe der Basisnummer zu:

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

Im ersten Test _0_givenNumber_whenAddAndAccumulate_thenSummedUp wird die Basisnummer geändert, wodurch der Kontext verschmutzt wird. Wenn wir versuchen, eine weitere Zahl hinzuzufügen, erhalten wir keine erwartete Summe.

Beachten Sie, dass auch wenn wir den ersten Test mit @DirtiesContext markiert haben , der zweite Test weiterhin betroffen ist: Nach dem Hinzufügen ist die Summe immer noch falsch. Warum?

Während der Verarbeitung der Methodenebene @DirtiesContext erstellt die Spring-Integration von Serenity nur den Testkontext für die aktuelle Testinstanz neu. Der zugrunde liegende Abhängigkeitskontext in @Steps wird nicht neu erstellt.

Um dieses Problem zu umgehen , können wir den @ Service in unsere aktuelle Testinstanz einfügen und den Service als explizite Abhängigkeit von @Steps festlegen :

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

Oder wir können den Schritt der Bedingungsinitialisierung in den Abschnitt @Before einfügen , um einen schmutzigen Kontext zu vermeiden. In einigen komplexen Situationen ist diese Art von Lösung möglicherweise nicht verfügbar.

@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

Um Annotationen auf Klassenebene zu aktivieren, sollten wir SpringIntegrationClassRule verwenden . Angenommen, wir haben die folgenden Testklassen; jedes verschmutzt den Kontext:

@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 diesem Beispiel werden alle impliziten Injektionen für @DirtiesContext auf Klassenebene neu erstellt .

3.3. SpringIntegrationSerenityRunner

Es gibt eine praktische Klasse SpringIntegrationSerenityRunner , die beide oben genannten Integrationsregeln automatisch hinzufügt. Wir können die oben genannten Tests mit diesem Läufer ausführen, um zu vermeiden, dass die Testregeln für Methoden oder Klassen in unserem Test angegeben werden:

@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. SpringMVC-Integration

In cases when we only need to test SpringMVC components with Serenity, we can simply make use of RestAssuredMockMvc in rest-assured instead of the serenity-spring integration.

4.1. Maven Dependency

We need to add the rest-assured spring-mock-mvc dependency to the pom.xml:

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

4.2. RestAssuredMockMvc in Action

Let's now test the following 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; } }

We can take advantage of the MVC-mocking utilities of RestAssuredMockMvc like this:

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

Then the rest part is no different from how we use rest-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, and Spring

Serenity's Spring integration support works seamlessly with JBehave. Let's write our test scenario as a JBehave story:

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

Wir können die Logik in einem @ Service implementieren und die Aktionen über APIs verfügbar machen :

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

Jetzt können wir den Serenity-JBehave-Test mit Hilfe von RestAssuredMockMvc wie folgt erstellen :

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

Wir können SerenityStory nur mit @ContextConfiguration markieren , dann wird die Federinjektion automatisch aktiviert. Dies funktioniert genauso wie die @ContextConfiguration auf @Steps .

6. Zusammenfassung

In diesem Artikel haben wir uns mit der Integration von Serenity BDD in Spring befasst. Die Integration ist nicht ganz perfekt, aber es kommt definitiv dahin.

Die vollständige Implementierung finden Sie wie immer im GitHub-Projekt.