1. Introduction
In this article, we’ll have a quick look at JBehave, then focus on testing a REST API from a BDD perspective.
2. JBehave and BDD
JBehave is a Behaviour Driven Development framework. It intends to provide an intuitive and accessible way for automated acceptance testing.
If you’re not familiar with BDD, it’s a good idea to start with this article, covering on another BDD testing framework – Cucumber, in which we’re introducing the general BDD structure and features.
Similar to other BDD frameworks, JBehave adopts the following concepts:
- Story – represents an automatically executable increment of business functionality, comprises one or more scenarios
- Scenarios – represent concrete examples of the behavior of the system
- Steps – represent actual behavior using classic BDD keywords: Given, When and Then
A typical scenario would be:
Given a precondition
When an event occurs
Then the outcome should be captured
Each step in the scenario corresponds to an annotation in JBehave:
- @Given: initiate the context
- @When: do the action
- @Then: test the expected outcome
3. Maven Dependency
To make use of JBehave in our maven project, the jbehave-core dependency should be included in the pom:
<dependency>
<groupId>org.jbehave</groupId>
<artifactId>jbehave-core</artifactId>
<version>4.1</version>
<scope>test</scope>
</dependency>
4. A Quick Example
To use JBehave, we need to follow the following steps:
- Write a user story
- Map steps from the user story to Java code
- Configure user stories
- Run JBehave tests
- Review results
4.1. Story
Let’s start with the following simple story: “as a user, I want to increase a counter, so that I can have the counter’s value increase by 1”.
We can define the story in a .story file:
Scenario: when a user increases a counter, its value is increased by 1
Given a counter
And the counter has any integral value
When the user increases the counter
Then the value of the counter must be 1 greater than previous value
4.2. Mapping Steps
Given the steps, let’s implementation this in Java:
public class IncreaseSteps {
private int counter;
private int previousValue;
@Given("a counter")
public void aCounter() {
}
@Given("the counter has any integral value")
public void counterHasAnyIntegralValue() {
counter = new Random().nextInt();
previousValue = counter;
}
@When("the user increases the counter")
public void increasesTheCounter() {
counter++;
}
@Then("the value of the counter must be 1 greater than previous value")
public void theValueOfTheCounterMustBe1Greater() {
assertTrue(1 == counter - previousValue);
}
}
Remember that the value in the annotations must accurately match the description.
4.3. Configuring Our Story
To perform the steps, we need to set up the stage for our story:
public class IncreaseStoryLiveTest extends JUnitStories {
@Override
public Configuration configuration() {
return new MostUsefulConfiguration()
.useStoryLoader(new LoadFromClasspath(this.getClass()))
.useStoryReporterBuilder(new StoryReporterBuilder()
.withCodeLocation(codeLocationFromClass(this.getClass()))
.withFormats(CONSOLE));
}
@Override
public InjectableStepsFactory stepsFactory() {
return new InstanceStepsFactory(configuration(), new IncreaseSteps());
}
@Override
protected List<String> storyPaths() {
return Arrays.asList("increase.story");
}
}
In storyPaths(), we provide our .story file path to be parsed by JBehave. Actual steps implementation is provided in stepsFactory(). Then in configuration(), the story loader and story report are properly configured.
Now that we have everything ready, we can begin our story simply by running: mvn clean test.
4.4. Reviewing Test Results
We can see our test result in the console. As our tests have passed successfully, the output would be the same with our story:
Scenario: when a user increases a counter, its value is increased by 1
Given a counter
And the counter has any integral value
When the user increases the counter
Then the value of the counter must be 1 greater than previous value
If we forget to implement any step of the scenario, the report will let us know. Say we didn’t implement the @When step:
Scenario: when a user increases a counter, its value is increased by 1
Given a counter
And the counter has any integral value
When the user increases the counter (PENDING)
Then the value of the counter must be 1 greater than previous value (NOT PERFORMED)
@When("the user increases the counter")
@Pending
public void whenTheUserIncreasesTheCounter() {
// PENDING
}
The report would say the @When a step is pending, and because of that, the @Then step would not be performed.
What if our @Then step fails? We can spot the error right away from the report:
Scenario: when a user increases a counter, its value is increased by 1
Given a counter
And the counter has any integral value
When the user increases the counter
Then the value of the counter must be 1 greater than previous value (FAILED)
(java.lang.AssertionError)
5. Testing REST API
Now we have grasped the basics of JBhave; we’ll see how to test a REST API with it. Our tests will be based on our previous article discussing how to test REST API with Java.
In that article, we tested the GitHub REST API and mainly focused on the HTTP response code, headers, and payload. For simplicity, we can write them into three separate stories respectively.
5.1. Testing the Status Code
The story:
Scenario: when a user checks a non-existent user on github, github would respond 'not found'
Given github user profile api
And a random non-existent username
When I look for the random user via the api
Then github respond: 404 not found
When I look for eugenp1 via the api
Then github respond: 404 not found
When I look for eugenp2 via the api
Then github respond: 404 not found
The steps:
public class GithubUserNotFoundSteps {
private String api;
private String nonExistentUser;
private int githubResponseCode;
@Given("github user profile api")
public void givenGithubUserProfileApi() {
api = "https://api.github.com/users/%s";
}
@Given("a random non-existent username")
public void givenANonexistentUsername() {
nonExistentUser = randomAlphabetic(8);
}
@When("I look for the random user via the api")
public void whenILookForTheUserViaTheApi() throws IOException {
githubResponseCode = getGithubUserProfile(api, nonExistentUser)
.getStatusLine()
.getStatusCode();
}
@When("I look for $user via the api")
public void whenILookForSomeNonExistentUserViaTheApi(
String user) throws IOException {
githubResponseCode = getGithubUserProfile(api, user)
.getStatusLine()
.getStatusCode();
}
@Then("github respond: 404 not found")
public void thenGithubRespond404NotFound() {
assertTrue(SC_NOT_FOUND == githubResponseCode);
}
//...
}
Notice how, in the steps implementation, we used the parameter injection feature. The arguments extracted from the step candidate are just matched following natural order to the parameters in the annotated Java method.
Also, annotated named parameters are supported:
@When("I look for $username via the api")
public void whenILookForSomeNonExistentUserViaTheApi(
@Named("username") String user) throws IOException
5.2. Testing the Media Type
Here’s a simple MIME type testing story:
Scenario: when a user checks a valid user's profile on github, github would respond json data
Given github user profile api
And a valid username
When I look for the user via the api
Then github respond data of type json
And here are the steps:
public class GithubUserResponseMediaTypeSteps {
private String api;
private String validUser;
private String mediaType;
@Given("github user profile api")
public void givenGithubUserProfileApi() {
api = "https://api.github.com/users/%s";
}
@Given("a valid username")
public void givenAValidUsername() {
validUser = "eugenp";
}
@When("I look for the user via the api")
public void whenILookForTheUserViaTheApi() throws IOException {
mediaType = ContentType
.getOrDefault(getGithubUserProfile(api, validUser).getEntity())
.getMimeType();
}
@Then("github respond data of type json")
public void thenGithubRespondDataOfTypeJson() {
assertEquals("application/json", mediaType);
}
}
5.3. Testing the JSON Payload
Then the last story:
Scenario: when a user checks a valid user's profile on github, github's response json should include a login payload with the same username
Given github user profile api
When I look for eugenp via the api
Then github's response contains a 'login' payload same as eugenp
And the plain straight steps implementation:
public class GithubUserResponsePayloadSteps {
private String api;
private GitHubUser resource;
@Given("github user profile api")
public void givenGithubUserProfileApi() {
api = "https://api.github.com/users/%s";
}
@When("I look for $user via the api")
public void whenILookForEugenpViaTheApi(String user) throws IOException {
HttpResponse httpResponse = getGithubUserProfile(api, user);
resource = RetrieveUtil.retrieveResourceFromResponse(httpResponse, GitHubUser.class);
}
@Then("github's response contains a 'login' payload same as $username")
public void thenGithubsResponseContainsAloginPayloadSameAsEugenp(String username) {
assertThat(username, Matchers.is(resource.getLogin()));
}
}
6. Summary
In this article, we have briefly introduced JBehave and implemented BDD-style REST API tests.
When compared to our plain Java test code, code implemented with JBehave looks much clear and intuitive and the test result report looks much more elegant.
As always, the example code can be found in the Github project.