1. Введение
Ранее мы представили фреймворк Serenity BDD .
В этой статье мы расскажем, как интегрировать Serenity BDD с Spring.
2. Зависимость от Maven
Чтобы включить Serenity в нашем проекте Spring, нам нужно добавить serenity-core
и serenity-spring
в pom.xml
:
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-core</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-spring</artifactId>
<version>1.4.0</version>
<scope>test</scope>
</dependency>
Также нам нужно настроить serenity-maven-plugin
, что важно для генерации тестовых отчетов Serenity:
<plugin>
<groupId>net.serenity-bdd.maven.plugins</groupId>
<artifactId>serenity-maven-plugin</artifactId>
<version>1.4.0</version>
<executions>
<execution>
<id>serenity-reports</id>
<phase>post-integration-test</phase>
<goals>
<goal>aggregate</goal>
</goals>
</execution>
</executions>
</plugin>
3. Весенняя интеграция
Интеграционный тест Spring должен быть @RunWith
SpringJUnit4ClassRunner
. Но мы не можем использовать средство запуска тестов напрямую с Serenity, так как тесты Serenity должны запускаться SerenityRunner
.
Для тестов с Serenity мы можем использовать SpringIntegrationMethodRule
и SpringIntegrationClassRule
для включения внедрения.
Мы будем основывать наш тест на простом сценарии: если задано число, при добавлении другого числа возвращается сумма.
3.1. SpringIntegrationMethodRule
SpringIntegrationMethodRule
— это правило MethodRule
, применяемое к методам тестирования. Контекст Spring будет создан до @Before
и после @BeforeClass
.
Предположим, у нас есть свойство для внедрения в наши компоненты:
<util:properties id="props">
<prop key="adder">4</prop>
</util:properties>
Теперь давайте добавим 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 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
в режиме rest-assured вместо интеграции с serenity -spring
.
4.1. Зависимость от Maven
Нам нужно добавить надежную зависимость spring-mock-mvc к pom.xml
:
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-mock-mvc</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
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();
}
}
Тогда остальная часть ничем не отличается от того, как мы используем 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 и 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 .