{#==========================================
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 and foremost, Spincast testing is about testing using a Guice context. In some cases,
a regular JUnit unit test (with some mocking) may be more than enough.
But using a Guice context to run the tests is so easy and powerful that we
practically don't use any regular unit test in Spincast!
Here's a quick introduction to how Spincast testing works:
A Guice context is always created before running a test class.
Instead of using a mocking library
(such as Mockito or PowerMock),
we use a custom
An HTTP Server is very often started before running a test class.
Not all tests require an HTTP Server to be started, but we're not shy at all to start a Server before running a test class
which may benefit from it, even if the tests could run faster using another approach!
For example, to test a
By starting an HTTP Server before running your tests, you can test your
You may think that it is not very efficient to start a web Server simply to run some basic tests.
And sometimes it's not! But the fact is that starting a Server is in general very fast, at least when the default
Undertow Server is used. As you'll see, the
Spincast JUnit runner, and the provided test base classes, only start
the Server once for a given test class, not for each individual test: this helps in keeping the
tests fast.
The real application itself is often started before running a test class!
Not only do we often start an HTTP Server before running a test class,
but Spincast makes it very easy to run the tests against the real application itself. No
mock, no simulation: the real application.
Testing
Guice context to provide the mocked environment into which tests are run.
controller, we do
not create a mocked request to call the controller's methods directly. Instead, we
test the controller using a real request, on the
real HTTP Server the application runs on.
web API as easily
as you test simple methods. Also, your tests are then run in an environment which is as close as possible
to the production environment.
In other words, there are two types of Spincast testing :
integration tests, you might say. We'll
see an example of this testing approach in the Integration test example
section.
unit tests: they do not require an HTTP Server... But they are still
run using a Guice context! We'll see an example of this testing approach in the
Custom context test example section.
Of course, when you run some tests against a real application, you first have to mock
some parts. Tests must be without side effects! The classic example is a database : a test
database must be used when tests are run or otherwise it would be total craziness. Another example is
that you may want to disable some initialization tasks your application runs when it starts,
so the tests run faster. The Spincast testing utilities and pattern make this
"mocking" super easy, by allowing you to provide an overriding Guice module.
Your real application is started but you have the opportunity to tweak some of its bindings
before the tests are run. We'll see how in the next sections.
But, first, let's quickly look at how to install the Spincast testing utilities...
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>
If you really don't want any dependencies to be pulled from the spincast-default
artifact, and even if only the test scope is affected here, you can
use the spincast-testing-core artifact instead. Note that,
in that case, you won't have access to the SpincastDefaultGuiceModule module
to create a testing Guice context, though.
Also, it's good to know that the Spincast JUnit Runner, at which we'll
have a look in a next section, can easily be used outside a Spincast
application. It has its own artifact, org.spincast:spincast-testing-junit-runner:{{spincast.spincastCurrrentVersion}},
and has no dependency to any other Spincast artifacts.
Let's write an integration test from scratch, to see how Spincast testing utilities work!
For this example, we will use the Quick Start application and we will
write a test to validate one of its API methods. If you look at the
App
class from the Quick Start, you will see that a POST("/sum") Route is defined. This is the Route we're going to test.
This Route expects a POST
request with two post parameters:
40.
2.
Json object:
{"result":"42"}
Before we start writing the test class, let's have another look at the
App
class, the main class of the Quick Start application. Notice that the main(...) method
is not the one that actually creates the
Guice context and starts the application: it delegates that job to a createApp(...) method:
// The main method
public static void main(String[] args) {
createApp(args, null);
}
// The createApp method
public static Injector createApp(String[] args, Module overridingModule) {
if(args == null) {
args = new String[]{};
}
//==========================================
// Should we override the base app modules
// with an overring module?
//==========================================
Injector guice = null;
if(overridingModule != null) {
guice = Guice.createInjector(Modules.override(getAppModules(args))
.with(overridingModule));
} else {
guice = Guice.createInjector(getAppModules(args));
}
App app = guice.getInstance(App.class);
app.start();
return guice;
}
This indirection may seem trivial at first, but we will see that it is at the very root of the integration testing Spincast suggests.
We'll come back to this createApp(...) method in a moment but,
for now, notice that in addition to creating the Guice context and starting the application,
this method also accepts an overridingModule module and returns
the application's Guice injector. When the application starts in production, using its main(...)
method, no overridingModule module is used and the returned
Guice injector is simply ignored. But things will be different inside our integration test classes!
Let's now start writing the test class.
Let's call our test class "QuickStartSumTest", and this is going to be its skeleton:
public class QuickStartSumTest {
@Test
public void validRequest() throws Exception {
// TODO
}
// Other tests...
}
As you see, we are going to implement only one test in this example:
validRequest(). This test will validate that
a valid request returns a valid response. Of course, in real life, we should also test for
invalid requests, for edge cases scenarios, etc.
Now, how are we going to implement this test to validate a web API?
Without using integration testing, as Spincast allows, we may have to:
controller by:
request.
sumRoute(AppRequestContext context) method
of the controller directly.
Router too, to make sure that POST("/sum") requests
are routed correctly.
Again, this would probably require a lot of mocking.
front controller), doesn't
make a difference so our tests are in fact useless!
Wouldn't it be nicer it we could test all those parts together, as they will actually be run in production? Without mocking and hacking? Well, this is exactly the kind of integration testing that Spincast provides! You start your real application, as it would be started in production, and you run your tests against it... Let's see how!
First, let's make our test class extend
SpincastIntegrationTestBase,
which is the base class Spincast provides for integration testing :
public class QuickStartSumTest extends
SpincastIntegrationTestBase<AppRequestContext, AppWebsocketContext> {
@Override
protected Injector createInjector() {
// TODO
}
@Test
public void validRequest() throws Exception {
// TODO
}
// Other tests...
}
Explanation :
SpincastIntegrationTestBase and parameterizes it with
the Route Context type and WebSocket Context type that our application uses. We'll see later
that it's also possible to create a custom base class so
we don't have to use that kind of parameterized class all the time.
createInjector(). This is what we are going to do next...
The SpincastIntegrationTestBase only requires one thing
from us : that we provide the Guice injector to use to run the test class. So it is now time to come back to
the famous createApp(...) method from our Quick Start application! Do you
remember what this method does? It starts the application but it also returns the Guice injector.
So let's start our application and retrieve its Guice injector:
public class QuickStartSumTest extends
SpincastIntegrationTestBase<AppRequestContext, AppWebsocketContext> {
@Override
protected Injector createInjector() {
Module overridingModule = getDefaultOverridingModule(AppRequestContext.class,
AppWebsocketContext.class);
return App.createApp(null, overridingModule);
}
@Test
public void validRequest() throws Exception {
// TODO
}
// Other tests...
}
Explanation :
SpincastIntegrationTestBase base class). More details on how to write a custom
overriding module in a subsequent section.
main(...)
method, but createApp(...) directly. This is exactly like the application
is actually started in production, but it also allows us to add an overriding Guice module. Finally, the
createApp(...) method returns the application's Guice injector and we return it too so it can be
used to run the test class.
At this point, our test class is ready and we can start implementing the "validRequest()" test itself.
We'll soon start to see the benefits of the setup we just put into place!
In the test we're about to write, we'll need an instance of the
JsonManager component to
validate the Json response returned by the Server.
With the setup we put into place, it is very easy: we now have full access to the application's Guice context.
Also, dependencies are automatically injected into the
test class (thanks to the SpincastIntegrationTestBase base class). Therefore, to get
an instance of the JsonManager component, we simply inject it!
public class QuickStartSumTest extends
SpincastIntegrationTestBase<AppRequestContext, AppWebsocketContext> {
@Override
protected Injector createInjector() {
Module overridingModule = getDefaultOverridingModule(AppRequestContext.class,
AppWebsocketContext.class);
return App.createApp(null, overridingModule);
}
@Inject
private JsonManager jsonManager;
@Test
public void validRequest() throws Exception {
// TODO
}
// Other tests...
}
All right! Our real (but tweakable) application is started, and we can write our tests the exact same way we write the application itself: with full dependency injection and with a running HTTP Server so we can test the web API.
Let's now write the test...
The SpincastIntegrationTestBase
base class provides methods to easily use a Spincast HTTP Client
instance. We will use this client to create and send a request to the "/sum" Route that we want to test.
public class QuickStartSumTest extends
SpincastIntegrationTestBase<AppRequestContext, AppWebsocketContext> {
@Override
protected Injector createInjector() {
Module overridingModule = getDefaultOverridingModule(AppRequestContext.class,
AppWebsocketContext.class);
return App.createApp(null, overridingModule);
}
@Inject
private JsonManager jsonManager;
@Test
public void validRequest() throws Exception {
HttpResponse response = POST("/sum").addEntityFormDataValue("first", "40")
.addEntityFormDataValue("second", "2")
.addJsonAcceptHeader()
.send();
// TODO : the assertions
}
// Other tests...
}
Explanation :
POST(...) method
provided by the base class to start building our request. We target the
"/sum" Route. The application's host and port will be automatically
added.
"first" and "second". Those are the numbers we want the
server to compute the sum of.
Accept header to inform
the application that we wish to receive a Json response. This is not
always mandatory, it depends on your application.
HttpResponse object.
Let's now add some assertions to validate the response :
public class QuickStartSumTest extends
SpincastIntegrationTestBase<AppRequestContext, AppWebsocketContext> {
@Override
protected Injector createInjector() {
Module overridingModule = getDefaultOverridingModule(AppRequestContext.class,
AppWebsocketContext.class);
return App.createApp(null, overridingModule);
}
@Inject
private JsonManager jsonManager;
@Test
public void validRequest() throws Exception {
HttpResponse response = POST("/sum").addEntityFormDataValue("first", "40")
.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.create(content);
assertNotNull(resultObj);
assertEquals(new Integer(42), resultObj.getInteger("result"));
assertNull(resultObj.get("error", null));
}
// Other tests...
}
Explanation :
HTTP status.
content-type is Json.
Json Manager (injected
at lines 12-13) to create a JsonObject object from
the response's content.
Json
returned by the application, validations are very easy. We make sure that this object contains a "result"
field and that its value is really the sum of the two parameters we sent. We also validate that no
error occurred.
And that's it! Here's the final test class, with a few more test examples. This class is actually part of the Quick Start application source.
Instead of having to implement the "createInjector()" method in each test class we create, let's
create a custom base class:
public abstract class AppIntegrationTestBase extends
SpincastIntegrationTestBase<AppRequestContext, AppWebsocketContext> {
@Override
protected Injector createInjector() {
Module overridingModule = getDefaultOverridingModule(AppRequestContext.class,
AppWebsocketContext.class);
return App.createApp(null, overridingModule);
}
protected String[] getMainArgs() {
return null;
}
}
Now, our test classes simply have to extend this base class:
public class QuickStartSumTest extends AppIntegrationTestBase {
@Test
public void validRequest() throws Exception {
// ...
}
// Other tests...
}
Make sure you read the following section, Writing an overriding module for an example of a better custom base class!
Finally, note that if you use the Quick Start as a start for your application, a
base class has already been created for you:
AppIntegrationTestBase.
And here's an example of a test class using it:
QuickStartIntegrationTest
As we saw, we can provide an overriding module when we start the application.
This allows us to mock some components so our tests run without side effects. For example,
we could change an UserDatabase binding so it points to a
TestUserDatabase implementation instead of pointing to the real database.
An important thing to know is that this overriding module must contain a binding for
the Spincast Http Client with WebSocket support
since the SpincastIntegrationTestBase base class uses this client intensively.
If you use the default overriding
module (the one returned by getDefaultOverridingModule(...)), then this binding
is already done. But if you need to use a custom overriding module, you have to make sure you include it
otherwise you'll get a binding exception when you'll try to run your tests.
In fact, the recommended way of creating a custom overriding module is by always combining it with the default one :
public abstract class AppIntegrationTestBase extends
SpincastIntegrationTestBase<AppRequestContext, AppWebsocketContext> {
@Override
protected Injector createInjector() {
return App.createApp(getMainArgs(), getOverridingModule());
}
protected String[] getMainArgs() {
return null;
}
protected Module getOverridingModule() {
Module defaultOverridingModule =
getDefaultOverridingModule(AppRequestContext.class,
AppWebsocketContext.class);
Module appOverridingModule = new AbstractModule() {
@Override
protected void configure() {
// your tests bindings...
}
};
return Modules.combine(defaultOverridingModule, appOverridingModule);
}
}
Explanation :
getOverridingModule() method
listed below.
SpincastIntegrationTestBase base class.
In the previous example, we used integration testing: we started our real application
and we used its Guice context and HTTP Server to run our test. But for some tests, starting an
HTTP Server is simply overkill.
In those cases, the tests that we are going to write will be closer to "unit tests". But
those tests will still be a little bit different than
traditional unit tests : they are going to use a Guice context.
By doing so, those tests will be able to use dependency injection and "mocking"
some components will be very easy.
Let's see an example of this kind of tests by looking at a real Spincast test class :
JsonObjectsTest.java.
As you can see, this test class extends SpincastTestBase instead of SpincastIntegrationTestBase and
doesn't start any application. Instead, the test class creates the Guice injector/context by itself :
public class JsonObjectsTest extends SpincastTestBase {
@Override
protected Injector createInjector() {
return Guice.createInjector(new SpincastDefaultTestingModule());
}
//...
}
Explanation :
SpincastTestBase
instead of SpincastIntegrationTestBase.
App.createApp(...) method! Instead, we create the Guice context
by ourself. This specific test class uses
the provided SpincastDefaultTestingModule as the Guice module, but
you can of course provide a custom one.
Exactly as we were able to do in the Integration test example, we can
inject any dependency we need in this test class:
public class JsonObjectsTest extends SpincastTestBase {
@Inject
protected JsonManager jsonManager;
@Override
protected Injector createInjector() {
return Guice.createInjector(new SpincastDefaultTestingModule());
}
//...
@Test
public void fromJsonString() throws Exception {
String str = "{\"innerObj\":{\"someBoolean\":true,\"someInt\":123}," +
"\"anotherBoolean\":true,\"anotherInt\":44444}";
JsonObject jsonObj = this.jsonManager.create(str);
assertNotNull(jsonObj);
assertEquals(true, jsonObj.getBoolean("anotherBoolean"));
assertEquals(new Integer(44444), jsonObj.getInteger("anotherInt"));
JsonObject jsonObj2 = jsonObj.getJsonObject("innerObj");
assertNotNull(jsonObj2);
assertEquals(true, jsonObj2.getBoolean("someBoolean"));
assertEquals(new Integer(123), jsonObj2.getInteger("someInt"));
String jsonString = jsonObj.toJsonString();
assertEquals(str.length(), jsonString.length());
}
//...
}
Explanation :
Ant that's it: unit testing with a Guice context!
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 application is started when the test class is initialized and both its Guice context and its HTTP Server are 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.
SpincastTestBase base class or a class extending it
(like SpincastIntegrationTestBase), a createInjector() method
is called from the beforeClass() method. Your test class has to implement it
to create the Guice context.
SpincastTestBase base class or a class extending it,
the dependencies are then automatically injected into the instance of the test class.
All your @Inject annotated fields and methods are fulfilled.
beforeClass() method, the process stops and the
tests are not run.
SpincastTestBase base class or a class extending it,
note that a @FixMethodOrder(MethodSorters.NAME_ASCENDING) annotation is active. This tells JUnit that your
tests must be sorted alphanumerically before they are run. Without this annotation,
JUnit doesn't guarantee the order in which your tests will 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 between all the tests of a test class, you have to make sure you reset everything
that may be required before running a test. To do this, used JUnit's
@Before annotations.
If you use the SpincastTestBase base class or a class extending it, you can
also override the existing beforeTest() and afterTest() method without the
need for annotations.
Spincast JUnit runner provides those features:
beforeClass() and afterClass() methods will be called.
We highly recommend that you implement this interface
if you don't use a base class provided by Spincast because the beforeClass() method
is the only way to initialize the test class before
the tests are run. But for very simple tests, you may not need it.
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!
SpincastTestBase class
The SpincastTestBase class is the root of all Spincast testing base classes. As we saw in the Custom context test example section, it is also the class to extend if you want to run some tests using a Guice context but without starting your actual application.
Some information about the SpincastTestBase class :
@RunWith(SpincastJUnitRunner.class), so any test class extending
it will use the Spincast JUnit runner.
@FixMethodOrder(MethodSorters.NAME_ASCENDING), so all
tests are sorted alphanumerically, using the name of their method, before they are run.
createInjector() method that needs to be implemented by the
extending classes. In the beforeClass() method, this injector is used and all the
required dependencies are injected in the test class instance.
File getTestingWritableDir()
String createTestingFilePath(String relativePath)
relativePath to the path of the root temporary directory.
It is the job of the caller to create the actual file.
String createTestingFilePath()
Injector getInjector()
SpincastConfig getSpincastConfig()
SpincastIntegrationTestBase class
As we saw in the Integration test example, SpincastIntegrationTestBase is the class to extend to run integration tests using Spincast. It is used when you want to run tests against your real application.
Some information about the SpincastIntegrationTestBase class:
Server, the Router, and an
HTTP Client. So those dependencies are already available to the
extending classes, using the associated getters.
overriding module to use to create the Guice context. This module
is available using the getDefaultOverridingModule() method. If you have
to define a custom overriding module, it is recommended that you keep the bindings
the default module does, by
combining both modules.
GetRequestBuilder GET/POST/DELETE/...(String path)
path. The application's host and port will be automatically found.
String websocket(String path)
WebSocket request, using the relative
path. The application's host and port will be automatically found.
String createTestUrl(String path)
path, using the
Server's host and port.
SpincastTestBase and SpincastIntegrationTestBase are the most important base classes Spincast provides for testing, but there are some others. We will briefly introduce them here.
The SpincastNoAppIntegrationTestBase
class extends SpincastNoAppIntegrationTestBase. It can be used when you need a running HTTP Server and all the
HTTP utilities provided by the SpincastNoAppIntegrationTestBaseclass, but you don't want to start your application.
The Guice context is, in that case, created using a module you provide by implementing
the abstract getTestingModule() method.
The Server is automatically started in the beforeClass() method.
All Routes are deleted before each test is run.
The SpincastDefaultNoAppIntegrationTestBase
class extends SpincastNoAppIntegrationTestBase. It simply parameterizes it with the default Route Context type
and WebSocket Context type. You should probably not have to use this class if you have
custom Context types (which is recommended and already done if your application is based on
the Quick Start).