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.
repositories {
mavenCentral()
}
dependencies {
testImplementation platform("com.agorapulse:gru-bom:2.1.2") (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.
package heist;
import com.agorapulse.gru.Gru;
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. |
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. |
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.
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 |
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 |
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.
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 |
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 |
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
.
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 |
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 |
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:
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 |
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 |
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.
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.
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.
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 |
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 |
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
|
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
@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))
);
}
@Test
fun stealTheMoonWithShrinkRay() {
gru.verify {
delete("/moons/earth/moon") {
param("with", "shrink-ray")
}
expect {
status(NO_CONTENT)
}
}
}
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.
@Test
public void visitSecretMoonNoom() throws Throwable {
gru.verify(test -> test
.get("/moons/earth/noom", req -> req.header("Authorization", "Felonius"))
);
}
@Test
fun visitSecretMoonNoom() {
gru.verify {
get("/moons/earth/noom") {
header("Authorization", "Felonius")
}
}
}
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.
@Test
public void createMoonForMargot() throws Throwable {
gru.verify(test -> test.
post("/moons/earth", req -> req.json("newMoonRequest.json"))
);
}
@Test
fun createMoonForMargot() {
gru.verify {
post("/moons/earth") {
json("newMoonRequest.json")
}
}
}
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.
@Test
public void createMoonForMargotGeneric() throws Throwable {
gru.verify(test -> test.
post("/moons/earth", req -> req.content("newMoonRequest.json", "application/json"))
);
}
@Test
fun createMoonForMargotGeneric() {
gru.verify {
post("/moons/earth") {
content("newMoonRequest.json", "application/json")
}
}
}
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.
@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")))
);
}
@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 |
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.
@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")
)
)
);
}
@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 |
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.
@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))
);
}
@Test
fun stealTheMoonWithShrinkRay() {
gru.verify {
delete("/moons/earth/moon") {
param("with", "shrink-ray")
}
expect {
status(NO_CONTENT)
}
}
}
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.
@Test
public void jsonIsRendered() throws Throwable {
gru.verify(test -> test
.get("/moons/earth/moon")
.expect(resp -> resp.header("Content-Type", "application/json"))
);
}
@Test
fun jsonIsRendered() {
gru.verify {
get("/moons/earth/moon")
expect { header("Content-Type", "application/json") }
}
}
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)
.
@Test
public void noPlanetNeeded() throws Throwable {
gru.verify(test -> test
.get("/moons/-/moon")
.expect(resp -> resp.redirect("/moons/earth/moon"))
);
}
@Test
fun noPlanetNeeded() {
gru.verify {
get("/moons/-/moon")
expect { redirect("/moons/earth/moon") }
}
}
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.
@Test
public void infoIsRendered() throws Throwable {
gru.verify(test -> test
.get("/moons/earth/moon/info")
.expect(resp -> resp.text("textResponse.txt"))
);
}
void 'verify text'() {
expect:
gru.test {
get '/moons/earth/moon/info', {
headers 'Accept': 'text/plain'
}
expect {
text 'textResponse.txt'
}
}
}
void 'verify text'() {
expect:
gru.test {
get '/moons/earth/moon/info', {
headers 'Accept': 'text/plain'
}
expect {
text 'textResponse.txt'
}
}
}
Moon goes around Earth
You can also specify the text inline. Inline method is also available for JSON and HTML.
@Test
public void infoIsRenderedInline() throws Throwable {
gru.verify(test -> test
.get("/moons/earth/moon/info")
.expect(resp -> resp.text(inline("moon goes around earth")))
);
}
@Test
fun infoIsRenderedInline() {
gru.verify {
get("/moons/earth/moon/info")
expect { text(inline("moon goes around earth")) }
}
}
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.
@Test
public void verifyJson() throws Throwable {
gru.verify(test -> test
.get("/moons/earth/moon")
.expect(resp -> resp.json("moonResponse.json"))
);
}
@Test
fun verifyJson() {
gru.verify {
get("/moons/earth/moon")
expect { json("moonResponse.json") }
}
}
void 'verify json'() {
expect:
gru.test {
get '/moons/earth/moon'
expect {
json 'moonResponse.json'
}
}
}
{
"name": "Moon",
"planet": "Earth",
"created": "${json-unit.matches:isoDate}"
}
You can pass JsonUnit options along with the file name.
@Test
public void verifyJsonWithOptions() throws Throwable {
gru.verify(test -> test
.get("/moons/earth")
.expect(resp -> resp.json("moonsResponse.json", Option.IGNORING_EXTRA_ARRAY_ITEMS))
);
}
@Test
fun verifyJsonWithOptions() {
gru.verify {
get("/moons/earth")
expect { json("moonsResponse.json", Option.IGNORING_EXTRA_ARRAY_ITEMS) }
}
}
void 'verify json 2'() {
expect:
gru.test {
get '/moons/earth'
expect {
json 'moonsResponse.json', IGNORING_EXTRA_ARRAY_ITEMS
}
}
}
[
{
"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:
Placeholder | Description |
---|---|
|
Ignore content of the property |
|
Content must match regular expression |
|
Any string |
|
Any boolean |
|
Any number |
Placeholder | Description |
---|---|
|
Any positive number as string |
|
Any date in ISO format |
|
ISO within last hour |
|
Any URL (string parsable by |
You can customize JsonUnit
fluent assertion within json
block inside expect
definition:
@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+")
)
)
)
);
}
@Test
fun customiseJsonUnit() {
gru.verify {
get("/moons/earth/moon")
expect {
json("moonResponse.json")
json {
withTolerance(0.1)
withMatcher(
"negativeIntegerString",
MatchesPattern.matchesPattern("-\\d+")
)
}
}
}
}
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 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.
@Test
public void verifyHtml() throws Throwable {
gru.verify(test -> test
.get("/moons/earth/moon/html")
.expect(resp -> resp.html("htmlResponse.html"))
);
}
@Test
fun verifyHtml() {
gru.verify {
get("/moons/earth/moon/html")
expect { html("htmlResponse.html") }
}
}
void 'verify html'() {
expect:
gru.test {
get '/moons/earth/moon/info'
expect {
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.
@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 |
@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 |
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.
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.
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.
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.
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.
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.
@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.
@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 |
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 |
@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.
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 |
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.
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
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.
/**
* @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.
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 |
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:
/**
* 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:
moduleName = gru-grails moduleVersion = @VERSION@ extensionClasses = com.agorapulse.gru.grails.GrailsGruExtensions
After that the new extension method can be used in any test
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
}
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 coregru
package -
The original HTTP module has been renamed to
gru-okhttp
and the client accordingly toOkHttp
-
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