Serenity BDD with Spring and JBehave – 使用Spring和JBehave的Serenity BDD

最后修改: 2017年 5月 22日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.介绍

Previously, we have introduced the Serenity BDD framework.

在此之前,我们已经介绍了Serenity BDD框架

In this article, we’ll introduce how to integrate Serenity BDD with Spring.

在这篇文章中,我们将介绍如何将Serenity BDD与Spring集成。

2. Maven Dependency

2.Maven的依赖性

To enable Serenity in our Spring project, we need to add serenity-core and serenity-spring to the pom.xml:

为了在我们的Spring项目中启用Serenity,我们需要在pom.xml中添加a href=”https://search.maven.org/classic/#artifactdetails%7Cnet.serenity-bdd%7Cserenity-core%7C1.4.0%7Cjar”>serenity-corea href=”https://search.maven.org/classic/#artifactdetails%7Cnet.serenity-bdd%7Cserenity-spring%7C1.4.0%7Cjar”>serenity-spring

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

We also need to configure the serenity-maven-plugin, which is important for generating Serenity test reports:

我们还需要配置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 Integration

3.Spring集成

Spring integration test needs to @RunWith SpringJUnit4ClassRunner. But we cannot use the test runner directly with Serenity, as Serenity tests need to be run by SerenityRunner.

Spring集成测试需要@RunWith SpringJUnit4ClassRunner。但是我们不能直接使用Serenity的测试运行器,因为Serenity测试需要由SerenityRunner运行。

For tests with Serenity, we can use SpringIntegrationMethodRule and SpringIntegrationClassRule to enable injection.

对于使用Serenity的测试,我们可以使用SpringIntegrationMethodRuleSpringIntegrationClassRule来启用注入。

We’ll base our test on a simple scenario: given a number, when adding another number, then returns the sum.

我们将基于一个简单的场景进行测试:给定一个数字,当加入另一个数字时,然后返回总和。

3.1. SpringIntegrationMethodRule

3.1.SpringIntegrationMethodRule

SpringIntegrationMethodRule is a MethodRule applied to the test methods. The Spring context will be built before @Before and after @BeforeClass.

SpringIntegrationMethodRule是一个MethodRule应用于测试方法。Spring上下文将在@Before之前和@BeforeClass之后被构建。

Suppose we have a property to inject in our beans:

假设我们有一个属性要注入到我们的bean中。

<util:properties id="props">
    <prop key="adder">4</prop>
</util:properties>

Now let’s add SpringIntegrationMethodRule to enable the value injection in our test:

现在让我们添加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(); 
    }
}

It also supports method level annotations of spring test. If some test method dirties the test context, we can mark @DirtiesContext on it:

它还支持spring test的方法级注释。如果某个测试方法弄脏了测试上下文,我们可以在其上标记@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();
    }

}

In the example above, when we invoke adderServiceSteps.whenAccumulate(), the base number field of the @Service injected in adderServiceSteps will be changed:

在上面的例子中,当我们调用adderServiceSteps.whenAccumulate()时,在adderServiceSteps中注入的@Service的基数字段将被改变。

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

}

Specifically, we assign the sum to the base number:

具体来说,我们把总和分配给基数。

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

In the first test _0_givenNumber_whenAddAndAccumulate_thenSummedUp, the base number is changed, making the context dirty. When we try to add another number, we won’t get an expected sum.

在第一个测试_0_givenNumber_whenAddAndAccumulate_thenSummedUp中,基数被改变,使上下文变脏。当我们试图添加另一个数字时,我们不会得到一个预期的总和。

Notice that even if we marked the first test with @DirtiesContext, the second test is still affected: after adding, the sum is still wrong. Why?

请注意,即使我们用@DirtiesContext标记了第一个测试,第二个测试仍然受到影响:添加后,总和仍然是错误的。为什么呢?

Now, while processing method level @DirtiesContext, Serenity’s Spring integration only rebuild the test context for the current test instance. The underlying dependency context in @Steps will not be rebuilt.

现在,在处理方法层@DirtiesContext时,Serenity的Spring集成只重建当前测试实例的测试上下文。@Steps中的底层依赖上下文将不会被重建。

To work around this problem, we can inject the @Service in our current test instance, and make service as an explicit dependency of @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;
    }

    // ...
}

Or we can put the condition initialisation step in the @Before section to avoid dirty context. But this kind of solution may not be available in some complex situations.

或者我们可以把条件初始化步骤放在@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

3.2.SpringIntegrationClassRule

To enable class level annotations, we should use SpringIntegrationClassRule. Say we have the following test classes; each dirties the context:

为了启用类级注释,我们应该使用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();
    }
}

In this example, all implicit injections will be rebuilt for class level @DirtiesContext.

在这个例子中,所有隐式注入将被重建为类级@DirtiesContext

3.3. SpringIntegrationSerenityRunner

3.3.SpringIntegrationSerenityRunner

There is a convenient class SpringIntegrationSerenityRunner that automatically adds both integration rules above. We can run tests above with this runner to avoid specifying the method or class test rules in our test:

有一个方便的类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 Integration

4.SpringMVC集成

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.

在我们只需要用Serenity测试SpringMVC组件的情况下,我们可以简单地利用rest-assured中的RestAssuredMockMvc而不是serenity-spring集成。

4.1. Maven Dependency

4.1.Maven的依赖性

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

我们需要将rest-assured 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 in Action

4.2.RestAssuredMockMvc在行动中

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:

我们可以像这样利用RestAssuredMockMvc的MVC-mocking实用工具。

@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:

那么剩下的部分与我们使用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

5 宁静,JBehave,和Spring

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

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

We can implement the logics in a @Service and expose the actions via APIs:

我们可以在@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);
    }
}

Now we can build Serenity-JBehave test with the help of RestAssuredMockMvc as follows:

现在我们可以在RestAssuredMockMvc的帮助下建立Serenity-JBehave测试,如下。

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

We can only mark SerenityStory with @ContextConfiguration, then Spring injection is enabled automatically. This works quite the same as the @ContextConfiguration on @Steps.

我们只能用@ContextConfiguration来标记SerenityStory,然后自动启用Spring注入。这与@ContextConfiguration@Steps上的作用是完全一样的。

6. Summary

6.总结

In this article, we covered on how to integrate Serenity BDD with Spring. The integration is not quite perfect, but it’s definitely getting there.

在这篇文章中,我们介绍了如何将Serenity BDD与Spring集成。这种整合还不是很完美,但肯定会有进展的。

As always, the full implementation can be found over on the GitHub project.

一如既往,完整的实现可以在GitHub项目上找到。