1. 序章

以前、SerenityBDDフレームワークを導入しました。

この記事では、SerenityBDDをSpringと統合する方法を紹介します。

2. Mavenの依存関係

SpringプロジェクトでSerenityを有効にするには、serenity-coreserenity-springpom.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テストは SerenityRunner で実行する必要があるため、Serenityで直接テストランナーを使用することはできません。

Serenityを使用したテストでは、SpringIntegrationMethodRuleおよびSpringIntegrationClassRuleを使用して注入を有効にできます。

テストは単純なシナリオに基づいて行います。数値を指定し、別の数値を加算すると、合計が返されます。

3.1. SpringIntegrationMethodRule

SpringIntegrationMethodRule は、テストメソッドに適用されるMethodRuleです。 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(); 
    }
}

また、 springtestのメソッドレベルのアノテーションもサポートしています。 一部のテストメソッドがテストコンテキストを汚す場合は、@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()を呼び出すと、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);
    }

}

具体的には、合計を基数に割り当てます。

@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でマークした場合でも、2番目のテストは影響を受けます。追加した後も合計は間違っています。 なんで?

現在、メソッドレベル @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統合

Serenityを使用してSpringMVCコンポーネントのみをテストする必要がある場合は、 serenity-spring 統合の代わりに、rest-assuredRestAssuredMockMvcを使用できます。

4.1. Mavenの依存関係

rest-assuredspring-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;
    }
}

次のように、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. セレニティ、JBehave、そして春

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

SerenityStory@ContextConfigurationのマークを付けることしかできません。そうすると、Springインジェクションが自動的に有効になります。 これは、@Steps@ContextConfigurationとまったく同じように機能します。

6. 概要

この記事では、SerenityBDDをSpringと統合する方法について説明しました。 統合は完全ではありませんが、確実に実現しています。

いつものように、完全な実装はGitHubプロジェクトにあります。