{#==========================================
Docs : "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.
Add this Maven artifact to your project to get access to the Spincast testing utilities:
Testing
Installation
<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 ==========================================#}
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").save(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 IntegrationTestAppDefaultContextsBase {
@Override
protected void initApp() {
App.main(null);
}
@Override
protected AppTestingConfigInfo getAppTestingConfigInfo() {
return new AppTestingConfigInfo() {
@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 :
initApp() method. In this method we have to start the application to test. This is
easily done by calling its main(...) method.
getAppTestingConfigInfo()
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!
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. There is some boilerplate
code to write though (to implement the getAppTestingConfigInfo() 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").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 :
"200") and that the content-type is the expected
"application/json".
null.
JsonManager
(injected previously) to convert the content to a JsonObject.
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 :
sumRoute(...) Route Handler
so it always returns "42".
createGuiceTweaker()
method to add a custom Guice module to the Guice Tweaker . As we will see in the next section,
the Guice Tweaker allows us to modify the Guice context of our application, without touching its code directly.
Here, we change the AppController binding so it uses
our mock controller implementation instead of the default one.
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.
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 :
When running integration tests, you don't want to use the same configurations then 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 getAppTestingConfigInfo() method. This method must return an instance of AppTestingConfigInfo. This object tells Spincast :
SpincastConfig binding. In other words, this hook allows you to easily mock the
configurations used by Spincast core components. Note that you can use the provided
SpincastConfigTestingDefault
class here... By returning it directly, or by using it as a parent for a custom implementation.
This class has some useful defaults, such as finding
a free port to use when starting the HTTP server.
null if you don't have a custom configurations class.
null if you don't have a custom configurations class.
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
getAppTestingConfigInfo() 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.
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 getAppTestingConfigInfo() 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
isDebugEnabled()
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.
You can modify System Properties before running your tests, by overriding the
getExtraSystemProperties()
method from the testing base class. Those system properties will be automatically reset once the tests are done.
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 :
main(...) method.
This class needs to be parameterized with the Request Context and WebSocket Context types
to use.
Request Context and WebSocket Context types. There is
no need to parameterize this class.
main(...) method. In that case, the base class
will start the HTTP Server by itself. Also, all routes are going to be cleared before each test.
This class needs to be parameterized with the Request Context and WebSocket Context types
to use.
Request Context and WebSocket Context types. There is
no need to parameterize this class.
Request Context and WebSocket Context types
to use.
Request Context and WebSocket Context types. There is
no need to parameterize this class.
Unit testing base classes :
getExtraOverridingModule() method). Using this class requires you to
specify the Request Context and WebSocket Context types to use.
Request Context and WebSocket Context types.
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 :
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.
createInjector() method is called in the beforeClass() method. This is where
the Guice context will be created, by starting an application or explictly.
@Inject annotated fields and methods are fulfilled.
beforeClass() method,
the beforeClassException(...) method will be called, the process will be stop and the tests won't be run.
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
annotation is used on the SpincastTestBase parent class, the
tests will be sorted alphanumerically before they are run. Without this annotation,
JUnit doesn't guarantee the order in which your tests are run.
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.
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.
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.
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.
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!