I'm getting started with micronaut and I would like to understand the difference between testing the controller using local host and using an Embedded server
For example I have a simple controller
@Controller("/hello")
public class HelloController {
@Get("/test")
@Produces(MediaType.TEXT_PLAIN)
public String index() {
return "Hello World";
}
}
and the tested class
@MicronautTest
public class HelloControllerTest {
@Inject
@Client("/hello")
RxHttpClient helloClient;
@Test
public void testHello() {
HttpRequest<String> request = HttpRequest.GET("/test");
String body = helloClient.toBlocking().retrieve(request);
assertNotNull(body);
assertEquals("Hello World", body);
}
}
I got the logs:
14:32:54.382 [nioEventLoopGroup-1-3] DEBUG mylogger - Sending HTTP Request: GET /hello/test
14:32:54.382 [nioEventLoopGroup-1-3] DEBUG mylogger - Chosen Server: localhost(51995)
But then, in which cases we need an Embedded Server? why? where I can find documentation to understand it. I read the documentation from Micronaut but is not clear for me, what is actually occurring and why? like this example:
@Test
public void testIndex() throws Exception {
EmbeddedServer server = ApplicationContext.run(EmbeddedServer.class);
RxHttpClient client = server.getApplicationContext().createBean(RxHttpClient.class, server.getURL());
assertEquals(HttpStatus.OK, client.toBlocking().exchange("/hello/status").status());
server.stop();
}
In both cases, you are using EmbeddedServer
implementation - NettyHttpServer
. This is an abstraction that represents Micronaut server implementation (a NettyHttpServer
in this case).
The main difference is that micronaut-test
provides components and annotations that make writing Micronaut HTTP unit tests much simpler. Before micronaut-test
, you had to start up your application manually with:
EmbeddedServer server = ApplicationContext.run(EmbeddedServer)
Then you had to prepare an HTTP client, for instance:
HttpClient http = HttpClient.create(server.URL)
The micronaut-test
simplifies it to adding @MicronautTest
annotation over the test class, and the runner starts the embedded server and initializes all beans you can inject. Just like you do with injecting RxHttpClient
in your example.
The second thing worth mentioning is that the @MicronautTest
annotation also allows you to use @MockBean
annotation to override existing bean with some mock you can define at the test level. By default, @MicronautTest
does not mock any beans, so the application that starts reflect 1:1 application's runtime environment. The same thing happens when you start EmbeddedServer
manually - this is just a programmatic way of starting a regular Micronaut application.
So the conclusion is quite simple - if you want to write less boilerplate code in your test classes, use micronaut-test
with all its annotations to make your tests simpler. Without it, you will have to manually control all things (starting Micronaut application, retrieving beans from application context instead of using @Inject
annotation, and so on.)
Last but not least, here is the same test written without micronaut-test
:
package com.github.wololock.micronaut.products
import io.micronaut.context.ApplicationContext
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.RxHttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification
class ProductControllerSpec extends Specification {
@Shared
@AutoCleanup
EmbeddedServer server = ApplicationContext.run(EmbeddedServer)
@Shared
@AutoCleanup
HttpClient http = server.applicationContext.createBean(RxHttpClient, server.URL)
def "should return PROD-001"() {
when:
Product product = http.toBlocking().retrieve(HttpRequest.GET("/product/PROD-001"), Product)
then:
product.id == 'PROD-001'
and:
product.name == 'Micronaut in Action'
and:
product.price == 29.99
}
def "should support 404 response"() {
when:
http.toBlocking().exchange(HttpRequest.GET("/product/PROD-009"))
then:
def e = thrown HttpClientResponseException
e.status == HttpStatus.NOT_FOUND
}
}
In this case, we can't use @Inject
annotation and the only way to create/inject beans is to use applicationContext
object directly. (Keep in mind that in this case, RxHttpClient
bean does not exist in the context and we have to create it - in micronaut-test
case this bean is prepared for us upfront.)
And here is the same test that uses micronaut-test
to make the test much simpler:
package com.github.wololock.micronaut.products
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.test.annotation.MicronautTest
import spock.lang.Specification
import javax.inject.Inject
@MicronautTest
class ProductControllerSpec extends Specification {
@Inject
@Client("/")
HttpClient http
def "should return PROD-001"() {
when:
Product product = http.toBlocking().retrieve(HttpRequest.GET("/product/PROD-001"), Product)
then:
product.id == 'PROD-001'
and:
product.name == 'Micronaut in Action'
and:
product.price == 29.99
}
def "should support 404 response"() {
when:
http.toBlocking().exchange(HttpRequest.GET("/product/PROD-009"))
then:
def e = thrown HttpClientResponseException
e.status == HttpStatus.NOT_FOUND
}
}
Less boilerplate code, and the same effect. We could even @Inject EmbeddedServer embeddedServer
if would like to access it, but there is no need to do so.