All Downloads are FREE. Search and download functionalities are using the official Maven repository.

templates.docs.testing.html Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
{#==========================================
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.

Most of the time, you'll want to extend AppBasedTestingBase, or AppBasedDefaultContextTypesTestingBase if your application uses the default request context types.

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

Demo

In this demo, we're going to test a simple application which only has one endpoint : "/sum". The Route Handler associated with this endpoint 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 when 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(args);
    }

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

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

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

Let's write a first test class :


public class SumTest extends AppBasedDefaultContextTypesTestingBase {

    @Override
    protected void callAppMainMethod() {
        App.main(null);
    }
    
    @Override
    protected AppTestingConfigs getAppTestingConfigs() {
        return new AppTestingConfigs() {

            @Override
            public boolean isBindAppClass() {
                return true;
            }

            @Override
            public Class<? extends SpincastConfig> getSpincastConfigTestingImplementationClass() {
                return SpincastConfigTestingDefault.class;
            }

            @Override
            public Class<?> getAppConfigTestingImplementationClass() {
                return null;
            }

            @Override
            public Class<?> getAppConfigInterface() {
                return null;
            }
        };
    }

    @Inject
    private JsonManager jsonManager;

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

Explanation :

  • 2 : Our test class extends AppBasedDefaultContextTypesTestingBase. This class is a child of SpincastTestBase and therefore allows us to use all the tools Spincast testing provides. Note there are other base classes you can extend, we're going to look at them soon.
  • 4-7 : The base class we are using requires that we implement the callAppMainMethod() method. In this method we have to initialize the application to test. This is easily done by calling its main(...) method.
  • 9-33 : We also have to implement the getAppTestingConfigs() method. This is to provide Spincast informations about the configurations we want to use when running this test class. Have a look at the Testing configurations section for more information!
  • 35-36 : Here we can see Spincast testing in action! Our test class has now full access to the Guice context of the application. Therefore, we can inject any component we need. In this test class, we are going to use the JsonManager.
  • 38-41 : a first test to implement.

As you can see, simply by extending AppBasedDefaultContextTypesTestingBase, 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. There is some boilerplate code to write though (you nee to implement the getAppTestingConfigs() method, for example), and this why you would in general create a base class to serve as a parent for all your test classes!

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

   //...

    @Test
    public void validRequest() throws Exception {
    
        HttpResponse response = POST("/sum").addFormBodyFieldValue("first", "1")
                                            .addFormBodyFieldValue("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 :

  • 6-9 : the Spincast HTTP Client plugin is fully integrated into Spincast testing utilities. This allows us to very easily send requests to test our application. We don't even have to configure the host and port to use : Spincast will automatically find and use those of our application.
  • 11-13 : we validate that the response is a success ("200") and that the content-type is the expected "application/json".
  • 15-16 : we get the content of the response as a String and we validate that it is not null.
  • 18-19 : we use the JsonManager (injected previously) to convert the content to a JsonObject.
  • 21-22 : we finally validate the result of the sum and that no error occured.

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 AppBasedDefaultContextTypesTestingBase {

    @Override
    protected void callAppMainMethod() {
        App.main(null);
    }
    
    @Override
    protected AppTestingConfigs getAppTestingConfigs() {
        return new AppTestingConfigs() {

            @Override
            public boolean isBindAppClass() {
                return true;
            }

            @Override
            public Class<? extends SpincastConfig> getSpincastConfigTestingImplementationClass() {
                return SpincastConfigTestingDefault.class;
            }

            @Override
            public Class<?> getAppConfigTestingImplementationClass() {
                return null;
            }

            @Override
            public Class<?> getAppConfigInterface() {
                return null;
            }
        };
    }

    public static class AppControllerTesting extends AppControllerDefault {

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

    @Override
    protected Module getExtraOverridingModule() {
        return new SpincastGuiceModuleBase() {

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

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

Explanation :

  • 2 : this test class also extends AppBasedDefaultContextTypesTestingBase.
  • 4-7 : we start our application.
  • 9-33 : if we had created a base class for our tests, we could have define the getAppTestingConfigs() there instead of having to repeat it in all test files!
  • 35-41 : we create a mock controller by extending the original one and replacing the sumRoute(...) Route Handler so it always returns "42".
  • 43-52 : We specify an overriding module to change the implementation that will be used for the AppController binding. Under the hood, this is done by the Guice Tweaker.

And let's write the test itself :

    //...

    @Test
    public void isGzipped() throws Exception {
     
        HttpResponse response = POST("/sum").addFormBodyFieldValue("toto", "titi")
                                            .addJsonAcceptHeader()
                                            .send();
    
        assertTrue(response.isGzipped());
    
        assertEquals(HttpStatus.SC_OK, response.getStatus());
        assertEquals(ContentTypeDefaults.TEXT.getMainVariationWithUtf8Charset(),
                     response.getContentType());
        assertEquals("42", response.getContentAsString());  
    }

Explanation :

  • 6-8 : We can send pretty much anything here as the parameters since the controller is mocked : they won't be validated.
  • 10 : We validate that the response was gzipped.
  • 12-15 : just to make sure our tweaking is working properly.

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 the GuiceTweaker, a component which is part of the Spincast testing machanism.

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.

By default, the Guice Tweaker automatically modifies the SpincastConfig binding of the application when tests are run. 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 are specified in the getAppTestingConfigs() method you have to implement. The Guice tweaker will use those informations and will create the required binding automatically. The default implementation for the SpincastConfig interface is SpincastConfigTestingDefault.

Those are the methods available, in a test file, to tweak your application :

  • getAppTestingConfigs(...) : a section dedicated to this method follows next.

  • getExtraOverridingModule(...) : to make the Guice Tweaker add an extra module to the created Guice context.
  • getExtraPlugins(...) : to make the Guice Tweaker add extra plugins to the created Guice context.
  • addExtraSystemProperties(...) : to add extra System properties before running the tests.

{#========================================== Testing configurations ==========================================#}

The testing configurations (getAppTestingConfigs())

When running integration tests, you don't want to use the same configurations than the ones you would when running the application directly. For example, you may want to provide a different connection string to use a mocked database instead of the real one.

As we saw in the previous section, the Guice Tweaker allows you to change some bindings when testing your application. But configurations is such an important component to modify, when running tests, that Spincast forces you to specify which implementations to use for those!

You specify the testing configurations by implementing the getAppTestingConfigs() method. This method must return an instance of AppTestingConfigs. This object tells Spincast :

  • getSpincastConfigTestingImplementationClass() : The implementation class to use for the SpincastConfig binding. In other words, this hook allows you to easily mock the configurations used by Spincast core components. The default testing implementation is the provided SpincastConfigTestingDefault class. You can use this provided class directly, or at least you may want to have a look at it when writing your own since it shows how to implement some useful things, such as finding a free port to use when starting the HTTP server during tests.
  • getAppConfigInterface() : The interface of your custom app configurations class. You can return null if you don't have a custom configurations class.
  • getAppConfigTestingImplementationClass() : The implementation class to use for your custom app configurations. You can return null if you don't have a custom configurations class.
  • isBindAppClass() : Should the App class itself (the class in which Spincast.init() or Spincast.configure() is called) be bound? In general, if you are running unit tests and don't need to start any HTTP server, you are going to return false... That way, your main class (in general named "App") won't be bound and therefore won't start the server.

Spincast will use the informations returned by this object and will add all the required bindings automatically. You don't need to do anything by yourself, for example by using the Guice Tweaker, to change the bindings for the configurations when running integration tests. You just need to implement the getAppTestingConfigs() method.

In most applications, the testing implementation to use for the SpincastConfig interface and the one for your custom configurations interface will be the same! Indeed, if you follow the suggested way of configuring your application, then your custom configurations interface AppConfig extends SpincastConfig.

In that case, Spincast will automatically intercept calls to methods made on the AppConfig instance, but that are defined in the parent SpincastConfig interface, and will route them to the SpincastConfig testing implementation (as returned by getSpincastConfigTestingImplementationClass(). Doing so, you can specify a config in the testing implementation (the HTTPS port to use, for example), and that config will be used in your app.

Note that you can annotate a method with @DontIntercept if you don't want it to be intercepted.

Your testing configurations can often be shared between multiple tests classes. It is therefore a good idea to create an abstract base class, named "AppTestingsBase" or something similar, to implement the getAppTestingConfigs() method there, and use this base class as the parent for all your integration test classes. Have a look at this base class for an example.

While mocking some configurations is often required, it's still a good idea to make testing configurations as close as possible as the ones that are going to be used in production. For example, returning false for the isDevelopmentMode() method is suggested. That way, you can be confident that once your tests pass, your application will do well in production.

You can mock some Environment Variables used as configurations, by overriding the getEnvironmentVariables() method in your configurations implementation class.

{#========================================== 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 based on your actual application and those that are not. Most of the time, you do want to test using the Guice context of your application! But you may sometimes have components that can be unit tested without the full Guice context of your application.

Those are the main testing base classes provided by Spincast. All of them can be modify using the Guice Tweaker :

App based

Not based on an app

  • NoAppTestingBase : base class to use to test components using the default Guice context (the default plugins only). No application class is involved.
  • NoAppStartHttpServerTestingBase : as NoAppTestingBase, but if you also need the HTTP server to be started! This base class will be responsible to start and stop the server.

{#========================================== 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 :

  • First, a beforeClass() method is called. As opposed to a classic JUnit's @BeforeClass annotated method, Spincast's beforeClass() method is not static. It is called when the test class is initialized.
  • The createInjector() method is called in the beforeClass() method. This is where the Guice context will be created, by starting an application or explictly.
  • The dependencies are automatically injected from the Guice context into the instance of the test class. All your @Inject annotated fields and methods are fulfilled.
  • If an exception occures during the execution of the beforeClass() method, the beforeClassException(...) method will be called, the process will be stop and the tests won't be run.
  • If no exception occures, the tests are then run.
  • The afterClass() method is called. Like the beforeClass() method, this method is not static. Note that the afterClass() method won't be called if an exception occurred in the beforeClass() method.

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

  • If your test class is annotated with @ExpectingBeforeClassException then the beforeClass() method is expected to throw an exception! In other words, the test class will be shown by JUnit as a "success" only of the beforeClass() method throws an exception. This is useful, in integration testing, to validate that your application refuses some invalid configuration when it starts, for example.
  • If your test class (or a parent) implements TestFailureListener then the testFailure(...) method will be called each time a test fails. This allows you to add a breakpoint or some logs, and to inspect the context of the failure.
  • You can use the @Repeat annotation. When added to the class itself, this annotation makes your test class loop X number of times. Note that the beforeClass() and afterClass() methods will also be called X number of time, so the Guice context will be recreated each time. You can specify an amount of milliseconds to sleep between two loops, using the sleep parameter.
  • You can also use the @Repeat annotation on a test. This will make the test run X number of time during the execution of a test class loop.
  • If your test class (or a parent) implements RepeatedClassAfterMethodProvider then the afterClassLoops() method will be called when all the loops of the test class have been run.

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!

{#========================================== Managing cookies ==========================================#}

Managing cookies

A frequent need during integration testing is to be able to keep cookies across multiple requests... By doing so, the behavior of a real browser is simulated.

To keep the cookies sent by a response, simple call saveResponseCookies(response) when a valid response is received. Then, you can add back those cookies to a new request using .setCookies(getPreviousResponseCookies()):

// First request, we save the cookies from the response...
HttpResponse response = GET("/one").send();
assertEquals(HttpStatus.SC_OK, response.getStatus());
saveResponseCookies(response);

// Second request, we resend the cookies!
response = GET("/two").setCookies(getPreviousResponseCookies()).send();
assertEquals(HttpStatus.SC_OK, response.getStatus());
saveResponseCookies(response);

{#========================================== Spincast Testing databases ==========================================#}

Testings/embedded databases

Two embedded databases are provided for testing: H2 and a PostgreSQL.

Those two allow you to run your tests on a real but ephemeral database.

Use the H2 database if your queries are simple and you want fast tests. Use PostgreSQL when you need "the real thing", even if it's slower to bootstrap.

H2

H2 is a very fast database to use to run tests. Its drawback is that is may not always support all real-world kind of queries, but other than that it does its job very well...

You enable the Spincast Testing H2 database simply by binding SpincastTestingH2 as a Provider for your DataSource:

@Override
protected Module getExtraOverridingModule() {
    return Modules.override(super.getExtraOverridingModule()).with(new SpincastGuiceModuleBase() {

        @Override
        protected void configure() {
            bind(DataSource.class).toProvider(SpincastTestingH2.class).in(Scopes.SINGLETON);
            // ...
        }
    });
}

You then inject SpincastTestingH2 and the DataSource in your test file (or a base class):

@Inject
protected SpincastTestingH2 spincastTestingH2;

@Inject
private DataSource testDataSource;

In beforeClass(), you can make sure the database starts in a clean state:

@Override
public void beforeClass() {
    super.beforeClass();
    spincastTestingH2.clearDatabase();
}

... you can also do this before each test, if required:

@Override
public void beforeTest() {
    super.beforeTest();
    spincastTestingH2.clearDatabase();
}

When the tests are over, you stop the server:

@Override
public void afterClass() {
    super.afterClass();
    spincastTestingH2.stopServer();
}

The way the H2 server is started, you are able to connect to your database using an external tool. For example, you can set a breakpoint and open the database using DBeaver (or another tool) using the proper connection string ("jdbc:h2:tcp://localhost:9092/mem:test;MODE=PostgreSQL;DATABASE_TO_UPPER=false" for example). This allows you to easily debug your tests.

You can change some configurations used by Spincast Testing H2 (the server port for example) by binding a custom implementation of the SpincastTestingH2Config interface. If you don't, the default configurations will be used.

PostgreSQL

The PostgreSQL (or simply Postgres) database provided by Spincast is based on otj-pg-embedded. It is a standalone version of PostgreSQL (no installation required) that can be used to run your tests. It is slower to start than H2 to run a tests file, but it is a real PostgreSQL database, so you can run any real-world SQL on it!

You enable it simply by binding SpincastTestingPostgres as a Provider for your DataSource:

@Override
protected Module getExtraOverridingModule() {
    return Modules.override(super.getExtraOverridingModule()).with(new SpincastGuiceModuleBase() {

        @Override
        protected void configure() {
            bind(DataSource.class).toProvider(SpincastTestingPostgres.class).in(Scopes.SINGLETON);
            // ...
        }
    });
}

You then inject SpincastTestingPostgres and the DataSource in your test file (or a base class):

@Inject
protected SpincastTestingPostgres spincastTestingPostgres;

@Inject
private DataSource testDataSource;

The standalone Postgres database will then be started automatically when your tests are started.

When the tests are over, you can stop Postgres:

@Override
public void afterClass() {
    super.afterClass();
    spincastTestingPostgres.stopPostgres();
}

You can change some configurations used the database by binding a custom implementation of the SpincastTestingPostgresConfig interface. If you don't, the default configurations will be used.





© 2015 - 2024 Weber Informatics LLC | Privacy Policy