DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Low-Code Development: Leverage low and no code to streamline your workflow so that you can focus on higher priorities.

DZone Security Research: Tell us your top security strategies in 2024, influence our research, and enter for a chance to win $!

Launch your software development career: Dive head first into the SDLC and learn how to build high-quality software and teams.

Open Source Migration Practices and Patterns: Explore key traits of migrating open-source software and its impact on software development.

Related

  • Streamlining Your Workflow With the Jenkins HTTP Request Plugin: A Guide to Replacing CURL in Scripts
  • Spring Boot: Cross-Origin AJAX HTTP Requests
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • How To Build Web Service Using Spring Boot 2.x

Trending

  • Mastering System Design: A Comprehensive Guide to System Scaling for Millions, Part 2
  • GenAI: Spring Boot Integration With LocalAI for Code Conversion
  • How To Compare DOCX Documents in Java
  • LLM Orchestrator: The Symphony of AI Services
  1. DZone
  2. Testing, Deployment, and Maintenance
  3. Testing, Tools, and Frameworks
  4. Ordering Chaos: Arranging HTTP Request Testing in Spring

Ordering Chaos: Arranging HTTP Request Testing in Spring

Learn how to use the Arrange-Act-Assert methodology for integration testing for an approach to writing tests with a clear division into separate stages.

By 
Anton Belyaev user avatar
Anton Belyaev
·
Jun. 14, 24 · Tutorial
Like (1)
Save
Tweet
Share
2.2K Views

Join the DZone community and get the full member experience.

Join For Free

In this article, I would like to describe an approach to writing tests with a clear division into separate stages, each performing its specific role. This facilitates the creation of tests that are easier to read, understand, and maintain.

Donald Duck with file boxes

The discussion will focus on using the Arrange-Act-Assert methodology for integration testing in the Spring Framework with mocking of HTTP requests to external resources encountered during the execution of the tested code within the system behavior. The tests under consideration are written using the Spock Framework in the Groovy language. MockRestServiceServer will be used as the mocking mechanism. There will also be a few words about WireMock.

Problem Description

When studying how to write integration tests for Spring, I often referred to materials on the topic. Examples for MockRestServiceServer mostly described an approach with the declaration of expectations as follows:

  • Expected URI
  • Number of requests to the expected URI
  • Expectations for the structure and content of the request body
  • Response to the request

The code looked something like this:

Java
 
@Test
public void testWeatherRequest() {
    mockServer.expect(once(), requestTo("https://external-weather-api.com/forecast"))         
            .andExpect(method(HttpMethod.POST))
            .andExpect(jsonPath("$.field1", equalTo("value1")))
            .andExpect(jsonPath("$.field2", equalTo("value2")))
            .andExpect(jsonPath("$.field3", equalTo("value3")))
            .andRespond(withSuccess('{"result": "42"}', MediaType.APPLICATION_JSON));
    weatherService.getForecast("London")
    mockServer.verify()
    assert ..
    assert ..
}


When applying this approach, I encountered a number of difficulties:

  1. Ambiguity in determining the reasons for AssertionErrorby the log text - the log text is the same for different scenarios:
    • The HTTP call code is missing/not executed according to business logic.
    • The HTTP call code is executed with an error.
    • The HTTP call code is executed correctly, but there is an error in the mock description.
  2. Difficulty in determining the scope of the tested states due to their dispersion throughout the test code. Formally, the result verification is carried out at the end of the test (mockServer.verify()), but the verification assertions regarding the composition and structure of the request are described at the beginning of the test (as part of creating the mock). At the same time, verification assertions not related to the mock were presented at the end of the test.
    • Important clarification: Using RequestMatcher for the purpose of isolating mocks within many requests seems like the right solution.

Proposed Solution

Clear division of test code into separate stages, according to the Arrange-Act-Assert pattern.

Arrange-Act-Assert

Arrange-Act-Assert is a widely used pattern in writing tests, especially in unit testing. Let's take a closer look at each of these steps:

Arrange (Preparation)

At this stage, you set up the test environment. This includes initializing objects, creating mocks, setting up necessary data, etc. The goal of this step is to prepare everything needed for the execution of the action being tested.

Act (Execution)

Here you perform the action you want to test. This could be a method call or a series of actions leading to a certain state or result to be tested.

Assert (Result Verification)

At the final stage, you check the results of the action. This includes assertions about the state of objects, returned values, changes in the database, messages sent, etc. The goal of this step is to ensure that the tested action has produced the expected result.

Demonstration Scenarios

The business logic of the service for which the tests will be provided can be described as follows:

Gherkin
 
given: The weather service provides information that the weather in city A equals B
when: We request weather data from the service for city A
then: We receive B


Sequence Diagram

Sequence Diagram

Example Implementation for MockRestServiceServer Before Proposed Changes

Tests for the above scenario will be described using MockRestServiceServer.

Difficulty in Determining the Scope of Tested States Due to Their Dispersion Throughout the Test Code

Groovy
 
def "Forecast for provided city London is 42"() {
    setup:          // (1)
    mockServer.expect(once(), requestTo("https://external-weather-api.com/forecast")) // (2)
            .andExpect(method(HttpMethod.POST))
            .andExpect(jsonPath('$.city', Matchers.equalTo("London")))                // (3)
            .andRespond(withSuccess('{"result": "42"}', MediaType.APPLICATION_JSON)); // (4)
    when:          // (5)
    def forecast = weatherService.getForecast("London")
    then:          // (6)
    forecast == "42"     // (7)
    mockServer.verify()  // (8)
}


  1. Setup stage: describing the mock
  2. Indicating that exactly one call is expected to https://external-weather-api.com
  3. Specifying expected request parameters
  4. Describing the response to return
  5. Execution stage, where the main call to get the weather for the specified city occurs
  6. Verification stage: Here, mockServer.verify() is also called to check the request (see item 3).
  7. Verification assertion regarding the returned value
  8. Calling to verify the mock's state

Here we can observe the problem described earlier as "difficulty in determining the scope of tested states due to their dispersion throughout the test code" - some of the verification assertions are in the then block, some in the setup block.

Ambiguity in Determining the Causes of AssertionError

To demonstrate the problem, let's model different error scenarios in the code. Below are the situations and corresponding error logs.

  • Scenario 1 - Passed an unknown city name: def forecast = weatherService.getForecast("Unknown")
Java
 
java.lang.AssertionError: No further requests expected: HTTP POST https://external-weather-api.com
0 request(s) executed.

	at org.springframework.test.web.client.AbstractRequestExpectationManager.createUnexpectedRequestError(AbstractRequestExpectationManager.java:193)


  • Scenario 2: Incorrect URI declaration for the mock; for example, mockServer.expect(once(), requestTo("https://foo.com"))
Java
 
java.lang.AssertionError: No further requests expected: HTTP POST https://external-weather-api.com
0 request(s) executed.

	at org.springframework.test.web.client.AbstractRequestExpectationManager.createUnexpectedRequestError(AbstractRequestExpectationManager.java:193)


  • Scenario 3: No HTTP calls in the code
Java
 
java.lang.AssertionError: Further request(s) expected leaving 1 unsatisfied expectation(s).
0 request(s) executed.


The main observation: All errors are similar, and the stack trace is more or less the same.

Example Implementation for MockRestServiceServer With Proposed Changes

Ease of Determining the Scope of Tested States Due to Their Dispersion Throughout the Test Code

Groovy
 
def "Forecast for provided city London is 42"() {
    setup:          // (1)
    def requestCaptor = new RequestCaptor()
    mockServer.expect(manyTimes(), requestTo("https://external-weather-api.com"))          // (2)
            .andExpect(method(HttpMethod.POST))
            .andExpect(requestCaptor)                                                      // (3)
            .andRespond(withSuccess('{"result": "42"}', MediaType.APPLICATION_JSON));      // (4)
    when:          // (5)
    def forecast = weatherService.getForecast("London")
    then:          // (6)
    forecast == "42"
    requestCaptor.times == 1              // (7)
    requestCaptor.entity.city == "London" // (8)
    requestCaptor.headers.get("Content-Type") == ["application/json"]
}


  • #3: Data capture object
  • #7: Verification assertion regarding the number of calls to the URI
  • #8: Verification assertion regarding the composition of the request to the URI

In this implementation, we can see that all the verification assertions are in the then block.

Unambiguity in Identifying the Causes of AssertionError

To demonstrate the problem, let's attempt to model different error scenarios in the code. Below are the situations and corresponding error logs.

  • Scenario 1: An unknown city name was provided def forecast = weatherService.getForecast("Unknown")
Groovy
 
requestCaptor.entity.city == "London"
|             |      |    |
|             |      |    false
|             |      |    5 differences (28% similarity)
|             |      |    (Unk)n(-)o(w)n
|             |      |    (Lo-)n(d)o(-)n
|             |      Unknown
|             [city:Unknown]
<pw.avvero.spring.sandbox.weather.RequestCaptor@6f77917c times=1 bodyString={"city":"Unknown"} entity=[city:Unknown] headers=[Accept:[application/json, application/*+json], Content-Type:[application/json], Content-Length:[18]]>


  • Scenario 2: Incorrect URI declaration for the mock; for example, mockServer.expect(once(), requestTo("https://foo.com"))
Groovy
 
java.lang.AssertionError: No further requests expected: HTTP POST https://external-weather-api.com
0 request(s) executed.


  • Scenario 3: No HTTP calls in the code
Groovy
 
Condition not satisfied:

requestCaptor.times == 1
|             |     |
|             0     false
<pw.avvero.spring.sandbox.weather.RequestCaptor@474a63d9 times=0 bodyString=null entity=null headers=[:]>


Using WireMock

WireMock provides the ability to describe verifiable expressions in the Assert block.

Groovy
 
def "Forecast for provided city London is 42"() {
    setup:          // (1)
    wireMockServer.stubFor(post(urlEqualTo("/forecast"))                              // (2)
            .willReturn(aResponse()                                                   // (4)
                    .withBody('{"result": "42"}')
                    .withStatus(200)
                    .withHeader("Content-Type", "application/json")))
    when:          // (5)
    def forecast = weatherService.getForecast("London")
    then:          // (6)
    forecast == "42"
    wireMockServer.verify(postRequestedFor(urlEqualTo("/forecast"))
            .withRequestBody(matchingJsonPath('$.city', equalTo("London"))))          // (7)
}


The above approach can also be used here, by describing the WiredRequestCaptor class.

Groovy
 
def "Forecast for provided city London is 42"() {
    setup:
    StubMapping forecastMapping = wireMockServer.stubFor(post(urlEqualTo("/forecast"))
            .willReturn(aResponse()
                    .withBody('{"result": "42"}')
                    .withStatus(200)
                    .withHeader("Content-Type", "application/json")))
    def requestCaptor = new WiredRequestCaptor(wireMockServer, forecastMapping)
    when:
    def forecast = weatherService.getForecast("London")
    then:
    forecast == "42"
    requestCaptor.times == 1
    requestCaptor.body.city == "London"
}


This allows us to simplify expressions and enhance the idiomaticity of the code, making the tests more readable and easier to maintain.

Conclusion

Throughout this article, I have dissected the stages of testing HTTP requests in Spring, using the Arrange-Act-Assert methodology and mocking tools such as MockRestServiceServer and WireMock. The primary goal was to demonstrate how clearly dividing the test into separate stages significantly enhances readability, understanding, and maintainability.

I highlighted the problems associated with the ambiguity of error determination and the difficulty of defining the scope of tested states and presented ways to solve them through a more structured approach to test writing. This approach is particularly important in complex integration tests, where every aspect is critical to ensuring the accuracy and reliability of the system.

Furthermore, I showed how the use of tools like RequestCaptor and WiredRequestCaptor simplifies the test-writing process and improves their idiomaticity and readability, thereby facilitating easier support and modification.

In conclusion, I want to emphasize that the choice of testing approach and corresponding tools should be based on specific tasks and the context of development. The approach to testing HTTP requests in Spring presented in this article is intended to assist developers facing similar challenges.

The link to the project repository with demonstration tests can be found here.

Thank you for your attention to the article, and good luck in your pursuit of writing effective and reliable tests!

Integration testing Assertion (software development) Groovy (programming language) Requests Spring Framework

Published at DZone with permission of Anton Belyaev. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Streamlining Your Workflow With the Jenkins HTTP Request Plugin: A Guide to Replacing CURL in Scripts
  • Spring Boot: Cross-Origin AJAX HTTP Requests
  • Distributed Tracing System (Spring Cloud Sleuth + OpenZipkin)
  • How To Build Web Service Using Spring Boot 2.x

Partner Resources


Comments

ABOUT US

  • About DZone
  • Send feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: