{#========================================== Docs : "Testing" ==========================================#}

Testing

Spincast provides some nice testing utilities. You obviously don't have to use those to test your Spincast application, you may already have your favorite testing toolbox and be happy with it. But those utilities are heavily used to test Spincast itself, and we think they are an easy, fun, and very solid testing foundation.

First, Spincast comes with a custom JUnit runner which allows testing using a Guice context really easily. But, the biggest feature is to be able to test your real application itself, without even changing the way it is bootstrapped. This is possible because of the Guice Tweaker component which allows to indirectly mock or extend some components.

{#========================================== Installation ==========================================#}

Installation

Add this Maven artifact to your project to get access to the Spincast testing utilities:

<dependency>
    <groupId>org.spincast</groupId>
    <artifactId>spincast-testing-default</artifactId>
    <version>{{spincast.spincastCurrrentVersion}}</version>
    <scope>test</scope>
</dependency>

Then, make your test classes extend SpincastTestBase or one of its children classes.

{#========================================== Testing demo ==========================================#}

Demo

In this demo, we're going to test a simple application which only has one route : "/sum". The Route Handler associated with this Route is going to receive two numbers, will add them up, and will return the result as a Json object. Here's the response we would be expecting from the "/sum" endpoint by sending the parameters "first" = "1" and "second" = "2" :

{
  "result": "3"
}

You can download that Sum application [.zip] if you want to try it by yourself or look at its code directly.

First, let's have a quick look at how the demo application is bootstrapped :

public class App {

    public static void main(String[] args) {
        Spincast.configure()
                .module(new AppModule())
                .init();
    }

    @Inject
    protected void init(DefaultRouter router,
                        AppController ctrl,
                        Server server) {

        router.POST("/sum").save(ctrl::sumRoute);
        server.start();
    }
}

The interesting lines to note here are 4-6 : we use the bootstrapper to start everything! We'll see that, without touching this bootstrapping, we'll still be able to modify the Guice context, to mock some components.

Let's write a first test class :


public class SumTest extends IntegrationTestAppDefaultContextsBase {

    @Override
    protected void initApp() {
        App.main(null);
    }

    @Inject
    private JsonManager jsonManager;

    @Test
    public void validRequest() throws Exception {
        // TODO...
    }
}

Explanation :

As you can see, simply by extending IntegrationTestAppDefaultContextsBase, and by starting our application using its main(...) method, we can write integration tests targeting our running application, and we can use any components from its Guice context.

Let's implement that first test. We're going to validate that the "/sum" endpoint of our application works properly :

@Test
public void validRequest() throws Exception {

    HttpResponse response = POST("/sum").addEntityFormDataValue("first", "1")
                                        .addEntityFormDataValue("second", "2")
                                        .addJsonAcceptHeader()
                                        .send();

    assertEquals(HttpStatus.SC_OK, response.getStatus());
    assertEquals(ContentTypeDefaults.JSON.getMainVariationWithUtf8Charset(),
                 response.getContentType());

    String content = response.getContentAsString();
    assertNotNull(content);

    JsonObject resultObj = this.jsonManager.fromString(content);
    assertNotNull(resultObj);

    assertEquals(new Integer(3), resultObj.getInteger("result"));
    assertNull(resultObj.getString("error", null));
}

Explanation :

Note that we could also have retrieved the content of the response as a JsonObject directly, by using response.getContentAsJsonObject() instead of response.getContentAsString(). But we wanted to demonstrate the use of an injected component, so bear with us!

If you look at the source of this demo, you'll see two more tests in that first test class : one that tests the endpoint when a parameter is missing, and one that tests the endpoint when the sum overflows the maximum Integer value.

Let's now write a second test class. In this one, we are going to show how easy it is to replace a binding, to mock a component.

Let's say we simply want to test that the responses returned by our application are gzipped. We may not care about the actual result of calling the "/sum" endpoint, so we are going to "mock" it. This is a simple example, but the process involved is similar if you need to mock a data source, for example.

Our second test class will look like this :


public class ResponseIsGzippedTest extends IntegrationTestAppDefaultContextsBase {

    @Override
    protected void initApp() {
        App.main(null);
    }

    public static class AppControllerTesting extends AppControllerDefault {

        @Override
        public void sumRoute(DefaultRequestContext context) {
            context.response().sendPlainText("42");
        }
    }

    @Override
    protected SpincastPluginThreadLocal createGuiceTweaker() {

        SpincastPluginThreadLocal guiceTweaker = super.createGuiceTweaker();

        guiceTweaker.module(new SpincastGuiceModuleBase() {

            @Override
            protected void configure() {
                bind(AppController.class).to(AppControllerTesting.class).in(Scopes.SINGLETON);
            }
        });

        return guiceTweaker;
    }

    @Test
    public void isGzipped() throws Exception {
        // TODO...
    }
}

Explanation :

And let's write the test itself :

@Test
public void isGzipped() throws Exception {
 
    HttpResponse response = POST("/sum").addEntityFormDataValue("toto", "titi")
                                        .addJsonAcceptHeader()
                                        .send();

    assertTrue(response.isGzipped());

    assertEquals(HttpStatus.SC_OK, response.getStatus());
    assertEquals(ContentTypeDefaults.TEXT.getMainVariationWithUtf8Charset(),
                 response.getContentType());
    assertEquals("42", response.getContentAsString());
        
}

Explanation :

Being able to change bindings like this is very powerful : you are testing your real application, as it is bootstrapped, without even changing its code. All is done indirectly, using the Guice Tweaker.

{#========================================== Guice Tweaker ==========================================#}

Guice Tweaker

As we saw in the previous demo, we can tweak the Guice context of our application in order to test it. This is done by configuring the GuiceTweaker.

The Guice Tweaker is in fact a plugin. This plugin is special because it is applied even if it's not registered during the bootstrapping of the application.

It's important to know that the Guice Tweaker only works if you are using the standard Bootstrapper. It is implemented using a ThreadLocal that the bootstrapper will look for.

The Guice Tweaker is created in the SpincastTestBase class. By extending this class or one of its children, you have access to it. To configure it, you override the createGuiceTweaker() method and modify it as you need. You can see an example of this in the previous demo.

By default, the Guice Tweaker automatically modifies the SpincastConfig binding of the application. This allows you to use testing configurations very easily (for example to make sure the server starts on a free port). The implementation class used for those configurations can be changed by overriding the getSpincastConfigTestingImplementation() method. The Guice tweaker will use this implementation for the binding. The default implementation is SpincastConfigTestingDefault. You can disable that automatic configurations tweaking by overriding the isEnableGuiceTweakerTestingConfigMecanism() method and making it return false.

For integration testing, when a test class extends IntegrationTestBase or one of its children, the Spincast HTTP Client with WebSockets plugin is also registered automatically by the Guice Tweaker. The features provided by this plugin are used intensively to perform requests.

Finally, the Guice Tweaker provides three main methods to help tweak the Guice context of your application :

{#========================================== Testing base classes ==========================================#}

Testing base classes

Multiple base classes are provided, depending on the needs of your test class. They all ultimately extend SpincastTestBase, they all use the Spincast JUnit runner and all give access to Guice Tweaker.

Those test base classes are split into two main categories : those made for integration testing and those made for unit testing. We use the expression "integration testing" when the HTTP Server is started to run the tests and "unit testing" otherwise.

Integration testing base classes :

Unit testing base classes :

{#========================================== Spincast JUnit runner ==========================================#}

Spincast JUnit runner

Spincast's testing base classes all use a custom JUnit runner: SpincastJUnitRunner.

This custom runner has a couple of differences as compared with the default JUnit runner, but the most important one is that instead of creating a new instance of the test class before each test, this runner only creates one instance.

This way of running the tests works very well when a Guice context is involved. The Guice context is created when the test class is initialized, and then this context is used to run all the tests of the class. If Integration testing is used, then the HTTP Server is started when the test class is initialized and it is used to run all the tests of the class.

Let's see in more details how the Spincast JUnit runner works :

Since the Guice context is shared by all the tests of a test class, you have to make sure you reset everything required before running a test. To do this, use JUnit's @Before annotation, or the beforeTest() and afterTest() method.

Spincast JUnit runner features

A quick note about the @Repeat annotation : this annotation should probably only be used for debugging purpose! A test should always be reproducible and should probably not have to be run multiple times. But this annotation, in association with the testFailure(...) method, can be a great help to debug a test which sometimes fails and you don't know why!