Testing a REST API with JBehave – 用JBehave测试REST API

最后修改: 2017年 4月 10日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.简介

In this article, we’ll have a quick look at JBehave, then focus on testing a REST API from a BDD perspective.

在本文中,我们将快速了解JBehave,然后从BDD的角度重点测试一个REST API。

2. JBehave and BDD

2.JBehave和BDD

JBehave is a Behaviour Driven Development framework. It intends to provide an intuitive and accessible way for automated acceptance testing.

JBehave是一个行为驱动的开发框架。它的目的是为自动验收测试提供一种直观的、易于使用的方式。

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.

如果您不熟悉BDD,最好从这篇文章开始,这篇文章涉及另一个BDD测试框架–Cucumber,其中我们将介绍BDD的一般结构和功能。

Similar to other BDD frameworks, JBehave adopts the following concepts:

与其他BDD框架类似,JBehave采用了以下概念。

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

场景中的每个步骤都对应于JBehave中的一个注释。

  • @Given: initiate the context
  • @When: do the action
  • @Then: test the expected outcome

3. Maven Dependency

3.Maven的依赖性

To make use of JBehave in our maven project, the jbehave-core dependency should be included in the pom:

为了在我们的maven项目中使用JBehave,pom中应包含jbehave-core依赖项。

<dependency>
    <groupId>org.jbehave</groupId>
    <artifactId>jbehave-core</artifactId>
    <version>4.1</version>
    <scope>test</scope>
</dependency>

4. A Quick Example

4.一个快速的例子

To use JBehave, we need to follow the following steps:

为了使用JBehave,我们需要遵循以下步骤。

  1. Write a user story
  2. Map steps from the user story to Java code
  3. Configure user stories
  4. Run JBehave tests
  5. Review results

4.1. Story

4.1.故事

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

让我们从下面这个简单的故事开始。”作为一个用户,我想增加一个计数器,这样我就可以让计数器的值增加1″。

We can define the story in a .story file:

我们可以在一个.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

4.2. Mapping Steps

4.2.绘图步骤

Given the steps, let’s implementation this in Java:

鉴于这些步骤,让我们用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

4.3.配置我们的故事

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.

storyPaths()中,我们提供我们的.story文件路径,以便被JBehave解析。实际的步骤实现在stepsFactory()中提供。然后在configuration()中,对故事加载器和故事报告进行适当配置。

Now that we have everything ready, we can begin our story simply by running: mvn clean test.

现在我们已经准备好了一切,我们可以简单地通过运行来开始我们的故事。mvn clean test

4.4. Reviewing Test Results

4.4.审查测试结果

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:

如果我们忘记实施方案的任何步骤,报告会让我们知道。比如我们没有实现@When步骤。

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.

报告会说@When一个步骤正在等待,因为这个原因,@Then步骤将不会被执行。

What if our @Then step fails? We can spot the error right away from the report:

如果我们的@Then步骤失败了怎么办?我们可以从报告中马上发现错误。

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

5.测试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.

现在我们已经掌握了JBhave的基本知识;我们将看看如何用它来测试REST API。我们的测试将基于我们的前一篇讨论如何用Java测试REST API的文章

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.

在那篇文章中,我们测试了GitHub REST API,并主要关注HTTP响应代码、头文件和有效载荷。为了简单起见,我们可以把它们分别写成三个独立的故事。

5.1. Testing the Status Code

5.1.测试状态代码

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.

请注意,在步骤的实现中,我们使用了参数注入功能。从候选步骤中提取的参数只是按照自然顺序与注释的Java方法中的参数相匹配。

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

5.2.测试媒体类型

Here’s a simple MIME type testing story:

这里有一个简单的MIME类型测试故事。

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

5.3.测试JSON的有效载荷

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

6.总结

In this article, we have briefly introduced JBehave and implemented BDD-style REST API tests.

在这篇文章中,我们简单介绍了JBehave并实现了BDD风格的REST API测试。

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.

与我们的普通Java测试代码相比,用JBehave实现的代码看起来更清晰、更直观,测试结果报告看起来更优雅。

As always, the example code can be found in the Github project.

一如既往,可以在Github项目中找到示例代码。