ribbon

Gru is HTTP interaction testing framework with out-of-box support for Grails and REST interfaces.

Installation

Gru is available in JCenter. You can use Grails module or HTTP module or both in your project.

Gradle Installation
repositories {
    jcenter()
}

dependencies {
    // pick any of these as you need
    testCompile "com.agorapulse:gru-http:0.8.2"                                     (1)
    testCompile "com.agorapulse:gru-grails:0.8.2"                                   (2)
    testCompile "com.agorapulse:gru-spring:0.8.2"                                   (3)
}
1 HTTP module for any HTTP backend
2 Grails module to emulate HTTP interaction within Grails Unit tests
3 Spring module to emulate HTTP interaction using MockMvc

If you are testing JSON responses, underlying library JsonUnit requires one of following libraries on the classpath: Jackson 1.x, Jackson 2.x, Gson, JSONObject or Moshi. If you don’t have any of these on classpath, also add for example Jackson 2 dependency:

testCompile 'com.fasterxml.jackson.core:jackson-databind:2.9.6'

Setup

HTTP

Gru can be used to test any HTTP endpoint, including Grails application running within integration tests.

Minimal HTTP Usage
package heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.http.Http
import org.junit.Rule
import spock.lang.Specification

class HttpSpec extends Specification{

    @Rule Gru<Http> gru = Gru.equip(Http.steal(this))                                   (1)
                             .prepare('https://despicableme.fandom.com')                (2)

    void 'despicable me'() {
        expect:
            gru.test {
                get "/wiki/Felonius_Gru"                                                (3)
            }
    }
}
1 Gru must be used as JUnit @Rule and initialized with the specification object
2 Set the base URL http://despicableme.wikia.com for HTTP calls
3 Execute HTTP GET request on /wiki/Felonius_Gru URI and verify it is accessible and returns OK(200) status code.

Grails

Gru for Grails tests controllers in context of other Grails artifacts such as url mappings or interceptors. Gru steals a lot from Grails Testing Support.

Minimal Grails Usage
package heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.grails.Grails
import grails.testing.web.controllers.ControllerUnitTest
import org.junit.Rule
import spock.lang.Specification

class BasicSpec extends Specification implements ControllerUnitTest<MoonController> {   (1)

    @Rule Gru<Grails<BasicSpec>> gru = Gru.equip(Grails.steal(this)).prepare {          (2)
        include UrlMappings                                                             (3)
    }

    void 'look at the moon'() {
        expect:
            gru.test {
                get '/moons/earth/moon'                                                  (4)
            }
    }

    void setup() {
        controller.moonService = new MoonService()
    }
}
1 The only requirement for Grails integration is that specification must implement ControllerUnitTest trait
2 Gru must be used as JUnit @Rule and initialized with the specification object
3 Include the appropriate UrlMappings so Gru knows which controller action should be executed
4 Gru executes action for GET request to URL /moons/earth/moon and verifies it does not throw any exception and it returns status OK(200)

If you declare test in expect block then the verification happens automatically. You can also call gru.verify() manually to fully leverage Spock’s power such as interaction based testing.

Minimal Grails Usage with When and Then Blocks
void 'look at the moon'() {
    given:
        MoonService moonService = Mock(MoonService)
        controller.moonService = moonService                                            (1)
    when:
        gru.test {                                                                      (2)
            get '/moons/earth/moon'
        }
    then:
        gru.verify()                                                                    (3)
        1 * moonService.findByPlanetAndName("earth", "moon") >> [name: "Moon"]          (4)
}
1 Mock the moonService
2 Call the test method within the when block
3 Call verify method manually
4 Verify Spock interactions on moonService in then block

Spring

Gru for Spring allows you to test your application with mocked HTTP requests. Under the hood MockMvc is used.

Minimal Spring Usage
package com.agorapulse.gru.spring.heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.spring.Spring
import org.junit.Rule
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc
import spock.lang.Specification

@WebMvcTest                                                                             (1)
class BasicSpec extends Specification {

    @Autowired MockMvc mvc                                                              (2)

    @Rule Gru<Spring> gru = Gru.equip(Spring.steal(this))                               (3)

    void 'look at the moon'() {
        expect:
            gru.test {
                get '/moons/earth/moon'                                                 (4)
            }
    }
}
1 Use @WebMvcTest to properly setup Spring MVC test, see the warning bellow
2 Field with type MockMvc must exist and must not be null during the test execution
3 Initiate Gru with Spring client
4 Issue mock GET request to /moon/earth/moon and expect it returns OK(200) response

Spock version at least 1.1 must be used when executing Spring tests. For example Spring Boot overrides the default transitive dependency back to 1.0 so you need to explicity declare Spock version in your build file:

String spockVersion = '1.1-groovy-2.4'
testCompile("org.spockframework:spock-spring:$spockVersion")
testCompile("org.spockframework:spock-core:$spockVersion")

Common Interactions

Request

Each Gru tests begins with HTTP request. There are methods for each of following HTTP methods: head, post, put, patch, delete, options, trace and get. Each method accepts the URI to perform the request and optional request configuration closure.

Grails client requires that UrlMappings for given URI is specified unless the mappings are defined in UrlMappings class declared in default package. See Minimal Grails Usage for reference.

URL Parameters

You can specify additional URL parameters

Additional URL Parameters
void 'steal the moon with shrink ray'() {
    expect:
        gru.test {
            delete '/moons/earth/moon', {
                params with: 'shrink-ray'
            }
            expect {
                status NO_CONTENT
            }
        }
}

Request Headers

You can specify additional HTTP headers sent with the request.

Request Headers
void 'visit secret moon Noom'() {
    expect:
        gru.test {
            get '/moons/earth/noom', {
                headers Authorization: 'Felonius'
            }
        }
}

JSON Payload

You can use JSON payload from file.

JSON Payload
void 'create moon for Margot'() {
    expect:
        gru.test {
            post '/moons/earth', {
                json 'newMoonRequest.json'
            }
        }
}
You can also define json as map or list e.g. json(foo: 'bar'). Both are serialized to JSON using a JSON integration on the classpath.
JSON files are loaded relatively to directory having the same name as the current specification and it is placed in the same directory corresponding the package of the the current specification. For example if your specification name is org.example.ExampleSpec then createNewMoonResponse.json file’s path should be org/example/ExampleSpec/createNewMoonResponse.json.
Fixture JSON files are created automatically if missing. Empty object ({}) is created for missing request JSON file and JSON file with the content same as the one returned from the controller is created if response file is missing but exception is thrown so you have to run the test again and verify it is repeatable. Fixture files are generated inside directory from system property TEST_RESOURCES_FOLDER or in src/test/resources relatively to the working directory. See the following example how to easily set the property in Gradle.

Generic Payload

You can use generic payload from file.

Generic Payload
void 'create moon for Margot generic'() {
    expect:
        gru.test {
            post '/moons/earth', {
                content 'newMoonRequest.json', 'application/json'
            }
        }
}
Content files are loaded relatively to directory having the same name as the current specification and it is placed in the same directory corresponding the package of the the current specification. For example if your specification name is org.example.ExampleSpec then createNewMoonResponse.json file’s path should be org/example/ExampleSpec/createNewMoonResponse.json.
Content files are created automatically if missing. Empty file is created for missing request content file. Fixture files are generated inside directory from system property TEST_RESOURCES_FOLDER or in src/test/resources relatively to the working directory.

File Upload

You can emulate multipart file uploads.

File Upload
void 'upload file with message'() {
    expect:
        gru.test {
            post '/moons/upload', {
                upload {                                                            (1)
                    params message: 'Hello'                                         (2)
                    file 'theFile', 'hello.txt', inline('Hello World'), 'text/plain'(3)
                }
                // Grails only - required because of bug in mock request
                executes controller.&postWithMessageAndImage                        (4)
            }
            expect {
                json 'uploadResult.json'
            }
        }
}
1 Define upload files and parameters
2 You can specify arbitrary parameter (i.g. hidden fields)
3 Specify the file to be uploaded with the name of the parameter, original filename, content and content type
4 In case of Grails unit test, you must point to the controller action’s method

Cookies

You can emulate cookies send by the browser or set by the server.

File Upload
void 'send cookies'() {
    expect:
        gru.test {
            get '/moons/cookie', {
                cookies chocolate: 'rules'                                          (1)
            }
            expect {
                json 'cookies.json', IGNORING_EXTRA_FIELDS
            }
        }
}

void 'set cookies'() {
    expect:
        gru.test {
            get '/moons/setCookie'
            expect {
                cookies chocolate: 'rules'                                          (2)
                cookie {                                                            (3)
                    name 'coffee'
                    value 'lover'
                    secure true
                    domain 'localhost'
                }
            }
        }
}
1 Send browser cookies
2 Expect cookies sent by the server
3 More detailed response cookie expectations

Response

Response expectation are defined using expect method within test definition.

Status Code

You can specify expected response status code. Constants for HTTP status codes are available within the expect closure call. Default status is OK(200) which is asserted within every test. See Redirection for exception.

Status Code
void 'steal the moon with shrink ray'() {
    expect:
        gru.test {
            delete '/moons/earth/moon', {
                params with: 'shrink-ray'
            }
            expect {
                status NO_CONTENT
            }
        }
}

Response Headers

You can specify expected response headers.

Response Headers
void 'json is rendered'() {
    expect:
        gru.test {
            get '/moons/earth/moon'
            expect {
                headers 'Content-Type': 'application/json;charset=UTF-8'
            }
        }
}

Redirection

You can specify expected redirection URI. For redirection, default status code is FOUND(302).

Redirection
void 'no planet needed'() {
    expect:
        gru.test {
            get '/moons/-/moon'
            expect {
                redirect '/moons/earth/moon'
            }
        }
}

Plain Text Response

You can specify expected plain text response. The responded JSON is verified just by equals method.

Plain Text Response
void 'verify text'() {
    expect:
        gru.test {
            get '/moons/earth/moon/info', {
                headers 'Accept': 'text/plain'
            }
            expect {
                text 'textResponse.txt'
            }
        }
}
textResponse.txt
Moon goes around Earth

You can also specify the text inline. Inline method is also available for JSON and HTML.

Plain Text Inline Response
void 'verify text inline'() {
    expect:
        gru.test {
            get '/moons/earth/moon/info', {
                headers 'Accept': 'text/plain'
            }
            expect {
                text inline('Moon goes around Earth')
            }
        }
}

JSON Response

You can specify expected JSON response. The responded JSON is verified using JsonUnit.

JSON Response
void 'verify json'() {
    expect:
        gru.test {
            get '/moons/earth/moon'
            expect {
                json 'moonResponse.json'
            }
        }
}
moonResponse.json
{
    "name": "Moon",
    "planet": "Earth",
    "created": "${json-unit.matches:isoDate}"
}

You can pass JsonUnit options along with the file name.

JSON Response with Options
void 'verify json 2'() {
    expect:
        gru.test {
            get '/moons/earth'
            expect {
                json 'moonsResponse.json', IGNORING_EXTRA_ARRAY_ITEMS
            }
        }
}
moonsResponse.json
[
    {
        "name": "Moon",
        "planet": "Earth",
        "created": "${json-unit.matches:isoDate}"
    }
]

See JSON Payload for information about the expected response file location.

JsonUnit Primer

There are built in and custom placeholders which can be used when evaluating the JSON response:

Table 1. Default JsonUnit Placeholders
Placeholder Description

"${json-unit.ignore}"

Ignore content of the property

"${json-unit.regex}[A-Z]+"

Content must match regular expression

"${json-unit.any-string}"

Any string

"${json-unit.any-boolean}"

Any boolean

"${json-unit.any-number}"

Any number

Table 2. Custom Gru’s Placeholders
Placeholder Description

"${json-unit.matches:positiveIntegerString}"

Any positive number as string

"${json-unit.matches:isoDate}"

Any date in ISO format

"${json-unit.matches:isoDateNow}"

ISO within last hour

"${json-unit.matches:url}"

Any URL (string parsable by java.net.URL)

You can customize JsonUnit fluent assertion within json block inside expect definition:

JsonUnit Customisation
void 'customise json unit'() {
    expect:
        gru.test {
            get '/moons/earth/moon'
            expect {
                json 'moonResponse.json'
                json {
                    withTolerance(0.1).withMatcher(
                        'negativeIntegerString',
                        MatchesPattern.matchesPattern(/-\d+/)
                    )
                }
            }
        }
}
JsonFluentAssert is immutable, so only last statement actually matters. That is why .withMatcher is called as method chain.

When you need to rewrite your JSON fixture files then you can temporarily set the value of environment variable COM_AGORAPULSE_GRU_REWRITE to true to let Gru rewrite all your fixtures files which are triggering assertion errors.

export COM_AGORAPULSE_GRU_REWRITE=true
./gradlew test
unset COM_AGORAPULSE_GRU_REWRITE

HTML Response

You can specify expected HTML response. The responded HTML is cleaned using jsoup and the difference is performed using XMLUnit. You can replace any text node with ${xml-unit.ignore} to ignore the value fo the node.

HTML Response
void 'verify html'() {
    expect:
        gru.test {
            get '/moons/earth/moon/info'
            expect {
                html 'htmlResponse.html'
            }
        }
}
htmlResponse.html
<html>
 <head>
  <title>Moon's Info</title>
  <meta name="layout" content="main" />
 </head>
 <body>
  <h1>Moon</h1>
  <h2>Earth</h2>
  <p>${xml-unit.ignore}</p>
 </body>
</html>
HTML support for Grails is currently experimental as it does not apply the layouts.

Grails Unit Test Interactions

On top of standard HTTP interaction, Gru provides additional methods specific for Grails only.

As of IntelliJ IDEA 2017.2 you get false warnings about the wrong method’s argument being used. You have to ignore them at the moment. Please, vote for IDEA-177530 bug to get this fixed soon.

Artifacts

You can add additional Grails artifacts using include method. Only interceptors and URL mappings are supported at the moment.

URL Mappings

Unless you are using UrlMappings class in default package, you have to specify the URL mappings class used for matching the URIs. You can use just name of the class as string if your URL mappings resides in default package.

URL Mappings
package heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.grails.Grails
import grails.testing.web.controllers.ControllerUnitTest
import org.junit.Rule
import spock.lang.Specification

class IncludeUrlMappingsSpec extends Specification implements ControllerUnitTest<MoonController> {

    @Rule Gru<Grails<IncludeUrlMappingsSpec>> gru = Gru.equip(Grails.steal(this)).prepare {
        include ApiUrlMappings                                                          (1)
    }

    void "moon is still there"() {
        expect:
            gru.test {
                delete '/api/v1/moons/earth/moon'                                       (2)
                expect {
                    status BAD_REQUEST
                }
            }
    }
}
1 Include ApiUrlMappings URL mappings
2 The URI will be matched using ApiUrlMappings URL mappings

Interceptors

If you’re controller heavily depends on interceptor it is sometimes better to test the interceptors and controllers as a single unit. You can include interceptors into test in similar way as url mappings using the include method. Once the interceptor is included it must match the given URL otherwise exception is thrown in the verification phase. You can add additional boolean true parameter to the include method to also autowire the interceptor automatically.

Interceptors
package heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.grails.Grails
import grails.testing.web.controllers.ControllerUnitTest
import org.junit.Rule
import spock.lang.Specification

class IncludeInterceptorSpec extends Specification implements ControllerUnitTest<MoonController> {

    @Rule Gru<Grails<IncludeInterceptorSpec>> gru = Gru.equip(Grails.steal(this)).prepare {
        include ApiUrlMappings
        include VectorInterceptor, true                                                 (1)
    }

    void "moon is still there"() {
        given:
            defineBeans {
                vectorMessage(VectorMessage, "Le Vecteur était là!")                    (2)
            }
        expect:
            gru.test {
                delete '/api/v1/moons/earth/moon'
                expect {
                    status NOT_FOUND
                    headers 'X-Message': "Le Vecteur était là!"                         (3)
                }
            }
    }

}
1 Include VectorInterceptor interceptor and let it autowire the beans
2 Declare bean which is used in the interceptor
3 The expectations reflects the interceptor changes and the defined bean

Request

Controller Action

You can verify that given URI is mapped to particular action. Use method reference with .& to obtain particular MethodClosure instance.

Controller Action
void 'verify action'() {
    expect:
        gru.test {
            get '/moons/earth/moon', {
                executes controller.&moon
            }
        }
}

The test only passes if /moons/earth/moon URI is mapped to action moon in MoonController.

Response

Forwarding

You can specify the expected forward URI.

Model
void 'verify forward'() {
    expect:
        gru.test {
            get '/moons/earth/moon', {
                params info: 'true'
            }
            expect {
                forward '/moons/earth/moon/info'
            }
        }
}

Model

You can specify the expected model object returned from the controller.

Model
void 'verify model'() {
    given:
        def moon = [name: 'Moon', planet: 'Earth']
        MoonService moonService = Mock(MoonService)
        controller.moonService = moonService
    when:
        gru.test {
            get '/moons/earth/moon/info'
            expect {
                model moon: moon
            }
        }
    then:
        gru.verify()
        1 * moonService.findByPlanetAndName('earth', 'moon') >> moon
}

Integration Tests

You use Http client to verify controller within @Integration test.

Integration Setup
@Value('${local.server.port}')
Integer serverPort                                                                      (1)

@Rule Gru<Http> gru = Gru.equip(Http.steal(this))                                       (2)

void setup() {
    final String serverUrl = "http://localhost:${serverPort}"                           (3)
    gru.prepare {
        baseUri serverUrl                                                               (4)
    }
}
1 Let Grails inject the server port
2 Use Http client instad of Grails
3 Store server URL in variable, otherwise it won’t get properly evaluated
4 Set the base URI for the executed test

Spring Unit Test Integration

On top of standard HTTP interaction, Gru provides additional methods specific for Spring only. These methods allows you to fully leverage the power of MockMvc.

As of IntelliJ IDEA 2017.2 you get false warnings about the wrong method’s argument being used. You have to ignore them at the moment. Please, vote for IDEA-177530 bug to get this fixed soon.

MockMVC Builders

You can configure additional request steps within request method closure definition and you can use that within expect block to add additional ResultMatcher. Both methods are aliased to and method within their blocks so you can write the assertions in more fluent way.

MockMVC Builders
    void 'json is rendered'() {
        expect:
            gru.test {
                get '/moons/earth/moon', {
                    request {                                                               (1)
                        accept(MediaType.APPLICATION_JSON_UTF8)                             (2)
                    }
                    and {                                                                   (3)
                        locale(Locale.CANADA)
                    }
                }
                expect {
                    headers 'Content-Type': 'application/json;charset=UTF-8'
                    json 'moonResponse.json'
                    that content().encoding('UTF-8')                        (4)
                    and content().contentType(MediaType.APPLICATION_JSON_UTF8)              (5)
                }
            }
    }
1 Use request method to fine-tune MockHttpServletRequestBuilder
2 Declare Accept header
3 Same as request, declares desired locale
4 Use that method to add additional result matchers, the content() method is statically imported from MockMvcResultMatchers class, asserts the content encoding
5 Same as that, asserts the content type returned

Integration Tests

You use Http client to verify controller within @Integration test.

Integration Setup
package com.agorapulse.gru.spring.heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.http.Http
import org.junit.Rule
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)             (1)
class MoonControllerIntegrationTest extends Specification {

    @Value('${local.server.port}') private int serverPort                               (2)

    @Rule Gru<Http> gru = Gru.equip(Http.steal(this))                                   (3)

    void setup() {
        gru.prepare {
            baseUri "http://localhost:${serverPort}"                                    (4)
        }
    }

    void 'render json'() {                                                              (5)
        expect:
            gru.test {
                get('/moons/earth/moon')
                expect {
                    json 'renderJsonResponse.json'
                }
            }
    }

}

AWS API Gateway

AWS API Gateway itegration is currently incubating. All the basic HTTP features are implemented but custom API Proxy related features are currently missing (e.g. customizing the API Gateway or Lambda context).

You can easily test your AWS API Gateway Lambda Integration with Gru. First of all you need to tell Gru which handlers are responsible for particular URLs.

API Gateway Lambda Proxy Configuration
            Gru gru = Gru.equip(ApiGatewayProxy.steal(this) {
                map '/one' to SampleHandler.name                                (1)
                map '/two' to SampleHandler                                     (2)
                map '/three', GET to SampleHandler.name                         (3)
                map '/four', GET, POST to SampleHandler
            })
1 You can specify the name of the handler by class name with optional method name after double colon ::
2 You can specify the handler by class reference
3 You can specify which HTTP methods are be handled for given URL

You can also test handlers which do not uses the proxy integration

API Gateway Lambda Configuration (without Proxy)
            Gru gru = Gru.equip(ApiGatewayProxy.steal(this) {
                map '/five/{action}/{id}' to NonProxyHandler, {                 (1)
                    pathParameters('action', 'id')                                  (2)
                    queryStringParameters 'foo', 'bar'                              (3)
                    response(200) {                                                     (4)
                        headers 'Content-Type': 'application/json'                              (5)
                    }
                }
            })
1 The main difference is that you specify the request and response mapping
2 You can specify which path parameters are being extracted into the function input object
3 You can specify which query parameters are being extracted into the function input object
4 You can specify the response mapping with given status
5 You can specify the headers mapping for the response

Extending Gru

Extending DSL

Gru DSL can be easily extend. Gru uses minions whenever it is possible. Minion interface requires several methods to be implemented to intercept the test flow.

Minion
/**
 * @return index of the minion to so they they handle the test in given order
 */
int getIndex();

/**
 * Prepares the execution of controller action.
 * @param client controller unit test
 * @param squad minion's colleagues
 * @param context context of execution, in this phase you should only add errors if some conditions are not met
 * @return new context holding any exception which happens during the preparation or the initial context from parameter
 */
GruContext beforeRun(Client client, Squad squad, GruContext context);

/**
 * Modifies the result of the execution of controller action.
 *
 * Minions handle the result in reversed order.
 *
 * @param client controller unit test
 * @param squad minion's colleagues
 * @param context context of execution, in this phase can modify the result or add exception
 * @return modified context or the initial one from the parameter
 */
GruContext afterRun(Client client, Squad squad, GruContext context);

/**
 * Verifies the response after execution of the controller action.
 *
 * @param client controller unit test
 * @param squad minion's colleagues
 * @param context context of the execution, in this phase you can't modify the context, you can just throw any {@link Throwable} based on the current context state
 * @throws Throwable if any of the conditions are not met
 */
void verify(Client client, Squad squad, GruContext context) throws Throwable;

Client represents the current client such as Http or Grails.

Squad is group of minions used in current test.

GruContext is immutable object which holds any error been thrown during the execution phase and also the result of the execution.

Grails implementation provides various example of extending the DSL. ModelMinion is responsible for checking the model returned from the controller.

ModelMinion.groovy
package com.agorapulse.gru.grails.minions

import com.agorapulse.gru.GruContext
import com.agorapulse.gru.Squad
import com.agorapulse.gru.grails.Grails
import com.agorapulse.gru.minions.AbstractMinion
import org.springframework.web.servlet.ModelAndView

/**
 * Minion responsible for verifying model returned from the controller action.
 */
class ModelMinion extends AbstractMinion<Grails> {                                      (1)

    final int index = MODEL_MINION_INDEX                                                (2)

    Object model                                                                        (3)

    ModelMinion() {
        super(Grails)                                                                   (4)
    }

    @Override
    @SuppressWarnings('Instanceof')
    void doVerify(Grails grails, Squad squad, GruContext context) throws Throwable {    (5)
        if (model instanceof Map && context.result instanceof Map) {
            model.each { key, value ->
                assert context.result[key] == value
            }
        } else if (model instanceof ModelAndView) {
            assert context.result

            ModelAndView result = context.result as ModelAndView

            assert result.model == model.model
            assert result.view == model.view
            assert result.status == model.status
        } else if (model != null) {
            assert context.result == model
        }
    }
}
1 AbstractMinion guarantees type safety and allows to implements just some of the methods of the interface
2 beforeRun and verify methods are executed sorted by ascending order by the index, afterRun in descending order
3 Internal state of the minion, at most one instance of the minion exists per test
4 Type token to ensure type safety
5 Type-safe method to verify the result of the execution
Engaging Minion
void 'verify model with engage'() {
    given:
        def moon = [name: 'Moon', planet: 'Earth']
        MoonService moonService = Mock(MoonService)
        controller.moonService = moonService
    when:
        gru.engage(new ModelMinion(model: [moon: moon])).test {
            get '/moons/earth/moon/info'
        }
    then:
        gru.verify()
        1 * moonService.findByPlanetAndName('earth', 'moon') >> moon
}

Although it is possible to use use engage method to add new minion, it is more convenient to enhance the DSL with new method. As our minion is enhancing the request definition, we simply add new extension method to RequestDefinitionBuilder object:

Extension Class
/**
 * Add convenient methods for Grails to test definition DSL.
 */
class GrailsGruExtensions {

    // ...

    /**
     * Sets the expected model returned from the controller action.
     *
     * @param aModel expected model
     * @return self
     */
    static ResponseDefinitionBuilder model(ResponseDefinitionBuilder self, Object aModel) { (1)
        self.command(ModelMinion) {                                                         (2)
            model = aModel
        }
    }

    // ...

}
1 New extension method model will be added to ResponseDefinitionBuilder class
2 command method will add new minion to the squad if not already present and it allows to do additional configuration such as storing the model’s value, there is its counterpart ask if you want to get information from another minion from the squad

You have to create extensions class descriptor in order to make the extension methods available in the code:

src/main/resources/META-INF/services/org.codehaus.groovy.runtime.ExtensionModule
moduleName = gru-grails
moduleVersion = @VERSION@
extensionClasses = com.agorapulse.gru.grails.GrailsGruExtensions

After that the new extension method can be used in any test

Using Method from Extension
void 'verify model'() {
    given:
        def moon = [name: 'Moon', planet: 'Earth']
        MoonService moonService = Mock(MoonService)
        controller.moonService = moonService
    when:
        gru.test {
            get '/moons/earth/moon/info'
            expect {
                model moon: moon
            }
        }
    then:
        gru.verify()
        1 * moonService.findByPlanetAndName('earth', 'moon') >> moon
}

Creating New Client

Creating new client is bit more work but you can check Http client which is rather small implementation. The core idea is to provide methods to update request and to check the response returned.