ribbon

Gru is HTTP interaction testing framework with out-of-box support for Micronaut, Grails, Spring MVC, API Gateway, but it can be used with any HTTP server.

Gru is based on snapshot testing of the payloads. It excels in testing JSON response by leveraging JsonUnit placeholders to substitute dynamic values such as timestamps or random IDs.

1. Installation

Gru is available in Maven Central. You can use any combination of the following libraries you need.

Gradle Installation
repositories {
    mavenCentral()
}

dependencies {
    testImplementation platform("com.agorapulse:gru-bom:2.0.6")             (1)

    // the basic dependency including the generic HTTP client
    testImplementation("com.agorapulse:gru")                                            (2)

    // pick any of these as you need, see the bullet points below for more details
    testImplementation("com.agorapulse:gru-kotlin")                                     (3)
    testImplementation("com.agorapulse:gru-micronaut")                                  (4)
    testImplementation("com.agorapulse:gru-spring")                                     (5)
    testImplementation("com.agorapulse:gru-spring-integration-testing")                 (6)
    testImplementation("com.agorapulse:gru-grails")                                     (7)
    testImplementation("com.agorapulse:gru-api-gateway")                                (8)
    testImplementation("com.agorapulse:gru-okhttp")                                     (9)
}
1 You can use BOM to manage the dependencies, otherwise include the version number with each dependency
2 The basic module including the generic HTTP client
3 Kotlin module for Kotlin DSL
4 Micronaut module for running HTTP request against the embedded server
5 Spring module to emulate HTTP interaction using MockMvc
6 Spring module for real HTTP calls in integration tests
7 Grails module to emulate HTTP interaction within Grails Unit tests
8 AWS API Gateway module for HTTP proxy lambdas
9 Alternative OkHttp module for real HTTP calls in integration tests

2. Setup

2.1. HTTP

Gru can be used to test any HTTP endpoint, including Micronaut applications running on embedded server or Grails application running within integration tests.

Minimal HTTP Usage (Java)
package heist;

import com.agorapulse.gru.Gru;
import com.agorapulse.gru.http.Http;
import org.junit.Test;

public class HttpTest {

    Gru gru = Gru.create("https://despicableme.fandom.com");                            (1)

    @Test
    public void testGetWiki() throws Throwable {
        gru.verify(test -> test.get("/wiki/Felonius_Gru"));                             (2)
    }

}
1 Gru initialized with the base URL http://despicableme.wikia.com for HTTP calls
2 Execute HTTP GET request on /wiki/Felonius_Gru URI and verify it is accessible and returns OK(200) status code.
Minimal HTTP Usage (Kotlin)
package heist.kt

import com.agorapulse.gru.kotlin.create

import io.kotest.core.spec.style.StringSpec

class HttpTest : StringSpec({

    "minimal Gru test" {
        val gru = create("https://despicableme.fandom.com")                             (1)
        gru.verify {
            get("/wiki/Felonius_Gru")                                                   (2)
        }
    }

})
1 Gru initialized with the base URL http://despicableme.wikia.com for HTTP calls
2 Execute HTTP GET request on /wiki/Felonius_Gru URI and verify it is accessible and returns OK(200) status code.
Minimal HTTP Usage (groovy)
package heist

import com.agorapulse.gru.Gru
import spock.lang.Specification

class HttpSpec extends Specification{

    Gru gru = Gru.create('https://despicableme.fandom.com')                             (1)

    void 'despicable me'() {
        expect:
            gru.test {
                get "/wiki/Felonius_Gru"                                                (2)
            }
    }

}
1 Gru initialized with the base URL http://despicableme.wikia.com for HTTP calls
2 Execute HTTP GET request on /wiki/Felonius_Gru URI and verify it is accessible and returns OK(200) status code.
The timeouts are automatically increased to one hour when debug mode is detected.

2.2. Micronaut

Micronaut plays well with @MicronautTest annotation. If you are using @MicronautTest you can simply inject Gru instance into your test.

Using @Inject (Java)
package heist;

import com.agorapulse.gru.Gru;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;

@MicronautTest                                                                          (1)
public class InjectNativeMicronautTest {

    @Inject Gru gru;                                                                    (2)

    @Test
    public void testGet() throws Throwable {
        gru.verify(test -> test
            .get("/moons/earth/moon")
            .expect(response -> response.json("moon.json"))
        );
    }

}
1 Annotate your test with @MicronautTest
2 Inject Gru
Using @Inject (Kotlin)
package heist.kt

import com.agorapulse.gru.kotlin.Gru
import io.kotest.core.spec.style.StringSpec
import io.micronaut.test.extensions.kotest5.annotation.MicronautTest

@MicronautTest                                                                          (1)
class InjectNativeMicronautSpec(private val gru: Gru) : StringSpec({                    (2)

    "test it works" {
        gru.verify {
            get("/moons/earth/moon")
            expect {
                json("moon.json")
            }
        }
    }

})
1 Annotate your test with @MicronautTest
2 Inject Gru
Using @Inject (Groovy)
package heist

import com.agorapulse.gru.Gru
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification

import jakarta.inject.Inject

@MicronautTest                                                                          (1)
class InjectNativeMicronautSpec extends Specification {

    @Inject Gru gru                                                                     (2)

    void 'test it works'() {
        expect:
            gru.test {
                get '/moons/earth/moon'
                expect {
                    json 'moon.json'
                }
            }
    }

}
1 Annotate your test with @MicronautTest
2 Inject Gru
By default, the HTTP client uses Micronaut’s HttpClient but you can switch to different implementation by setting the property gru.http.client to okhttp or jdk. Using OkHttp client requires adding gru-okhttp library to the classpath. If you want to provide your own client implementation, you can implement Client interface and register it as a @Primary bean.

Alternatively, you can let Gru to create default ApplicationContext for you.

Default Application Context (Java)
package heist;

import com.agorapulse.gru.Gru;
import com.agorapulse.gru.micronaut.Micronaut;
import io.micronaut.context.env.Environment;
import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class AutomaticMicronautTest {

    Gru gru = Gru.create(Micronaut.create(this));                                       (1)

    @Inject Environment environment;                                                    (2)

    @Test
    public void testAutomaticContext() throws Throwable {
        assertNotNull(environment);
        assertTrue(environment.getActiveNames().contains("test"));

        gru.verify(test -> test
            .get("/moons/earth/moon")
            .expect(response -> response.json("moon.json"))
        );
    }

}
1 Create a simple Micronaut client for Gru
2 Default application context is created and fields can be injected into the test
Default Application Context (Kotlin)
package heist.kt

import com.agorapulse.gru.kotlin.create
import com.agorapulse.gru.micronaut.Micronaut
import io.micronaut.context.env.Environment
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class AutomaticMicronautTest {

    val gru = create(Micronaut.create(this))                                            (1)

    @Inject
    lateinit var environment: Environment                                               (2)

    @Test
    fun basicTest() {
        assertTrue(environment.activeNames.contains("test"))

        gru.verify {
            get("/moons/earth/moon")
            expect {
                json("moon.json")
            }
        }
    }

}
1 Create a simple Micronaut client for Gru
2 Default application context is created and fields can be injected into the test
Default Application Context (Groovy)
package heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.micronaut.Micronaut
import io.micronaut.context.env.Environment
import spock.lang.Specification

import jakarta.inject.Inject

class AutomaticMicronautSpec extends Specification {

    Gru gru = Gru.create(Micronaut.create(this))                                        (1)

    @Inject Environment environment                                                     (2)

    void 'test it works'() {
        expect:
            'test' in environment.activeNames
        and:
            gru.test {
                get '/moons/earth/moon'
                expect {
                    json 'moon.json'
                }
            }
    }

}
1 Create a simple Micronaut client for Gru
2 Default application context is created and fields can be injected into the test

If you construct the application context yourself then you need to point Gru to the field of type ApplicationContext.

Custom Application Context (Java)
package heist;

import com.agorapulse.gru.Gru;
import com.agorapulse.gru.micronaut.Micronaut;
import io.micronaut.context.ApplicationContext;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class ManualWithSimpleProviderMicronautTest {

    private Gru gru = Gru.create(Micronaut.create(this, this::getContext));             (1)

    private ApplicationContext context;
    private EmbeddedServer embeddedServer;

    @BeforeEach
    public void setup() {
        context = ApplicationContext.builder().build();
        context.start();

        embeddedServer = context.getBean(EmbeddedServer.class);
        embeddedServer.start();
    }

    @AfterEach
    public void cleanUp() {
        embeddedServer.close();
        context.close();
    }


    @Test
    public void testMoon() throws Throwable {
        gru.verify(test -> test
            .get("/moons/earth/moon")
            .expect(response -> response.json("moon.json"))
        );
    }

    private ApplicationContext getContext() {
        return context;
    }

}
1 Point Gru to field context holding the custom ApplicationContext instance
Custom Application Context (Kotlin)
package heist.kt

import com.agorapulse.gru.kotlin.create
import com.agorapulse.gru.micronaut.Micronaut
import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.server.EmbeddedServer
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class ManualWithSimpleProviderMicronautTest {

    private val gru = create(Micronaut.create(this) { context })                        (1)

    private lateinit var context: ApplicationContext
    private lateinit var embeddedServer: EmbeddedServer

    @BeforeEach
    fun setup() {
        context = ApplicationContext.builder().build()
        context.start()

        embeddedServer = context.getBean(EmbeddedServer::class.java)
        embeddedServer.start()
    }

    @AfterEach
    fun cleanUp() {
        embeddedServer.close()
        context.close()
    }


    @Test
    fun testMoon() {
        gru.verify {
            get("/moons/earth/moon")
            expect {
                json("moon.json")
            }
        }
    }

}
1 Point Gru to field context holding the custom ApplicationContext instance
Custom Application Context (Groovy)
package heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.micronaut.Micronaut
import io.micronaut.context.ApplicationContext
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Specification

class ManualWithSimpleProviderMicronautSpec extends Specification {

    Gru gru = Gru.create(Micronaut.create(this) { context })                            (1)

    @AutoCleanup ApplicationContext context
    @AutoCleanup EmbeddedServer embeddedServer

    void setup() {
        context = ApplicationContext.builder().build()
        context.start()

        embeddedServer = context.getBean(EmbeddedServer)
        embeddedServer.start()
    }

    void 'test it works'() {
        expect:
            gru.test {
                get '/moons/earth/moon'
                expect {
                    json 'moon.json'
                }
            }
    }

}
1 Point Gru to field context holding the custom ApplicationContext instance
As an alternative, you can make your test class implement io.micronaut.context.ApplicationContextProvider.

Gru can actually help you create the custom context:

Custom Application Context (Java)
package heist;

import com.agorapulse.gru.Gru;
import com.agorapulse.gru.micronaut.Micronaut;
import heist.micronaut.Moon;
import heist.micronaut.MoonService;
import io.micronaut.context.env.Environment;
import org.junit.jupiter.api.Test;

import jakarta.inject.Inject;

import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.*;

public class AdvancedAutomaticMicronautTest {

    private MoonService moonService = mock(MoonService.class);                          (1)

    private Gru gru = Gru.create(
        Micronaut.build(this)
            .doWithContextBuilder(b ->
                b.environments("my-custom-env")                                         (2)
            ).doWithContext(c ->
                c.registerSingleton(MoonService.class, moonService)                     (3)
            ).start()                                                                   (4)
    );

    @Inject
    private Environment environment;                                                    (5)

    @Test
    public void testMoon() throws Throwable {
        assertNotNull(environment);

        assertTrue(environment.getActiveNames().contains("test"));
        assertTrue(environment.getActiveNames().contains("my-custom-env"));

        when(moonService.get("earth", "moon")).thenReturn(new Moon("earth", "moon"));

        gru.verify(test -> test
            .get("/moons/earth/moon")
            .expect(response -> response.json("moon.json"))
        );

        verify(moonService, times(1)).get("earth", "moon");
    }

}
1 Declare any field you want to reference from the context before Gru declaration
2 Customize the ApplicationContextBuilder
3 Customize the ApplicationContext itself, e.g. register mocks
4 Start the application and injects fields into the test automatically
5 The field can be automatically injected
Custom Application Context (Kotlin)
package heist.kt

import com.agorapulse.gru.kotlin.create
import com.agorapulse.gru.micronaut.Micronaut
import heist.micronaut.Moon
import heist.micronaut.MoonService
import io.micronaut.context.env.Environment
import jakarta.inject.Inject
import org.junit.jupiter.api.Test

import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertTrue
import org.mockito.Mockito.*

class AdvancedAutomaticMicronautTest {

    private val moonService = mock(MoonService::class.java)                             (1)

    private val gru = create(
        Micronaut.build(this)
            .doWithContextBuilder { b ->
                b.environments("my-custom-env")                                         (2)
            }.doWithContext { c ->
                c.registerSingleton(MoonService::class.java, moonService)               (3)
            }.start()                                                                   (4)
    )

    @Inject
    private lateinit var environment: Environment                                       (5)

    @Test
    fun testMoon() {
        assertNotNull(environment)

        assertTrue(environment.activeNames.contains("test"))
        assertTrue(environment.activeNames.contains("my-custom-env"))

        `when`(moonService.get("earth", "moon")).thenReturn(Moon("earth", "moon"))

        gru.verify {
            get("/moons/earth/moon")
            expect {
                json("moon.json")
            }
        }

        verify(moonService, times(1)).get("earth", "moon")
    }

}
1 Declare any field you want to reference from the context before Gru declaration
2 Customize the ApplicationContextBuilder
3 Customize the ApplicationContext itself, e.g. register mocks
4 Start the application and injects fields into the test automatically
5 The field can be automatically injected
Custom Application Context (Groovy)
package heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.micronaut.Micronaut
import heist.micronaut.Moon
import heist.micronaut.MoonService
import io.micronaut.context.env.Environment
import spock.lang.Specification

import jakarta.inject.Inject

class AdvancedAutomaticMicronautSpec extends Specification {

    MoonService moonService = Mock()                                                    (1)

    Gru gru = Gru.create(
        Micronaut.build(this) {
            environments 'my-custom-env'                                                (2)
        }.then {
            registerSingleton(MoonService, moonService)                                 (3)
        }.start()                                                                       (4)
    )

    @Inject Environment environment                                                     (5)

    void 'test it works'() {
        expect:
            'my-custom-env' in environment.activeNames
        when:
            gru.test {
                get '/moons/earth/moon'
                expect {
                    json 'moon.json'
                }
            }
        then:
            gru.verify()

            1 * moonService.get('earth', 'moon') >> new Moon('earth', 'moon')
    }

}
1 Declare any field you want to reference from the context before Gru declaration
2 Customize the ApplicationContextBuilder
3 Customize the ApplicationContext itself, e.g. register mocks
4 Start the application and injects fields into the test automatically
5 The field can be automatically injected
You can further customize the creation of the underlying OkHttp client by implementing MicronautGruConfiguration interface.

2.3. 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 spock.lang.Specification

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

    Gru gru = Gru.create(Grails.create(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 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

2.4. Spring

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

Minimal Spring Usage (Java)
package com.agorapulse.gru.spring.heist;

import com.agorapulse.gru.Gru;
import com.agorapulse.gru.spring.Spring;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest                                                                             (1)
class BasicTest {

    @Autowired MockMvc mvc;                                                             (2)

    Gru gru = Gru.create(Spring.create(this));                                          (3)

    @Test
    void testMoon() throws Throwable {
        gru.verify(test -> test.get("/moons/earth/moon"));                              (4)
    }
}
1 Use @WebMvcTest to properly setup Spring MVC test
2 Field with type MockMvc must exist and must not be null during the test execution (implement HasMockMvc interface for faster execution)
3 Initiate Gru with Spring client
4 Issue mock GET request to /moon/earth/moon and expect it returns OK(200) response
Minimal Spring Usage (Kotlin)
package com.agorapulse.gru.spring.heist.kt

import com.agorapulse.gru.kotlin.create
import com.agorapulse.gru.spring.Spring
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc

@WebMvcTest                                                                             (1)
class BasicTest {

    @Autowired lateinit var mvc: MockMvc                                                (2)

    val gru = create(Spring.create(this))                                               (3)

    @Test
    fun testMoon() {
        gru.verify { 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
Minimal Spring Usage (Groovy)
package com.agorapulse.gru.spring.heist

import com.agorapulse.gru.Gru
import com.agorapulse.gru.spring.Spring
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)

    Gru gru = Gru.create(Spring.create(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

For Groovy tests, 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:

// use the latest appropriate version, check Maven Central if you are not sure
// https://central.sonatype.com/artifact/org.spockframework/spock-core
String spockVersion = '1.1-groovy-2.4'
testImplementation("org.spockframework:spock-spring:$spockVersion")
testImplementation("org.spockframework:spock-core:$spockVersion")

3. Common Interactions

3.1. 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.

3.1.1. URL Parameters

You can specify additional URL parameters

Additional URL Parameters (Java)
@Test
public void stealTheMoonWithShrinkRay() throws Throwable {
    gru.verify(test -> test
        .delete("/moons/earth/moon", req -> req.param("with", "shrink-ray"))
        .expect(resp -> resp.status(NO_CONTENT))
    );
}
Additional URL Parameters (Kotlin)
@Test
fun stealTheMoonWithShrinkRay() {
    gru.verify {
        delete("/moons/earth/moon") {
            param("with", "shrink-ray")
        }
        expect {
            status(NO_CONTENT)
        }
    }
}
Additional URL Parameters (Groovy)
void 'steal the moon with shrink ray'() {
    expect:
        gru.test {
            delete '/moons/earth/moon', {
                params with: 'shrink-ray'
            }
            expect {
                status NO_CONTENT
            }
        }
}

3.1.2. Request Headers

You can specify additional HTTP headers sent with the request.

Request Headers (Java)
@Test
public void visitSecretMoonNoom() throws Throwable {
    gru.verify(test -> test
        .get("/moons/earth/noom", req -> req.header("Authorization", "Felonius"))
    );
}
Request Headers (Kotlin)
@Test
fun visitSecretMoonNoom() {
    gru.verify {
        get("/moons/earth/noom") {
            header("Authorization", "Felonius")
        }
    }
}
Request Headers (Groovy)
void 'visit secret moon Noom'() {
    expect:
        gru.test {
            get '/moons/earth/noom', {
                headers Authorization: 'Felonius'
            }
        }
}

3.1.3. JSON Payload

You can use JSON payload from file.

JSON Payload (Java)
@Test
public void createMoonForMargot() throws Throwable {
    gru.verify(test -> test.
        post("/moons/earth", req -> req.json("newMoonRequest.json"))
    );
}
JSON Payload (Kotlin)
@Test
fun createMoonForMargot() {
    gru.verify {
        post("/moons/earth") {
            json("newMoonRequest.json")
        }
    }
}
JSON Payload (Groovy)
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(Collections.singletonMap("foo", "bar")) in Java or json(foo: 'bar') in Groovy. 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. See Fixt for more details.
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.

3.1.4. Generic Payload

You can use generic payload from file.

Generic Payload (Java)
@Test
public void createMoonForMargotGeneric() throws Throwable {
    gru.verify(test -> test.
        post("/moons/earth", req -> req.content("newMoonRequest.json", "application/json"))
    );
}
Generic Payload (Kotlin)
@Test
fun createMoonForMargotGeneric() {
    gru.verify {
        post("/moons/earth") {
            content("newMoonRequest.json", "application/json")
        }
    }
}
Generic Payload (Groovy)
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 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.

3.1.5. File Upload

You can emulate multipart file uploads.

Generic Payload (Java)
@Test
public void uploadFile() throws Throwable {
    gru.verify(test -> test
        .post(
            "/upload",
            req -> req.upload(u -> u
                .param("message", "Hello")
                .file(
                    "theFile",
                    "hello.txt",
                    inline("Hello World"),
                    "text/plain"
                )
            )
        )
        .expect(resp -> resp.text(inline("11")))
    );
}
File Upload (Kotlin)
@Test
fun uploadFile() {
    gru.verify {
        post("/upload") {
            upload {                                                                (1)
                param("message", "Hello")                                           (2)
                file(                                                               (3)
                    "theFile",
                    "hello.txt",
                    inline("Hello World"),
                    "text/plain"
                )
            }
        }
        expect {
            text(inline("11"))
        }
    }
}
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
File Upload (Groovy)
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

3.1.6. Cookies

You can emulate cookies send by the browser or verify they have been set by the server response.

Cookies (Java)
@Test
public void sendCookies() throws Throwable {
    gru.verify(test -> test
        .get("/moons/cookie", req -> req.
            cookie("chocolate", "rules")                                            (1)
        )
        .expect(resp -> resp.
            json("cookies.json", Option.IGNORING_EXTRA_FIELDS)
        )
    );
}

@Test
public void setCookies() throws Throwable {
    gru.verify(test -> test
        .get("/moons/setCookie")
        .expect(resp -> resp
            .cookie("chocolate", "rules")                                           (2)
            .cookie(c -> c                                                          (3)
                .name("coffee")
                .value("lover")
                .secure(true)
                .domain("localhost")
            )
        )
    );
}
Cookies (Kotlin)
@Test
fun sendCookies() {
    gru.verify {
        get("/moons/cookie") {
            cookie("chocolate", "rules")                                            (1)
        }
        expect {
            json("cookies.json", Option.IGNORING_EXTRA_FIELDS)
        }
    }
}

@Test
fun setCookies() {
    gru.verify {
        get("/moons/setCookie")
        expect {
            cookie("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
Cookies (Groovy)
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

3.2. Response

Response expectation are defined using expect method within test definition.

3.2.1. 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 (Java)
@Test
public void stealTheMoonWithShrinkRay() throws Throwable {
    gru.verify(test -> test
        .delete("/moons/earth/moon", req -> req.param("with", "shrink-ray"))
        .expect(resp -> resp.status(NO_CONTENT))
    );
}
Status Code (Kotlin)
@Test
fun stealTheMoonWithShrinkRay() {
    gru.verify {
        delete("/moons/earth/moon") {
            param("with", "shrink-ray")
        }
        expect {
            status(NO_CONTENT)
        }
    }
}
Status Code (Groovy)
void 'steal the moon with shrink ray'() {
    expect:
        gru.test {
            delete '/moons/earth/moon', {
                params with: 'shrink-ray'
            }
            expect {
                status NO_CONTENT
            }
        }
}
If multiple statuses are possible then you can use statuses(…​) method instead.

3.2.2. Response Headers

You can specify expected response headers.

Response Headers (Java)
@Test
public void jsonIsRendered() throws Throwable {
    gru.verify(test -> test
        .get("/moons/earth/moon")
        .expect(resp -> resp.header("Content-Type", "application/json"))
    );
}
Response Headers (Kotlin)
@Test
fun jsonIsRendered() {
    gru.verify {
        get("/moons/earth/moon")
        expect { header("Content-Type", "application/json") }
    }
}
Response Headers (Groovy)
void 'json is rendered'() {
    expect:
        gru.test {
            get '/moons/earth/moon'
            expect {
                headers 'Content-Type': 'application/json;charset=UTF-8'
            }
        }
}

3.2.3. Redirection

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

Redirection (Java)
@Test
public void noPlanetNeeded() throws Throwable {
    gru.verify(test -> test
        .get("/moons/-/moon")
        .expect(resp -> resp.redirect("/moons/earth/moon"))
    );
}
Redirection (Kotlin)
@Test
fun noPlanetNeeded() {
    gru.verify {
        get("/moons/-/moon")
        expect { redirect("/moons/earth/moon") }
    }
}
Redirection (Groovy)
void 'no planet needed'() {
    expect:
        gru.test {
            get '/moons/-/moon'
            expect {
                redirect '/moons/earth/moon'
            }
        }
}

3.2.4. Plain Text Response

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

Plain Text Response (Java)
@Test
public void infoIsRendered() throws Throwable {
    gru.verify(test -> test
        .get("/moons/earth/moon/info")
        .expect(resp -> resp.text("textResponse.txt"))
    );
}
Plain Text Response (Kotlin)
void 'verify text'() {
    expect:
        gru.test {
            get '/moons/earth/moon/info', {
                headers 'Accept': 'text/plain'
            }
            expect {
                text 'textResponse.txt'
            }
        }
}
Plain Text Response (Groovy)
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 (Java)
@Test
public void infoIsRenderedInline() throws Throwable {
    gru.verify(test -> test
        .get("/moons/earth/moon/info")
        .expect(resp -> resp.text(inline("moon goes around earth")))
    );
}
Plain Text Inline Response (Kotlin)
@Test
fun infoIsRenderedInline() {
    gru.verify {
        get("/moons/earth/moon/info")
        expect { text(inline("moon goes around earth")) }
    }
}
Plain Text Inline Response (Groovy)
void 'verify text inline'() {
    expect:
        gru.test {
            get '/moons/earth/moon/info', {
                headers 'Accept': 'text/plain'
            }
            expect {
                text inline('Moon goes around Earth')
            }
        }
}

3.2.5. JSON Response

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

JSON Response (Java)
@Test
public void verifyJson() throws Throwable {
    gru.verify(test -> test
        .get("/moons/earth/moon")
        .expect(resp -> resp.json("moonResponse.json"))
    );
}
JSON Response (Groovy)
@Test
fun verifyJson() {
    gru.verify {
        get("/moons/earth/moon")
        expect { json("moonResponse.json") }
    }
}
JSON Response (Groovy)
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 (Java)
@Test
public void verifyJsonWithOptions() throws Throwable {
    gru.verify(test -> test
        .get("/moons/earth")
        .expect(resp -> resp.json("moonsResponse.json", Option.IGNORING_EXTRA_ARRAY_ITEMS))
    );
}
JSON Response with Options (Kotlin)
@Test
fun verifyJsonWithOptions() {
    gru.verify {
        get("/moons/earth")
        expect { json("moonsResponse.json", Option.IGNORING_EXTRA_ARRAY_ITEMS) }
    }
}
JSON Response with Options (Groovy)
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 (Java)
@Test
public void customiseJsonUnit() throws Throwable {
    gru.verify(test -> test
        .get("/moons/earth/moon")
        .expect(resp -> resp
            .json("moonResponse.json")
            .json(json -> json
                .withTolerance(0.1)
                .withMatcher(
                    "negativeIntegerString",
                    MatchesPattern.matchesPattern("-\\d+")
                )
            )
        )
    );
}
JsonUnit Customisation (Kotlin)
@Test
fun customiseJsonUnit() {
    gru.verify {
        get("/moons/earth/moon")
        expect {
            json("moonResponse.json")
            json {
                withTolerance(0.1)
                withMatcher(
                    "negativeIntegerString",
                    MatchesPattern.matchesPattern("-\\d+")
                )
            }
        }
    }
}
JsonUnit Customisation (Groovy)
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

3.2.6. 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 (Java)
@Test
public void verifyHtml() throws Throwable {
    gru.verify(test -> test
        .get("/moons/earth/moon/html")
        .expect(resp -> resp.html("htmlResponse.html"))
    );
}
HTML Response (Kotlin)
@Test
fun verifyHtml() {
    gru.verify {
        get("/moons/earth/moon/html")
        expect { html("htmlResponse.html") }
    }
}
HTML Response (Groovy)
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.

3.2.7. Capturing Text Response

You can reach the response text from any content minion such as JsonMinion if you need to access the actual response.

Reading Response Text (Java)
@Test
public void testGet() throws Throwable {
    gru.test(test -> test                                                           (1)
        .get("/moons/earth/moon")
        .expect(response -> response.json("moon.json"))
    );

    gru.verify();                                                                   (2)

    String responseText = gru.getSquad()
        .ask(JsonMinion.class, AbstractContentMinion::getResponseText);             (3)

    Assertions.assertTrue(responseText.contains("moon"));                           (4)

    gru.close();                                                                    (5)
}
1 use test instead of verify(Consumer) to keep Gru’s internal state
2 use verify to perform the verification call and populate the response text
3 read the response text from JsonMinion
4 make some assertion using the response text
5 don’t forget to close/reset the Gru instance manually
Reading Response Text (Kotlin)
@Test
fun testGet() {
    gru.test {                                                                      (1)
        get("/moons/earth/moon")
        expect { json("moon.json") }
    }

    gru.verify()                                                                    (2)

    val responseText = gru.squad.ask<JsonMinion, String> {
        responseText                                                                (3)
    }

    Assertions.assertNotNull(responseText)
    Assertions.assertTrue(responseText!!.contains("moon"))                          (4)

    gru.close()                                                                     (5)
}
1 split the verification call between when and then instead of expect
2 use verify to perform the verification call and populate the response text
3 read the response text from JsonMinion
4 make some assertion using the response text
5 don’t forget to close/reset the Gru instance manually
Reading Response Text (Groovy)
void 'test it works'() {
    when:                                                                           (1)
        gru.test {
            get '/moons/earth/moon'
            expect {
                json 'moon.json'
            }
        }

    then:
        gru.verify()                                                                (2)

    when:
        String responseText = gru.squad.ask(JsonMinion) { responseText }            (3)
    then:
        responseText.contains('moon')                                               (4)
}
1 split the verification call between when and then instead of expect
2 use verify to perform the verification call and populate the response text
3 read the response text from JsonMinion
4 make some assertion using the response text

4. 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.

4.1. Artifacts

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

4.1.1. 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 spock.lang.Specification

class IncludeUrlMappingsSpec extends Specification implements ControllerUnitTest<MoonController> {

    Gru gru = Gru.create(Grails.create(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

4.1.2. 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 spock.lang.Specification

class IncludeInterceptorSpec extends Specification implements ControllerUnitTest<MoonController> {

    Gru gru = Gru.create(Grails.create(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

4.2. Request

4.2.1. 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.

4.3. Response

4.3.1. 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'
            }
        }
}

4.3.2. 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
}

4.4. Integration Tests

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

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

Gru gru = Gru.create(Http.create(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

5. 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.

5.1. 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 (Java)
@Test
public void jsonIsRendered() throws Throwable {
    gru.verify(test -> test
        .get("/moons/earth/moon", req -> req
            .command(RequestBuilderMinion.class, m -> m                             (1)
                .addBuildStep(mock -> mock
                    .accept(MediaType.APPLICATION_JSON_UTF8)                        (2)
                    .locale(Locale.CANADA)
                )
            )
        )
        .expect(resp -> resp
            .header("Content-Type", "application/json;charset=UTF-8")
            .json("moonResponse.json")
            .command(ResultMatcherMinion.class, m -> m                              (3)
                .addMatcher(content().encoding("UTF-8"))                            (4)
                .addMatcher(content().contentType(MediaType.APPLICATION_JSON_UTF8))
            )
        )
    );
}
1 Command RequestBuilderMinion minion to fine-tune MockHttpServletRequestBuilder
2 Declare Accept header
3 Command ResultBuilderMinion minion to add additional result matchers <4>> Check the content encoding using the content() method that is statically imported from MockMvcResultMatchers class, asserts the content encoding
MockMVC Builders (Groovy)
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
MockMVC Builders (Kotlin)
@Test
fun jsonIsRendered() {
    gru.verify {
        get("/moons/earth/moon") {
            command<RequestBuilderMinion> {                                         (1)
                addBuildStep { mock ->
                    mock.accept(MediaType.APPLICATION_JSON_UTF8)                    (2)
                        .locale(Locale.CANADA)
                }
            }
        }
        expect {
            header("Content-Type", "application/json;charset=UTF-8")
            json("moonResponse.json")
            command<ResultMatcherMinion> {                                          (3)
                addMatcher(content().encoding("UTF-8"))                             (4)
                addMatcher(content().contentType(MediaType.APPLICATION_JSON_UTF8))
            }
        }
    }
}
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.2. Integration Tests

You use Http client to verify controller within @Integration test. Once gru-spring-integration-test is on the classpath then you can inject Gru bean into your tests.

Integration Setup (Java)
package com.agorapulse.gru.spring.heist;

import com.agorapulse.gru.Gru;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

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

    @Autowired private Gru gru;                                                         (2)

    @Test
    public void renderJson() throws Throwable {                                         (3)
        gru.verify(test -> test
            .get("/moons/earth/moon")
            .expect(resp -> resp.json("renderJsonResponse.json"))
        );
    }

}
1 Run integration test using the random port
2 Inject Gru into your tests
3 Test the application
Integration Setup (Groovy)
package com.agorapulse.gru.spring.heist

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

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

    @Autowired Gru gru                                                                  (2)

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

}
1 Run integration test using the random port
2 Inject Gru into your tests
3 Test the application

6. 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.create(ApiGatewayProxy.create {
    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.create(ApiGatewayProxy.create {
    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

7. Extending Gru

7.1. 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 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.
 */
@CompileDynamic
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
}

7.2. 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.

8. Release Notes

8.1. 2.0.0

8.1.1. Breaking Changes

  • The minimal Java version has been set to Java 11

  • The Micronaut module has been upgraded to Micronaut 4.x, Groovy 4.x and Java 17

  • The HTTP module has been removed as the Http client is now part of the core gru package

  • The original HTTP module has been renamed to gru-okhttp and the client accordingly to OkHttp

  • The libraries required to evaluate HTML responses has been moved to compile only dependencies

  • Micronaut module now uses micronaut-http-client instead of the default HTTP one

8.1.2. New Features

  • New JDK based Http client

  • New Microaut HTTP Client based client

  • Small improvements in the Kotlin DSL

  • No need to specify the reference class when calling Gru.create() or Http.create(). The class is automatically detected using the stack walker API.