Search code examples
javajunitmicronautembedded-server

Micronaut Embedded Server vs localhost


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

}

Solution

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