Behaviour driven REST API development with Spring

The case

Bob was sitting desperately in front of a bunch of failing JUnit tests intended to verify the part of the application he just refactored.
The one who wrote them - to be honest it could have been any of us at a certain stage of our life - had the idea that JUnit is somewhat different from production Java: there is no need for code conventions, clean code, comments or whatever that could make the maintainer’s life easier.

Well, Bob spending the past twenty minutes of his life reverse engineering the code, silently looked around, realised that nobody is watching him and put a sad @Ignore annotation before a test. He was thinking about removing the tests, but he just didn’t want to hurt the inner feelings of any fellow developer.

BDD is here to help

Behaviour Driven Development was introduced by Dan North back in 2003.
BDD focuses on obtaining a clear understanding of desired software behavior through writing test cases in a natural language that non-programmers can read:

Given some initial context,
When an event occurs,
Then ensure some outcomes.

It proved to be really useful in the following topics:

  • test definitions can serve as functional documentations
  • writing test first is easier
  • better APIs come from writing testable code
  • help maintainers understand the intention behind the code
  • defining test steps involves having fewer parts to change (low duplication)
  • etc...

Integration test

In the integration tests it needs to be verified that all of the code works correctly, all of the combined code units work well in their environment and the module is ready to be integrated with external systems and resources.
The external connections should be mocked for these tests to cut the dependencies and to avoid side effects of the tests.
I reuse the component structure from the previous post and mark the mocked components with orange: placeholder

Rest API

Defining a REST API as application interface is what everybody is doing nowadays, it comes with certain benefits:

  • simplifies a very complex functionality around resources
  • reduces client server coupling
  • provides stateless communication
  • is easy to use / implement
  • etc...

As the provided REST interface will be the one which determines the behaviour of the application towards a consumer.

So why not BDD it?

Example in book inventory app

For my example book inventory app I implemented automatic behaviour driven tests to verify the REST APIs in a full application context embedded into Spring’s Mock MVC.
I set up a full application context but mocked the RestTemplate to cut external service calls and used an in memory (hsql) db.

Under the hood Cucumber library provides the framework for defining the expected behaviour. The testcases are using Cucumber’s plain text DSL: Gherkin and defining given-when-then steps for the scenarios.

Gherkin is a business readable, Domain Specfic DSL that lets you describe software's behaviour without detailing how that behaviour is implemented.

The REST API tests are located under src/test/rest/resources/bookinventory/integration folder as .feature files.
Running them are part of the JUnit execution that is hooked by BookInventoryIntegrationTest.java to execute all the features annotated with @restApiIntegration.

@RunWith(Cucumber.class)
@CucumberOptions(features = "classpath:bookinventory.integration",
tags = {"@restApiIntegration", "~@ignore"},
format = {"html:target/cucumber-report/bookInventoryIntegration",
"json:target/cucumber-report/bookInventoryIntegration.json"})
public class BookInventoryIntegrationTest {
}

Common step definitions are defined to verify the REST calls in CommonRestCallStepDefs.java.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class, loader = SpringApplicationContextLoader.class)
@WebAppConfiguration
public class CommonRestCallStepDefs {

@Autowired
private volatile WebApplicationContext webApplicationContext;

private volatile MockMvc mockMvc;

@Given("^the web context is set$")
public void givenServerIsUpAndRunning() {
this.mockMvc = webAppContextSetup(this.webApplicationContext).build();
}

@When("^client request GET ([\\S]*)$")
public void performGetOnResourceUri(String resourceUri) throws Exception {
resultActions = this.mockMvc.perform(get(resourceUri).headers(httpHeaders));
}
/*...*/
}

Full source.

The book inventory business specific test steps are defined in BookInventoryStepDefs.java.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = Application.class, loader = SpringApplicationContextLoader.class)
@WebAppConfiguration
public class BookInventoryStepDefs {
/*...*/
@Given("^the following books exist:$")
public void createBooks(DataTable books) throws Throwable {
List<Book> bookList = books.asList(Book.class);
bookRepo.save(bookList);
}

/*...*/
}

Full source.

@RunWith(Cucumber.class)
@Cucumber.Options(features = "classpath:rest.api.integration",
tags = {"@restApiIntegration", "~@ignore"},

format = {"html:target/cucumber-report/restApiIntegration",
"json:target/cucumber-report/restApiIntegration.json"})
public class APITest {
}

REST API testcase definitions

The testcases for the book inventory resource are defined in the BookInventoryIntegrationTest.feature file.

Test: Create a book

  Scenario: create a book
Given the web context is set
Given the db is empty
Given the isbn gateway is mocked to success
When client request POST /api/books with json data:
"""
{"isbn":null,"title":"my book","author":"me"}
"""

Then the response code should be 201
Then the following header should present "Location" with value "http://localhost/api/books/isbn1234"

Test: Find a book by isbn

  Scenario: find by isbn
Given the web context is set
Given the db is empty
Given the following books exist:
| isbn | title | author |
| isbn1234 | Hamlet | William Shakespeare |
| isbn1235 | Romeo and Juliet | William Shakespeare |
| isbn1236 | To Kill a Mockingbird | Harper lee |
When client request GET /api/books/isbn1236
Then the response code should be 200
Then the result json should be:
"""
{"isbn":"isbn1236","title":"To Kill a Mockingbird","author":"Harper lee"}
"""

Test: Find a book by a not existing isbn

  Scenario: find by isbn -> no result
Given the web context is set
Given the db is empty
When client request GET /api/books/not-existing-isbn
Then the response code should be 404
Then the result json should be:
"""
{"errorCode":"BOOK_NOT_FOUND","errorMessage":"The book was not found.","params":{"isbn":"not-existing-isbn"}}
"""

Test: Find a book by author

  Scenario: find by author
Given the web context is set
Given the db is empty
Given the following books exist:
| isbn | title | author |
| isbn1234 | Hamlet | William Shakespeare |
| isbn1235 | Romeo and Juliet | William Shakespeare |
| isbn1236 | To Kill a Mockingbird | Harper lee |
When client request GET /api/books?author=William%20Shakespeare
Then the response code should be 200
Then the result json should be:
"""
[
{"isbn":"isbn1234","title":"Hamlet","author":"William Shakespeare"},
{"isbn":"isbn1235","title":"Romeo and Juliet","author":"William Shakespeare"}
]
"""

Test: Find a book by title

  Scenario: find by title
Given the web context is set
Given the db is empty
Given the following books exist:
| isbn | title | author |
| isbn1234 | Hamlet | William Shakespeare |
| isbn1235 | Romeo and Juliet | William Shakespeare |
| isbn1236 | To Kill a Mockingbird | Harper lee |
When client request GET /api/books?title=Romeo%20and%20Juliet
Then the response code should be 200
Then the result json should be:
"""
[{"isbn":"isbn1235","title":"Romeo and Juliet","author":"William Shakespeare"}]
"""

Test: Find all books

  Scenario: find all
Given the web context is set
Given the db is empty
Given the following books exist:
| isbn | title | author |
| isbn1234 | Hamlet | William Shakespeare |
| isbn1235 | Romeo and Juliet | William Shakespeare |
| isbn1236 | To Kill a Mockingbird | Harper lee |
When client request GET /api/books
Then the response code should be 200
Then the result json should be:
"""
[
{"isbn":"isbn1234", "title":"Hamlet","author":"William Shakespeare"},
{"isbn":"isbn1235", "title":"Romeo and Juliet","author":"William Shakespeare"},
{"isbn":"isbn1236", "title":"To Kill a Mockingbird","author":"Harper lee"}
]
"""

Test: Create a book

  Scenario: create a book gateway error
Given the web context is set
Given the db is empty
Given the isbn gateway is mocked to error
When client request POST /api/books with json data:
"""
{"isbn":null,"title":"my book","author":"me"}
"""

Then the response code should be 500
Then the result json should be:
"""
{
"errorCode":"GENERAL_GATEWAY_ERROR",
"errorMessage":"Internal Server Error",
"params":{"statusText":"Internal Server Error","message":"500 Internal Server Error"}
}
"""

Summary

Behaviour driven integration testing of the REST API helps in rapid application development, facilitates test driven and contract first interface design. Using the described approach makes it really quick to verify the REST endpoints and application context set up.

See the full source code on my github .

T.

##Further reading: Introducing BDD
How to get the most out of Given-When-Then
Gherkin reference
Cucumber JVM: Web Application with Spring MVC
Given When Then