unit-testingtestinggroovyspockhttpbuilder

How to write Unit Test for HTTPBuilder using SPOCK framework?


I want the Unit Test to go through both, Success and Failure execution paths. How to make the test case to go to Success or Failure path?

void addRespondents()
{
      
            http.request(POST, TEXT) {
                uri.path = PATH
                headers.Cookie = novaAuthentication
                headers.Accept = 'application/json'
                headers.ContentType = 'application/json'
                body = respondentString
                response.success = { resp, json ->
                    statusCode = 2
                   
                }
                
                response.failure = { resp, json ->
                    if(resp.status == 400) {
                        statusCode = 3
                        def parsedJson = new JsonSlurper().parse(json)
                        
                    }else{
                        autoCreditResponse =  createErrorResponse(resp)
                    }
                }
            }
    }

Solution

  • OK, it seems you use this library:

    <dependency>
      <groupId>org.codehaus.groovy.modules.http-builder</groupId>
      <artifactId>http-builder</artifactId>
      <version>0.7.1</version>
    </dependency>
    

    Because I never used HTTPBuilder before and it looks like a nice tool when using Groovy, I played around with it a bit, replicating your use case, but converting it into a full MCVE. I have to admit that testability for this library is in bad shape. Even the tests for the library itself are no proper unit tests, but rather integration tests, actually performing network requests instead of mocking them. The tool itself also contains to test mocks or hints about how to test.

    Because the functionality heavily relies on dynamically binding variables in closures, mock-testing is somewhat ugly and I had to look into the tool's source code in order to pull it off. Nice black-box testing is basically impossible, but here is how you can inject a mock HTTP client returning a predefined mock response which contains enough information not to derail the application code:

    Class under test

    As you can see, I added enough data in the class to be able to run it and do something meaningful. The fact that your method returns void instead of a testable result and we only have to rely on testing side effects, does not make testing easier.

    package de.scrum_master.stackoverflow.q68093910
    
    import groovy.json.JsonSlurper
    import groovyx.net.http.HTTPBuilder
    import groovyx.net.http.HttpResponseDecorator
    
    import static groovyx.net.http.ContentType.TEXT
    import static groovyx.net.http.Method.POST
    
    class JsonApiClient {
      HTTPBuilder http = new HTTPBuilder("https://jsonplaceholder.typicode.com")
      String PATH = "/users"
      String novaAuthentication = ''
      String respondentString = ''
      String autoCreditResponse = ''
      int statusCode
      JsonSlurper jsonSlurper = new JsonSlurper()
    
      void addRespondents() {
        http.request(POST, TEXT) {
          uri.path = PATH
          headers.Cookie = novaAuthentication
          headers.Accept = 'application/json'
          headers.ContentType = 'application/json'
          body = respondentString
          response.success = { resp, json ->
            println "Success -> ${jsonSlurper.parse(json)}"
            statusCode = 2
          }
    
          response.failure = { resp, json ->
            if (resp.status == 400) {
              println "Error 400 -> ${jsonSlurper.parse(json)}"
              statusCode = 3
            }
            else {
              println "Other error -> ${jsonSlurper.parse(json)}"
              autoCreditResponse = createErrorResponse(resp)
            }
          }
        }
      }
      
      String createErrorResponse(HttpResponseDecorator responseDecorator) {
        "ERROR"
      }
    }
    

    Spock specification

    This spec covers all 3 cases for responses in the above code, using an unrolled test which returns different status codes.

    Because the method under test returns void, I decided to verify the side effect that HTTPBuilder.request was actually called. In order to do this, I had to use a Spy on the HTTPBuilder. Testing for this side effect is optional, then you do not need the spy.

    package de.scrum_master.stackoverflow.q68093910
    
    import groovyx.net.http.HTTPBuilder
    import org.apache.http.HttpResponse
    import org.apache.http.client.HttpClient
    import org.apache.http.client.ResponseHandler
    import org.apache.http.entity.StringEntity
    import org.apache.http.message.BasicHttpResponse
    import org.apache.http.message.BasicStatusLine
    import spock.lang.Specification
    import spock.lang.Unroll
    
    import static groovyx.net.http.ContentType.TEXT
    import static groovyx.net.http.Method.POST
    import static org.apache.http.HttpVersion.HTTP_1_1
    
    class JsonApiClientTest extends Specification {
      @Unroll
      def "verify status code #statusCode"() {
    
        given: "a JSON response"
        HttpResponse response = new BasicHttpResponse(
          new BasicStatusLine(HTTP_1_1, statusCode, "my reason")
        )
        def json = "{ \"name\" : \"JSON-$statusCode\" }"
        response.setEntity(new StringEntity(json))
        
        and: "a mock HTTP client returning the JSON response"
        HttpClient httpClient = Mock() {
          execute(_, _ as ResponseHandler, _) >> { List args ->
            (args[1] as ResponseHandler).handleResponse(response)
          }
        }
    
        and: "an HTTP builder spy using the mock HTTP client"
        HTTPBuilder httpBuilder = Spy(constructorArgs: ["https://foo.bar"])
        httpBuilder.setClient(httpClient)
        
        and: "a JSON API client using the HTTP builder spy"
        def builderUser = new JsonApiClient(http: httpBuilder)
    
        when: "calling 'addRespondents'"
        builderUser.addRespondents()
    
        then: "'HTTPBuilder.request' was called as expected"
        1 * httpBuilder.request(POST, TEXT, _)
    
        where:
        statusCode << [200, 400, 404]
      }
    }
    

    If you have used Spock for a while, probably I do not need to explain much. If you are a Spock or mock testing beginner, probably this is a bit too complex. But FWIW, I hope that if you study the code, you can wrap your head around how I did it. I tried to use Spock label comments in order to explain it.

    Console log

    The console log indicates that all 3 execution paths are covered by the specification:

    Success -> [name:JSON-200]
    Error 400 -> [name:JSON-400]
    Other error -> [name:JSON-404]
    

    If you use a code coverage tool, of course you do not need the log statements I inserted into the application code. They are just for demonstration purposes.


    Verifying the result of http.request(POST, TEXT) {...}

    In order to circumvent the fact that your method returns void, you can save the result of HTTPBuilder.request(..) by stubbing the method call in the spy interaction, passing through the original result at first, but also checking for the expected result.

    Simply add def actualResult somewhere in the given ... and blocks (in when it is too late), then assign the result of callRealMethod() to it and then compare to expectedResult like this:

        and: "a JSON API client using the HTTP builder spy"
        def builderUser = new JsonApiClient(http: httpBuilder)
        def actualResult
    
        when: "calling 'addRespondents'"
        builderUser.addRespondents()
    
        then: "'HTTPBuilder.request' was called as expected"
        1 * httpBuilder.request(POST, TEXT, _) >> {
          actualResult = callRealMethod()
        }
        actualResult == expectedResult
    
        where:
        statusCode << [200, 400, 404]
        expectedResult << [2, 3, "ERROR"]
    

    If you prefer a data table instead of data pipes, the where block looks like this:

        where:
        statusCode | expectedResult
        200        | 2
        400        | 3
        404        | "ERROR"
    

    I think this pretty much covers all that makes sense to test here.