March 8, 2017

Ratpacked: Override Registry Objects With Mocks In Integration Specifications

Testing a Ratpack application is not difficult. Ratpack has excellent support for writing unit and integration tests. When we create the fixture MainClassApplicationUnderTest we can override the method addImpositions to add mock objects to the application. We can add them using the ImpositionsSpec object. When the application starts with the test fixture the provided mock objects are used instead of the original objects. For a Groovy based Ratpack application we can do the same thing when we create the fixture GroovyRatpackMainApplicationUnderTest.

We start with a simple Java Ratpack application. The application adds an implementation of a NumberService interface to the registry. A handler uses this implementation for rendering some output.

// File: src/main/java/mrhaki/ratpack/RatpackApplication.java
package mrhaki.ratpack;

import ratpack.registry.Registry;
import ratpack.server.RatpackServer;

public class RatpackApplication {
    public static void main(String[] args) throws Exception {
        RatpackServer.start(server -> server
                // Add a implementation of NumberService interface
                // to the registry, so it can be used by the handler.
                .registry(Registry.single(NumberService.class, new NumberGenerator()))
                
                // Register a simple handler to get the implementation
                // of the NumberService interface, invoke the give() method
                // and render the value.
                .handler(registry -> ctx -> ctx
                        .get(NumberService.class).give()
                        .then(number -> ctx.render(String.format("The answer is: %d", number)))));
    }
}

The NumberService interface is not difficult:

// File: src/main/java/mrhaki/ratpack/NumberService.java
package mrhaki.ratpack;

import ratpack.exec.Promise;

public interface NumberService {
    Promise<Integer> give();
}

The implementation of the NumberService interface returns a random number:

// File: src/main/java/mrhaki/ratpack/NumberGenerator.java
package mrhaki.ratpack;

import ratpack.exec.Promise;

import java.util.Random;

public class NumberGenerator implements NumberService {
    @Override
    public Promise<Integer> give() {
        return Promise.sync(() -> new Random().nextInt());
    }
}

To test the application we want to use a mock for the NumberService interface. In the following specification we override the addImpositions method of the MainClassApplicationUnderTest class:

// File: src/test/groovy/mrhaki/ratpack/RatpackApplicationSpec.groovy
package mrhaki.ratpack

import ratpack.exec.Promise
import ratpack.impose.ImpositionsSpec
import ratpack.impose.UserRegistryImposition
import ratpack.registry.Registry
import ratpack.test.MainClassApplicationUnderTest
import ratpack.test.http.TestHttpClient
import spock.lang.AutoCleanup
import spock.lang.Specification

/**
 * Integration test for the application.
 */
class RatpackApplicationSpec extends Specification {
    
    /**
     * Mock implementation for the {@link NumberService}.
     */
    private final NumberService mockNumberService = Mock()

    /**
     * Setup {@link RatpackApplication} for testing and provide
     * the {@link #mockNumberService} instance to the Ratpack registry.
     */
    @AutoCleanup
    private aut = new MainClassApplicationUnderTest(RatpackApplication) {
        @Override
        protected void addImpositions(final ImpositionsSpec impositions) {
            // Set implementation of NumberService interface to
            // our mock implementation for the test.
            impositions.add(
                    UserRegistryImposition.of(
                            Registry.single(NumberService, mockNumberService)))
        }
    }

    /**
     * Use HTTP to test our application.
     */
    private TestHttpClient httpClient = aut.httpClient
    
    void 'render output with number'() {
        when:
        final response = httpClient.get()
        
        then:
        // Our mock should get invoked once and we return 
        // the fixed value 42 wrapped in a Promise.
        1 * mockNumberService.give() >> Promise.sync { 42 }

        and:
        response.statusCode == 200
        response.body.text == 'The answer is: 42'
    }
}

Written with Ratpack 1.4.5.