首页 > 解决方案 > 如何使用 SPOCK 框架为 HTTPBuilder 编写单元测试?

问题描述

我希望单元测试通过成功和失败执行路径。如何使测试用例走向成功或失败路径?

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)
                    }
                }
            }
    }

标签: unit-testingtestinggroovyspockhttpbuilder

解决方案


好的,看来你使用这个库:

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

因为我以前从未使用过 HTTPBuilder,而且在使用 Groovy 时它看起来是一个不错的工具,所以我尝试了一下,复制了您的用例,但将其转换为完整的MCVE。我不得不承认这个库的可测试性很糟糕。甚至库本身的测试也不是适当的单元测试,而是集成测试,实际执行网络请求而不是模拟它们。该工具本身还包含测试模拟或有关如何测试的提示。

因为该功能在很大程度上依赖于闭包中的动态绑定变量,所以模拟测试有点难看,我不得不查看该工具的源代码才能完成它。好的黑盒测试基本上是不可能的,但是您可以通过以下方法注入一个模拟 HTTP 客户端,该客户端返回一个预定义的模拟响应,其中包含足够的信息,不会使应用程序代码脱轨:

待测类

如您所见,我在类中添加了足够的数据,以便能够运行它并做一些有意义的事情。您的方法返回void而不是可测试的结果并且我们只需要依赖测试副作用的事实并不能使测试更容易。

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"
  }
}

斯波克规格

本规范涵盖了上述代码中响应的所有 3 种情况,使用返回不同状态代码的展开测试。

因为被测方法返回,我决定验证实际调用void的副作用。HTTPBuilder.request为了做到这一点,我不得不SpyHTTPBuilder. 测试这种副作用是可选的,那么你不需要间谍。

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]
  }
}

如果你使用 Spock 有一段时间了,可能我不需要解释太多。如果您是 Spock 或模拟测试初学者,可能这有点太复杂了。但是FWIW,我希望如果您研究代码,您可以了解我是如何做到的。我尝试使用 Spock 标签注释来解释它。

控制台日志

控制台日志表明规范涵盖了所有 3 个执行路径:

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

如果您使用代码覆盖工具,当然您不需要我在应用程序代码中插入的日志语句。它们仅用于演示目的。


验证结果http.request(POST, TEXT) {...}

为了避免您的方法返回的事实,void您可以HTTPBuilder.request(..)通过在 spy 交互中存根方法调用来保存结果,首先传递原始结果,但还要检查预期结果。

只需在块中def actualResult的某个位置添加(为时已晚),然后将结果分配给它,然后像这样比较:given ... andwhencallRealMethod()expectedResult

    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"]

如果您更喜欢数据表而不是数据管道,则该where块如下所示:

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

我认为这几乎涵盖了在这里测试的所有意义。


推荐阅读