Expand Authors Top

If you have a few years of experience in the Java ecosystem and you’d like to share that with the community, have a look at our Contribution Guidelines.

Expanded Audience – Frontegg – Security (partner)
announcement - icon User management is very complex, when implemented properly. No surprise here.

Not having to roll all of that out manually, but instead integrating a mature, fully-fledged solution - yeah, that makes a lot of sense.
That's basically what Frontegg is - User Management for your application. It's focused on making your app scalable, secure and enjoyable for your users.
From signup to authentication, it supports simple scenarios all the way to complex and custom application logic.

Have a look:

>> Elegant User Management, Tailor-made for B2B SaaS

November Discount Launch 2022 – Top
We’re finally running a Black Friday launch. All Courses are 30% off until the end of this week:

>> GET ACCESS NOW

NPI – Lightrun – Spring (partner)

We rely on other people’s code in our own work. Every day. It might be the language you’re writing in, the framework you’re building on, or some esoteric piece of software that does one thing so well you never found the need to implement it yourself.

The problem is, of course, when things fall apart in production - debugging the implementation of a 3rd party library you have no intimate knowledge of is, to say the least, tricky. It’s difficult to understand what talks to what and, specifically, which part of the underlying library is at fault.

Lightrun is a new kind of debugger.

It's one geared specifically towards real-life production environments. Using Lightrun, you can drill down into running applications, including 3rd party dependencies, with real-time logs, snapshots, and metrics. No hotfixes, redeployments, or restarts required.

Learn more in this quick, 5-minute Lightrun tutorial:

>> The Essential List of Spring Boot Annotations and Their Use Cases

1. Introduction

Previously, we have introduced the Serenity BDD framework.

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

2. Maven Dependency

To enable Serenity in our Spring project, we need to add serenity-core and serenity-spring to the 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>

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

<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

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.

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

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

3.1. SpringIntegrationMethodRule

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

Suppose we have a property to inject in our beans:

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

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

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

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

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

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?

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.

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

@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

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

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

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:

@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

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.

4.1. Maven Dependency

We need to add the rest-assured spring-mock-mvc dependency to the 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

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:

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

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

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

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:

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

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

6. Summary

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.

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

November Discount Launch 2022 – Bottom
We’re finally running a Black Friday launch. All Courses are 30% off until the end of this week:

>> GET ACCESS NOW

Junit footer banner
Comments are closed on this article!