知識庫 / Spring RSS 訂閱

Serenity BDD 與 Spring 和 JBehave 結合使用

Spring,Testing
HongKong
6
02:33 PM · Dec 06 ,2025

1. 引言

之前,我們已經介紹了 Serenity BDD 框架。

在本文中,我們將介紹如何將 Serenity BDD 與 Spring 集成。

2. Maven 依賴

為了在我們的 Spring 項目中啓用 Serenity,我們需要將 <a href="https://mvnrepository.com/artifact/net.serenity-bdd/serenity-core">serenity-core</a><a href="https://mvnrepository.com/artifact/net.serenity-bdd/serenity-spring">serenity-spring</a> 添加到 <em>pom.xml</em> 中:

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

Spring Integration 測試需要使用 <em @RunWith</em> 標記以及 <a href="https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.html"><em>SpringJUnit4ClassRunner</em></a>。 但是,我們不能直接使用測試運行器與 Serenity 配合使用,因為 Serenity 測試需要通過SerenityRunner` 運行。

對於使用 Serenity 的測試,我們可以使用 <em>SpringIntegrationMethodRule</em><em>SpringIntegrationClassRule</em> 來啓用注入。

我們將基於一個簡單的場景進行測試:給定一個數字,當添加另一個數字時,則返回它們的和。

3.1. <em>SpringIntegrationMethodRule</em>

<em>SpringIntegrationMethodRule</em> 是一個應用於測試方法的 <a href="http://junit.org/junit4/javadoc/4.12/org/junit/rules/MethodRule.html"><em>MethodRule</em></a>。 Spring上下文將在@Before@BeforeClass之前構建,並在之後銷燬。

假設我們有一個需要在 Bean 中注入的屬性:

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

它還支持方法級別的註解,即 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();
    }

}

在上面的示例中,當我們調用 <em>adderServiceSteps.whenAccumulate()</em> 時,@Service 註解中注入到 adderServiceSteps 中的 base number 字段將被修改:

@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. <em>SpringIntegrationClassRule</em>

為了啓用類級別的註解,我們應該使用 <em>SpringIntegrationClassRule</em>。 如下所示的測試類會污染上下文:

@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 集成

在僅需使用 Serenity 測試 SpringMVC 組件的情況下,我們可以直接利用 RestAssuredMockMvc 在 RestAssured 中,而不是使用 serenity-spring 集成。

4.1. Maven 依賴

我們需要將 io.rest-assured/spring-mock-mvc 依賴添加到 `pom.xml</em/> 中:

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>spring-mock-mvc</artifactId>
    <version>5.3.0</version>
    <scope>test</scope>
</dependency>

4.2. RestAssuredMockMvc 在 Action

現在,我們來測試下面的控制器:

@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 的 MVC 模擬實用工具,例如如下所示:

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

現在我們可以藉助 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();
    }
}

我們只能使用 @ContextConfiguration 來標記 SerenityStory,然後 Spring 注入會自動啓用。這與 @ContextConfiguration@Steps 上的使用方式完全相同。

6. 總結

本文介紹瞭如何將 Serenity BDD 與 Spring 集成。雖然集成尚未完全完善,但正在不斷改進中。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.