2

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();

}
marhg
  • 659
  • 1
  • 17
  • 30

1 Answers1

4

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.

Szymon Stepniak
  • 40,216
  • 10
  • 104
  • 131