如何使用 SPOCK 框架为 HTTPBuilder 编写单元测试?

时间:2021-06-23 05:22:07

标签: unit-testing testing groovy 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)
                    }
                }
            }
    }

1 个答案:

答案 0 :(得分:1)

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

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

Spock 规范

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

因为被测方法返回void,我决定验证实际调用HTTPBuilder.request的副作用。为此,我必须在 Spy 上使用 HTTPBuilder。测试这个副作用是可选的,那么你就不需要间谍了。

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的情况,你可以通过在spy交互中存根方法调用来保存HTTPBuilder.request(..)的结果,首先通过原始结果,但也检查预期结果。

只需在 def actualResult 块的某处添加 given ... and(在 when 中为时已晚),然后将 callRealMethod() 的结果分配给它,然后与 {{ 1}} 像这样:

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

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