1

I'm building a groovy rest client for my java app to use with test automation. I originally wrote the service in httpBuilder, but couldn't figure out how to parse the response. On non-200 responses, I got an exception which I could catch and assert on the message. Not found, bad request, etc. After updating, I can parse the response, but anytime I get a non-200 response, it tries to parse it as my object which then throws a useless 'missingProperty' exception. The doc shows how to parse the response using response.parser <CONTENT_TYPE>, { config, fs ->...}, and how to branch on the status code using response.success{fs -> ...}, or response.when(<CODE>){fs -> ...}, but not how to parse only for success and use different logic for failure. My current code is as follows:

import groovyx.net.http.ChainedHttpConfig
import groovyx.net.http.FromServer
import groovyx.net.http.HttpBuilder
import groovyx.net.http.NativeHandlers

import static groovyx.net.http.ContentTypes.JSON
import static groovyx.net.http.NativeHandlers.Parsers.json

class CarClient {

    private final HttpBuilder http

    CarClient() {
        http = HttpBuilder.configure {
            request.uri = "localhost:8080"
            request.encoder JSON, NativeHandlers.Encoders.&json
        }
    }

    List<Car> getCars(make) {
        http.get(List) {
            request.uri.path = "/cars/make/${make}"
            response.failure { fs ->
                println("request failed: ${fs}")
            }
            response.parser JSON, { ChainedHttpConfig config, FromServer fs ->
                json(config, fs).collect { x -> new Car(make: x."make", model: x."model") }
            }
        }
    }
}

class Car {
    def make
    def model
}

then my spock tests:

def "200 response should return list of cars"() {
  when:
  def result = client.getCars("honda")
  then:
  result.size == 3
  result[0].make == "honda"
  result[0].model == "accord"
}

def "404 responses should throw exception with 'not found'"() {
  when:
  client.getCars("ford")
  then:
  final Exception ex = thrown()
  ex.message == "Not Found"
}

Under the old version the first test failed, and the second test passed. Under new version, the first test passes and the second test fails. I never actually see the request failed:... message, either. i just get a groovy.lang.MissingPropertyException. when I step through, I can see it trying to load the not found response as a Car object.

Bonus: why do i have to use explicit property mappings instead of groovy casting like in the doc?

json(config, fs).collect { x -> x as Car }

update - For clarification, this isn't my actual source. I'm hitting a proprietary internal API running on WAS, which I don't fully control. I am writing the API's business logic, but response is being marshalled/unmarshalled using WAS and proprietary libraries to which I do not have access. Names have been changed to protect the innocent/my job. These are the workarounds I've tried since initial post: This triggers the failure block correctly on non-200 responses, but the parsing fails with IO - stream closed error. Also, any exceptions i throw in the failure block get wrapped in a RuntimeException which prevents me from accessing the info. I've tried wrapping it in a transport exception as suggested in the doc, but it's still a RuntimeException by the time i get it.

    List<Car> getCars(make) {
        http.get(List) {
            request.uri.path = "/cars/make/${make}"
            response.failure { fs ->
              println("request failed: ${fs}")
              throw new AutomationException("$fs.statusCode : $fs.message")
            }
            response.success { FromServer fs ->
               new JsonSlurper().parse(new InputStreamReader(fs.getInputStream, fs.getCharset())).collect { x -> new Car(make: x."make", model: x."model") }
            }
        }
    }
}

This one parses correctly on 200 responses with entries, 200's with no entries still throw missing property exceptions. Like the previous impl, the AutomationException is wrapped and therefor not useful.

    List<Car> getCars(make) {
        http.get(List) {
            request.uri.path = "/cars/make/${make}"
            response.parser JSON, { ChainedHttpConfig config, FromServer fs ->
            if (fs.statusCode == 200) { 
                json(config, fs).collect { x -> new Car(make: x."make", model: x."model") }
            } else {
              throw new AutomationException("$fs.statusCode : $fs.message")
            }
        }
    }
}

Regarding the bonus, the guide I was following showed implicit casting of the json(config, fs) output to the Car object. I have to explicitly set the props of the new object. Not a big deal, but it makes me wonder if I've configured something else incorrectly.

thejames42
  • 447
  • 6
  • 22
  • I also find this frustrating! if you declare a parser in your configuration, and if for some reason the parser fails, you will have a null object returns. Seems there is no way to handle the Parser error. – Al-Punk Oct 16 '20 at 10:09

1 Answers1

1

You can throw an Exception in the failure handler and it will do what you are looking for:

response.failure { fs ->
    throw new IllegalStateException('No car found')
}

I am not sure what server you are testing against, so I wrote a test using Ersatz:

import com.stehno.ersatz.ErsatzServer
import spock.lang.AutoCleanup
import spock.lang.Specification

import static com.stehno.ersatz.ContentType.APPLICATION_JSON

class CarClientSpec extends Specification {

    @AutoCleanup('stop')
    private final ErsatzServer server = new ErsatzServer()

    def 'successful get'() {
        setup:
        server.expectations {
            get('/cars/make/Toyota').responder {
                content '[{"make":"Toyota","model":"Corolla"}]', APPLICATION_JSON
            }
        }

        CarClient client = new CarClient(server.httpUrl)

        when:
        List<Car> cars = client.getCars('Toyota')

        then:
        cars.size() == 1
        cars.contains(new Car('Toyota', 'Corolla'))
    }

    def 'failed get'() {
        setup:
        server.expectations {
            get('/cars/make/Ford').responds().code(404)
        }

        CarClient client = new CarClient(server.httpUrl)

        when:
        client.getCars('Ford')

        then:
        def ex = thrown(IllegalStateException)
        ex.message == 'No car found'
    }
}

Note that I had to make your client have a configurable base url (and Car needed the @Canonical annotation). If you haven't read the blog post about Take a REST with HttpBuilder-NG and Ersatz I would suggest doing so as it provides a good overview.

I am not sure what you mean in your bonus question.

cjstehno
  • 13,468
  • 4
  • 44
  • 56
  • Unfortunately not. I think the issue is that the response is always coming back with a content type `application/json`, so the parser always gets triggered. I've tried removing the parser and calling the 'new JSONSlurper(...fs.getIntputStream) in the success closure, but that throws a `stream closed` error. Best I've got so far is adding try catch in the parser closure before invoking the native handler, but the exception handling is muddled, and it still chokes on a 200 returning an empty list. Trying to build test w/ ersatz, but it's a side project, so not much time to work on it. – thejames42 Nov 17 '17 at 20:54