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.
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:{project-version}") // (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)
}
-
You can use BOM to manage the dependencies, otherwise include the version number with each dependency
-
The basic module including the generic HTTP client
-
Kotlin module for Kotlin DSL
-
Micronaut module for running HTTP request against the embedded server
-
Spring module to emulate HTTP interaction using
MockMvc -
Spring module for real HTTP calls in integration tests
-
Grails module to emulate HTTP interaction within Grails Unit tests
-
AWS API Gateway module for HTTP proxy lambdas
-
Alternative OkHttp module for real HTTP calls in integration tests
Setup
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://example.com"); // (1)
@Test
public void testGetWiki() throws Throwable {
gru.verify(test -> test.get("/")); // (2)
}
}
-
Gruinitialized with the base URLhttp://despicableme.wikia.comfor HTTP calls -
Execute HTTP GET request on
/wiki/Felonius_GruURI and verify it is accessible and returnsOK(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://example.com") // (1)
gru.verify {
get("/") // (2)
}
}
})
-
Gruinitialized with the base URLhttp://despicableme.wikia.comfor HTTP calls -
Execute HTTP GET request on
/wiki/Felonius_GruURI and verify it is accessible and returnsOK(200)status code.
package heist
import com.agorapulse.gru.Gru
import spock.lang.Specification
class HttpSpec extends Specification{
Gru gru = Gru.create('https://example.com') // (1)
void 'example get'() {
expect:
gru.test {
get '/' // (2)
}
}
}
-
Gruinitialized with the base URLhttp://despicableme.wikia.comfor HTTP calls -
Execute HTTP GET request on
/wiki/Felonius_GruURI and verify it is accessible and returnsOK(200)status code.
|
Tip
|
The timeouts are automatically increased to one hour when debug mode is detected. |
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"))
);
}
}
-
Annotate your test with
@MicronautTest -
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")
}
}
}
})
-
Annotate your test with
@MicronautTest -
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'
}
}
}
}
-
Annotate your test with
@MicronautTest -
Inject
Gru
|
Tip
|
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"))
);
}
}
-
Create a simple
Micronautclient forGru -
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")
}
}
}
}
-
Create a simple
Micronautclient forGru -
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'
}
}
}
}
-
Create a simple
Micronautclient forGru -
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;
}
}
-
Point
Gruto fieldcontextholding the customApplicationContextinstance
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")
}
}
}
}
-
Point
Gruto fieldcontextholding the customApplicationContextinstance
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'
}
}
}
}
-
Point
Gruto fieldcontextholding the customApplicationContextinstance
|
Note
|
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");
}
}
-
Declare any field you want to reference from the context before
Grudeclaration -
Customize the
ApplicationContextBuilder -
Customize the
ApplicationContextitself, e.g. register mocks -
Start the application and injects fields into the test automatically
-
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")
}
}
-
Declare any field you want to reference from the context before
Grudeclaration -
Customize the
ApplicationContextBuilder -
Customize the
ApplicationContextitself, e.g. register mocks -
Start the application and injects fields into the test automatically
-
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')
}
}
-
Declare any field you want to reference from the context before
Grudeclaration -
Customize the
ApplicationContextBuilder -
Customize the
ApplicationContextitself, e.g. register mocks -
Start the application and injects fields into the test automatically
-
The field can be automatically injected
|
Tip
|
You can further customize the creation of the underlying OkHttp client by implementing MicronautGruConfiguration interface.
|
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()
}
}
-
The only requirement for Grails integration is that specification must implement
ControllerUnitTesttrait -
Gruinitialized with the specification object -
Include the appropriate
UrlMappingssoGruknows which controller action should be executed -
Gruexecutes action forGETrequest to URL/moons/earth/moonand verifies it does not throw any exception and it returns statusOK(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)
}
-
Mock the
moonService -
Call the
testmethod within thewhenblock -
Call
verifymethod manually -
Verify Spock interactions on
moonServiceinthenblock
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)
}
}
-
Use
@WebMvcTestto properly setup Spring MVC test -
Field with type
MockMvcmust exist and must not benullduring the test execution (implementHasMockMvcinterface for faster execution) -
Initiate
GruwithSpringclient -
Issue mock
GETrequest to/moon/earth/moonand expect it returnsOK(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)
}
}
-
Use
@WebMvcTestto properly setup Spring MVC test, see the warning bellow -
Field with type
MockMvcmust exist and must not benullduring the test execution -
Initiate
GruwithSpringclient -
Issue mock
GETrequest to/moon/earth/moonand expect it returnsOK(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)
}
}
}
-
Use
@WebMvcTestto properly setup Spring MVC test, see the warning bellow -
Field with type
MockMvcmust exist and must not benullduring the test execution -
Initiate
GruwithSpringclient -
Issue mock
GETrequest to/moon/earth/moonand expect it returnsOK(200)response
|
Warning
|
For Groovy tests, Spock version at least
|
Common Interactions
Request
Each Gru tests begins with HTTP request. There are methods for each of following HTTP methods:
head, post, put, patch,
delete, options, trace and get. Each method accepts the URI to perform the request and optional request
configuration closure.
|
Note
|
Grails client requires that UrlMappings for given URI is specified unless the mappings are defined in UrlMappings class
declared in default package. See Minimal Grails Usage for reference.
|
URL Parameters
You can specify additional URL parameters
@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
}
}
}
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'
}
}
}
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'
}
}
}
|
Tip
|
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.
|
|
Note
|
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.
|
|
Tip
|
Fixture JSON files are created automatically if missing. Empty object ({}) is created for missing request JSON file and JSON file
with the content same as the one returned from the controller is created if response file is missing but exception is thrown
so you have to run the test again and verify it is repeatable. Fixture files are generated inside directory from system property TEST_RESOURCES_FOLDER or
in src/test/resources relatively to the working directory. See the following example how to easily set the property in Gradle.
|
Generic Payload
You can use generic payload from file.
@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'
}
}
}
|
Note
|
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.
|
|
Tip
|
Content files are created automatically if missing. Empty file is created for missing request content file.
Fixture files are generated inside directory from system property TEST_RESOURCES_FOLDER or
in src/test/resources relatively to the working directory.
|
File Upload
You can emulate multipart file uploads.
@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"))
}
}
}
-
Define upload files and parameters
-
You can specify arbitrary parameter (i.g. hidden fields)
-
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'
}
}
}
-
Define upload files and parameters
-
You can specify arbitrary parameter (i.g. hidden fields)
-
Specify the file to be uploaded with the name of the parameter, original filename, content and content type
-
In case of Grails unit test, you must point to the controller action’s method
Cookies
You can emulate cookies send by the browser or 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")
}
}
}
}
-
Send browser cookies
-
Expect cookies sent by the server
-
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'
}
}
}
}
-
Send browser cookies
-
Expect cookies sent by the server
-
More detailed response cookie expectations
Response
Response expectation are defined using expect method within test definition.
Status Code
You can specify expected response status code. Constants for HTTP status codes are available within the expect closure call.
Default status is OK(200) which is asserted within every test. See Redirection for exception.
@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
}
}
}
|
Tip
|
If multiple statuses are possible then you can use statuses(…) method instead.
|
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'
}
}
}
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'
}
}
}
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')
}
}
}
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+/)
)
}
}
}
}
|
Warning
|
JsonFluentAssert is immutable, so only last statement actually matters. That is why .withMatcher is called
as method chain.
|
|
Tip
|
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 |
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>
</head>
<body>
<h1>Moon</h1>
<h2>Earth</h2>
<p>${xml-unit.ignore}</p>
</body>
</html>
|
Warning
|
HTML support for Grails is currently experimental as it does not apply the layouts. |
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 {
Assertions.assertNull(gru.getLastResponseBody());
gru.verify(test -> test // (1)
.get("/moons/earth/moon")
.expect(response -> response.json("moon.json"))
);
Assertions.assertNotNull(gru.getLastResponseBody()); // (2)
Assertions.assertTrue(gru.getLastResponseBody().contains("moon")); // (3)
}
-
verify the HTTP interaction
-
read the response text from
Gru -
make some assertion using the response text
@Test
fun testGet() {
gru.verify { // (1)
get("/moons/earth/moon")
expect { json("moon.json") }
}
Assertions.assertNotNull(gru.getLastResponseBody()) // (2)
Assertions.assertTrue(gru.getLastResponseBody().contains("moon")) // (3)
}
-
verify the HTTP interaction
-
read the response text from
Gru -
make some assertion using the response text
void 'test it works'() {
when: // (1)
gru.test {
get '/moons/earth/moon'
expect {
json 'moon.json'
}
}
then:
gru.verify() // (2)
when:
String responseText = gru.lastResponseBody // (3)
then:
responseText.contains('moon') // (4)
}
-
split the verification call between
whenandtheninstead ofexpect -
use
verifyto perform the verification call and populate the response text -
read the response text from
JsonMinion -
make some assertion using the response text
Grails Unit Test Interactions
On top of standard HTTP interaction, Gru provides additional methods specific for Grails only.
|
Note
|
As of IntelliJ IDEA 2017.2 you get false warnings about the wrong method’s argument being used. You have to ignore them at the moment. Please, vote for IDEA-177530 bug to get this fixed soon. |
Artifacts
You can add additional Grails artifacts using include method. Only interceptors and URL mappings are supported
at the moment.
URL Mappings
Unless you are using UrlMappings class in default package, you have to specify the URL mappings class used for matching
the URIs. You can use just name of the class as string if your URL mappings resides in default package.
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
}
}
}
}
-
Include
ApiUrlMappingsURL mappings -
The URI will be matched using
ApiUrlMappingsURL mappings
Interceptors
If you’re controller heavily depends on interceptor it is sometimes better to test the interceptors and controllers as
a single unit. You can include interceptors into test in similar way as url mappings using the include method.
Once the interceptor is included it must match the given URL otherwise exception is thrown in the verification phase.
You can add additional boolean true parameter to the include method to also autowire the interceptor automatically.
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)
}
}
}
}
-
Include
VectorInterceptorinterceptor and let it autowire the beans -
Declare bean which is used in the interceptor
-
The expectations reflects the interceptor changes and the defined bean
Request
Controller Action
You can verify that given URI is mapped to particular action. Use method reference with .& to obtain
particular MethodClosure instance.
void 'verify action'() {
expect:
gru.test {
get '/moons/earth/moon', {
executes controller.&moon
}
}
}
The test only passes if /moons/earth/moon URI is mapped to action moon in MoonController.
Response
Forwarding
You can specify the expected forward URI.
void 'verify forward'() {
expect:
gru.test {
get '/moons/earth/moon', {
params info: 'true'
}
expect {
forward '/moons/earth/moon/info'
}
}
}
Model
You can specify the expected model object returned from the controller.
void 'verify model'() {
given:
def moon = [name: 'Moon', planet: 'Earth']
MoonService moonService = Mock(MoonService)
controller.moonService = moonService
when:
gru.test {
get '/moons/earth/moon/info'
expect {
model moon: moon
}
}
then:
gru.verify()
1 * moonService.findByPlanetAndName('earth', 'moon') >> moon
}
Integration Tests
You use Http client to verify controller within @Integration test.
@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)
}
}
-
Let Grails inject the server port
-
Use
Httpclient instad ofGrails -
Store server URL in variable, otherwise it won’t get properly evaluated
-
Set the base URI for the executed test
Spring Unit Test Integration
On top of standard HTTP interaction, Gru provides additional methods specific for Spring only. These methods allows you to fully leverage the power of MockMvc.
|
Note
|
As of IntelliJ IDEA 2017.2 you get false warnings about the wrong method’s argument being used. You have to ignore them at the moment. Please, vote for IDEA-177530 bug to get this fixed soon. |
MockMVC Builders
You can configure additional request steps within request method closure definition and you can use that
within expect block to add additional ResultMatcher. Both methods are aliased to and method within their
blocks so you can write the assertions in more fluent way.
@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) // (2)
.locale(Locale.CANADA)
)
)
)
.expect(resp -> resp
.header("Content-Type", "application/json")
.json("moonResponse.json")
.command(ResultMatcherMinion.class, m -> m // (3)
.addMatcher(content().encoding("UTF-8")) // (4)
.addMatcher(content().contentType(MediaType.APPLICATION_JSON))
)
)
);
}
-
Command
RequestBuilderMinionminion to fine-tune MockHttpServletRequestBuilder -
Declare
Acceptheader -
Command
ResultBuilderMinionminion to add additional result matchers <4>> Check the content encoding using thecontent()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) // (2)
}
and { // (3)
locale(Locale.CANADA)
}
}
expect {
headers 'Content-Type': 'application/json'
json 'moonResponse.json'
that content().encoding('UTF-8') // (4)
and content().contentType(MediaType.APPLICATION_JSON) // (5)
}
}
}
-
Use
requestmethod to fine-tune MockHttpServletRequestBuilder -
Declare
Acceptheader -
Same as
request, declares desired locale -
Use
thatmethod to add additional result matchers, thecontent()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) // (2)
.locale(Locale.CANADA)
}
}
}
expect {
header("Content-Type", "application/json")
json("moonResponse.json")
command<ResultMatcherMinion> { // (3)
addMatcher(content().encoding("UTF-8")) // (4)
addMatcher(content().contentType(MediaType.APPLICATION_JSON))
}
}
}
}
-
Use
requestmethod to fine-tune MockHttpServletRequestBuilder -
Declare
Acceptheader -
Same as
request, declares desired locale -
Use
thatmethod to add additional result matchers, thecontent()method is statically imported from MockMvcResultMatchers class, asserts the content encoding
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"))
);
}
}
-
Run integration test using the random port
-
Inject
Gruinto your tests -
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'
}
}
}
}
-
Run integration test using the random port
-
Inject
Gruinto your tests -
Test the application
AWS API Gateway
|
Note
|
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
})
-
You can specify the name of the handler by class name with optional method name after double colon
:: -
You can specify the handler by class reference
-
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)
}
}
})
-
The main difference is that you specify the request and response mapping
-
You can specify which path parameters are being extracted into the function input object
-
You can specify which query parameters are being extracted into the function input object
-
You can specify the response mapping with given status
-
You can specify the headers mapping for the response
Extending Gru
Extending DSL
Gru DSL can be easily extend. Gru uses minions whenever it is possible. Minion interface
requires several methods to be implemented to intercept the test flow.
/**
* @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.
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.
Implement AbstractMinion<C> to get type safety and to implement only the methods you need. Engage the minion with gru.engage(new YourMinion()) inside the test, or expose it through a Groovy category/extension class so callers can configure it with a single DSL method instead of a manual engage call.
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.
Release Notes
3.0.0
Breaking Changes
Baseline:
-
Minimum Java raised to Java 25; Gradle 9.5 required to build.
-
Groovy bumped from 3.x to 5.0.x. The published Maven coordinate also moved from
org.codehaus.groovy:groovytoorg.apache.groovy:groovy. Consumers on Groovy 3 will need to upgrade. -
Spock test dependency bumped from
2.3-groovy-3.0to2.4-groovy-5.0. -
Kotlin baseline raised to 2.3.21 (required for the JVM-25 target).
Micronaut module:
-
gru-micronautis now compiled against Micronaut 5.x. Consumers must be on Micronaut 5 (io.micronaut:micronaut-core:5.x).
Spring module:
-
gru-springnow compiles against Spring Framework 6.2.x and Servlet 6 (jakarta.servlet). The legacyjavax.servletdependency has been dropped. Consumers must be on Spring 6+ / Spring Boot 3+. -
The deprecated
MediaType.APPLICATION_JSON_UTF8is no longer used; Spring 6 no longer appends;charset=UTF-8toapplication/jsonresponses. Tests asserting on the literalapplication/json;charset=UTF-8content type must be updated toapplication/json. -
MockMvcRequestBuilders.fileUpload(…)was removed in Spring 6;gru-springnow usesMockMvcRequestBuilders.multipart(…)internally. Custom code building multipart requests directly should do the same. -
gru-spring-integration-testingnow publishes its auto-configuration viaMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3+). The legacyMETA-INF/spring.factoriesis kept for back-compatibility.
Grails module:
-
gru-grailsnow targets Apache Grails 7.1.1 (coordinates:org.apache.grails:grails-) instead of legacy Grails 6 (org.grails:grails-). The Servlet API moved with it:javax.servlet.http.Cookieis replaced withjakarta.servlet.http.Cookie. Consumers must be on Apache Grails 7.x. -
The module has been rewritten from Groovy to Java internally so the published jar contains no Groovy-version-specific bytecode and can safely sit on a Groovy 4 (Apache Grails 7) consumer classpath even though the rest of gru is built against Groovy 5. The user-facing DSL (
include,model,forward,executesextension methods on the gru test builders) is unchanged. -
Apache Grails 7’s
UrlMappingsHolder.matchAll(uri, method)no longer returns generic catch-all mappings (e.g./$controller/$action?/$id?).gru-grailsnow falls back tomatch(uri)and filters the result by HTTP method, preserving the previous"controller is not mocked"and"action does not exist"assertion paths when a testinclude`s a catch-all `UrlMappingsclass.
Testing helpers:
-
fixtbumped to1.0.0.RC5+; system properties now take precedence over environment variables when resolving the test resources folder (fixes the case whereGradle’s test.environment(…)was silently overriding an in-testSystem.setPropertyredirect). -
com.stehno.ersatz:ersatz:1.9.0is no longer used. Consumers extending the gru-okhttp test base (or wiring an Ersatz mock server themselves) should switch toio.github.cjstehno.ersatz:ersatz:4.xand, for the Groovy DSL, also depend onio.github.cjstehno.ersatz:ersatz-groovy:4.x. The ersatz 4.x DSL renames HTTP-method blocks from lowercase (get,post) to uppercase (GET,POST). -
Gradle 9’s
jvm-test-suiteplugin no longer launches JUnit Platform withoutorg.junit.platform:junit-platform-launcheron the test runtime classpath. Add it astestRuntimeOnlyto any project that runs Spock or JUnit 5 tests.
2.0.0
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
Httpclient is now part of the coregrupackage -
The original HTTP module has been renamed to
gru-okhttpand the client accordingly toOkHttp -
The libraries required to evaluate HTML responses has been moved to compile only dependencies
-
Micronaut module now uses
micronaut-http-clientinstead of the default HTTP one
New Features
-
New JDK based
Httpclient -
New Microaut HTTP Client based client
-
Small improvements in the Kotlin DSL
-
No need to specify the reference class when calling
Gru.create()orHttp.create(). The class is automatically detected using the stack walker API.
