Serenity BDD с Spring и JBehave

1. Въведение

Преди това въведохме рамката на Serenity BDD.

В тази статия ще ви представим как да интегрирате Serenity BDD с Spring.

2. Зависимост на Maven

За да активираме Serenity в нашия проект Spring, трябва да добавим serenity-core и serenity-spring към pom.xml :

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

Също така трябва да конфигурираме приставката serenity-maven , което е важно за генерирането на отчети за тест Serenity:

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

3. Пролетна интеграция

Тестът за пролетна интеграция трябва да @RunWith SpringJUnit4ClassRunner . Но не можем да използваме тестовия бегач директно със Serenity, тъй като тестовете за Serenity трябва да се изпълняват от SerenityRunner .

За тестове със Serenity можем да използваме SpringIntegrationMethodRule и SpringIntegrationClassRule, за да активираме инжектирането.

Ние ще базираме нашия тест на прост сценарий: дадено число, при добавяне на друго число, след това връща сумата.

3.1. SpringIntegrationMethodRule

SpringIntegrationMethodRule е MethodRule, приложен към методите за тестване. Пролетният контекст ще бъде изграден преди @Before и след @BeforeClass .

Да предположим, че имаме свойство да инжектираме в нашия боб:

 4 

Сега нека добавим SpringIntegrationMethodRule, за да активираме инжектирането на стойност в нашия тест:

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

Той също така поддържа анотации на ниво метод за пролетен тест . Ако някой метод за изпитване замърси контекста на теста, можем да маркираме @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(); } }

В горния пример, когато извикаме adderServiceSteps.whenAccumulate () , полето на базовия номер на @Service, инжектирано в adderServiceSteps, ще бъде променено:

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

По-конкретно, присвояваме сумата на базовия номер:

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

В първия тест _0_givenNumber_whenAddAndAccumulate_thenSummedUp , базовият номер се променя, което прави контекста мръсен. Когато се опитаме да добавим друго число, няма да получим очаквана сума.

Забележете, че дори ако маркирахме първия тест с @DirtiesContext , вторият тест все още е засегнат: след добавяне сумата все още е грешна. Защо?

Сега, докато обработваме метод на ниво @DirtiesContext , интеграцията на Serenity's Spring само възстановява тестовия контекст за текущия тестов екземпляр. Основният контекст на зависимостта в @Steps няма да бъде възстановен.

За да заобиколим този проблем, можем да инжектираме @Service в текущия ни тестов екземпляр и да направим услугата като изрична зависимост на @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; } // ... }

Или можем да поставим стъпката за инициализиране на условието в раздела @Before, за да избегнем мръсен контекст. Но този вид решение може да не е налице в някои сложни ситуации.

@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

За да активираме анотациите на ниво клас, трябва да използваме SpringIntegrationClassRule . Да кажем, че имаме следните тестови класове; всеки замърсява контекста:

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

В този пример всички неявни инжекции ще бъдат възстановени за ниво на класа @DirtiesContext .

3.3. SpringIntegrationSerenityRunner

Има удобен клас SpringIntegrationSerenityRunner, който автоматично добавя двете правила за интеграция по-горе. Можем да стартираме тестове по-горе с този бегач, за да избегнем определянето на метода или правилата за тестване на класа в нашия тест:

@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 интеграция

В случаите, когато трябва да тестваме само компоненти SpringMVC със Serenity, ние можем просто да използваме RestAssuredMockMvc в спокойствие вместо интеграцията на serenity-spring .

4.1. Зависимост на Maven

Трябва да добавим спокойната зависимост spring-mock-mvc към pom.xml :

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

4.2. RestAssuredMockMvc в действие

Нека сега тестваме следния контролер:

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

Можем да се възползваме от MVC-присмехулните помощни програми на RestAssuredMockMvc по следния начин:

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

Тогава частта за почивка не се различава от това как използваме спокойствие :

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. Спокойствие, JBehave и Spring

Поддръжката на Serenity Spring за интеграция работи безпроблемно с JBehave. Нека напишем нашия тестов сценарий като 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

Можем да приложим логиките в @Service и да изложим действията чрез 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); } }

Сега можем да изградим тест Serenity-JBehave с помощта на RestAssuredMockMvc, както следва:

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

Можем да маркираме SerenityStory само с @ContextConfiguration , тогава инжектирането на Spring се активира автоматично. Това работи по същия начин като @ContextConfiguration на @Steps .

6. Обобщение

В тази статия разгледахме как да интегрираме Serenity BDD с Spring. Интеграцията не е съвсем перфектна, но определено стига до там.

Както винаги, пълното внедряване може да бъде намерено в проекта GitHub.