1. 引言

之前我们介绍过 Serenity BDD 框架。本文将深入探讨如何将 Serenity BDD 与 Spring 框架集成,实现更强大的测试能力。

2. Maven 依赖

要在 Spring 项目中启用 Serenity,需要在 pom.xml 中添加以下依赖:

<dependency>
    <groupId>net.serenity-bdd</groupId>
    <artifactId>serenity-core</artifactId>
    <version>1.9.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>net.serenity-bdd</groupId>
    <artifactId>serenity-spring</artifactId>
    <version>1.9.0</version>
    <scope>test</scope>
</dependency>

还需要配置 serenity-maven-plugin,这对生成 Serenity 测试报告至关重要:

<plugin>
    <groupId>net.serenity-bdd.maven.plugins</groupId>
    <artifactId>serenity-maven-plugin</artifactId>
    <version>4.0.18</version>
    <executions>
        <execution>
            <id>serenity-reports</id>
            <phase>post-integration-test</phase>
            <goals>
                <goal>aggregate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

3. Spring 集成

Spring 集成测试通常使用 @RunWith(SpringJUnit4ClassRunner.class),但 Serenity 测试必须由 SerenityRunner 执行。为解决冲突,可使用以下规则:

3.1. SpringIntegrationMethodRule

SpringIntegrationMethodRule 是一个 MethodRule,在 @Before 之后、@BeforeClass 之前构建 Spring 上下文。

假设有需要注入的属性:

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

在测试中添加规则启用注入:

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

问题根源:Serenity 的 Spring 集成在处理方法级 @DirtiesContext 时,只会重建当前测试实例的上下文,不会重建 @Steps 中的依赖上下文。

解决方案

  1. 显式注入依赖:

    @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);
     }
    }
    
  2. @Before 中初始化状态:

    @Before
    public void init() {
     adderServiceSteps.givenBaseAndAdder(randomInt(), randomInt());
    }
    

3.2. SpringIntegrationClassRule

支持类级别注解(如 @DirtiesContext(classMode = AFTER_CLASS)):

@RunWith(SerenityRunner.class)
@ContextConfiguration(classes = AdderService.class)
public static abstract class Base {

    @Steps AdderServiceSteps adderServiceSteps;

    @ClassRule public static SpringIntegrationClassRule springIntegrationClassRule = new SpringIntegrationClassRule();

    // 测试方法...
}

@DirtiesContext(classMode = AFTER_CLASS)
public static class DirtiesContextIntegrationTest extends Base {
    // 测试实现...
}

优势:类级别的 @DirtiesContext 会重建所有隐式注入的依赖。

3.3. 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 组件时,可直接使用 RestAssuredMockMvc 替代 serenity-spring 集成。

4.1. Maven 依赖

添加 spring-mock-mvc 依赖:

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>spring-mock-mvc</artifactId>
    <version>5.3.0</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;
    }
}

使用 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("获取当前数字")
    public void givenCurrentNumber() throws UnsupportedEncodingException {
        currentNum = Integer.valueOf(given()
          .when()
          .get("/adder/current")
          .mvcResult()
          .getResponse()
          .getContentAsString());
    }

    @Step("添加数字 {0}")
    public void whenAddNumber(int num) {
        mockMvcResponse = given()
          .queryParam("num", num)
          .when()
          .post("/adder");
        currentNum += num;
    }

    @Step("验证求和结果")
    public void thenSummedUp() {
        mockMvcResponse
          .then()
          .statusCode(200)
          .body(equalTo(currentNum + ""));
    }
}

5. Serenity、JBehave 与 Spring 的结合

Serenity 的 Spring 集成与 JBehave 无缝协作。以下为 JBehave 故事示例:

Scenario: 用户可提交数字到加法器并获取结果
Given 一个数字
When 我提交另一个数字 5 到加法器
Then 我得到数字之和

实现逻辑:

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

测试实现:

@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("一个数字")
    public void givenANumber() throws Exception{
        restSteps.givenCurrentNumber();
    }

    @When("我提交另一个数字 $num 到加法器")
    public void whenISubmitToAdderWithNumber(int num){
        restSteps.whenAddNumber(num);
    }

    @Then("我得到数字之和")
    public void thenIGetTheSum(){
        restSteps.thenSummedUp();
    }
}

关键点:在 SerenityStory 上添加 @ContextConfiguration 即可自动启用 Spring 注入,与 @Steps 上的注解效果相同。

6. 总结

本文详细介绍了 Serenity BDD 与 Spring 框架的集成方案,涵盖:

  • 基础依赖配置
  • 三种 Spring 集成规则(方法级/类级/便捷运行器)
  • SpringMVC 测试的替代方案
  • 与 JBehave 的结合使用

虽然集成方案仍有改进空间,但已能满足大多数测试场景。完整实现代码可在 GitHub 项目 中获取。


原始标题:Serenity BDD with Spring and JBehave | Baeldung