In this part, I am going to write some mock tests and run them with JsTestDriver.
Here is the plan:
|
Part1
|
App written completely in JSF and totally server-side
|
Source code
|
Article
|
Article (Turkish)
|
|
Part2
|
Converting to a Javascript client, using Servlets for web services
|
Source code
|
Article
|
Article (Turkish)
|
|
Part3
|
Optimizing the Javascript code for unit tests, using REST (used Apache CXF) for web
services
|
Source code
|
Article
|
|
Part4
|
Writing mock tests and running them with JsTestDriver
|
Source code
|
Article
|
|
Part5
|
Writing view tests |
Source code
|
Article
|
|
Part6
|
Using local storage for caching the wordbook on modern browsers |
Source code
|
|
Part7
|
Using WebSqlDatabase for caching the wordbook on modern browsers |
Source code
|
|
Part8
|
Using offline cache for resources, making the application support running offline |
Source code
|
|
Part9
|
Embedding the application in a Android WebView |
|
Part10
|
Bundling the application with PhoneGap |
|
Part11
|
Preparing for production and distribution |
|
Part12
|
Integrating the JS unit tests with continuous build systems |
|
Part13
|
Measuring Javascript code coverage with JsTestDriver |
The Javascript code from the previous part was optimized for testing and now I am gonna write some tests.
First I want to mention JsTestDriver. It is a Javascript test runner which has a unusual approach. You can read more from their website, but it basically:
- Creates a testing server which sends testing code to browsers and receives the test results sent from browsers
- Uses slave browsers to run tests on
So, we’re really running our Javascript tests on real browsers. Since you can bind multiple browsers, you are able to test your JS code in multiple browsers and multiple browser versions.
This approach allows us to test the Javascript in a continuous integration manner with having slave browsers bound to the testing server and sending the test codes from the testing server. What we could also use is Rhino. With Rhino engine, we would be able to test our Javascript in a headless environment (to simplify: without real browsers), but then our application would be only tested for Mozilla (not sure how up-to-date Rhino is). So this is an advantage of JsTestDriver, to be able to have your code tested on multiple “real” browsers; on the other hand it is a disadvantage because it is hard to integrate JsTestDriver into continuous build systems. I am gonna explain how to do the integration in Part 12.
In this project, I used the Maven plugin for JsTestDriver which opens a server, binds multiple browsers to it, makes the server send the test codes to browsers and make the server receive the results. If some tests are failed, then so does the Maven build.
I will explain how to integrate it later on this article.
For mocking, I used Jack. With Jack we can create mock objects and we can verify the behavior. Best is explaining with some code.
First let’s remember some bits of the AppController:
artikelApp.AppController = function(appView, wordManager) {
/** @private */
var init = function() {
...
};
/** @public */
this.start = function(){
appView.registerPageCreateHandler(init);
};
};
So, here let’s say we want to verify the behavior of the “start” method which is called in the Html page (not shown here).
What “start” method does is pretty simple, it calls “registerPageCreateHandler” method of appView with method “init” passed as a parameter.
And here is the test for this part:
var jc; //the Jack Context
var appController;
var appView;
AppControllerTest = TestCase("AppControllerTest");
AppControllerTest.prototype.setUp = function() {
jc = createJackContext();
appView = jc.create('appView', [
'registerPageCreateHandler',
...
]);
...
appController = new artikelApp.AppController(appView, wordManager);
...
};
AppControllerTest.prototype.testStartShouldRegisterPageCreateHandlerOnView = function() {
jc(function() {
jc.expect("appView.registerPageCreateHandler")
.once()
.withArguments(appController.init);
appController.start();
});
};
- First, I defined a Jack Context (“jc”) global variable, which is initialized in the “setUp” method of the testcase. JsTestDriver calls “setUp” method before running each test method, thus we have a new Jack context in each test method.
- In “setUp”, which is called before every test method, I created a brand new “appView” object and passed that to a brand new “appController”. As you can see, “appView” is a mock object and I defined the mock methods by their name.
- On test method (“testStartShouldRegisterPageCreateHandlerOnView”), in our Jack context (which is recreated on each test method
) I defined some expectations which are pretty self explanatory. Then I called the method I want to test. After this call, Jack context is ended and Jack verified the expectations.
One problem I didn’t mention is, “init” method of “appController” is private (not global), thus Jack or no other code in this test can’t access it. So, I applied a workaround, which I don’t like (any improvement is really appreciated):
artikelApp.AppController = function(appView, wordManager) {
var instance = this;
....
this._makeAllPublicForTests = function(){
instance.init = init;
...
};
};
Now, when “makeAllPublicForTests” is called, the method “init” is accessible from outside, and our test can access it.
AppControllerTest.prototype.setUp = function() {
...
appController._makeAllPublicForTests();
};
It is a little bit ugly, but OK.
Here is a more complex code and its test:
artikelApp.AppController = function(appView, wordManager) {
var instance = this;
var currentTranslation = null;
var currentArticle = null;
var score = 0;
...
/** @private */
var answerDas = function() {
answer('das');
};
/** @private */
var answer = function(article) {
var correctAnswer = (article === currentArticle);
if (correctAnswer)
score++;
else
score--;
appView.setArticleColor(correctAnswer);
appView.setScore(score);
appView.showResult();
};
/**
* Should only be used for tests
* @public
*/
this._makeAllPublicForTests = function(){
...
instance.answerDas = answerDas;
instance.answer = answer;
};
/** ... */
this._setScore = function(newScore){
score = newScore;
};
/** ... */
this._setCurrentArticle = function(newArticle){
currentArticle = newArticle;
};
/** ... */
this._setCurrentTranslation = function(newTranslation){
currentTranslation = newTranslation;
};
/** ... */
this._getScore = function(){
return score;
};
...
};
The behaviors we need to verify are:
- Increasing the score if the answer is correct
- Setting the article color based on the result if the answer is correct
- Setting new score on the view
- Showing result on view
...
AppControllerTest.prototype.setUp = function() {
jc = createJackContext();
appView = jc.create('appView', [
...
'registerAnswerDasButtonHandler',
'setWord',
'setTranslation',
'setArticle',
'setScore',
...
'showResult',
'setArticleColor',
... ]);
...
appController = new artikelApp.AppController(appView, wordManager);
appController._makeAllPublicForTests();
};
...
AppControllerTest.prototype.testWrongAnswerShouldBeHandled = function() {
jc(function() {
appController._setScore(10);
appController._setCurrentArticle('der');
appController._setCurrentTranslation('initial translation');
jc.expect("appView.setArticleColor").once().withArguments(false);
jc.expect("appView.setScore").once().withArguments(9);
jc.expect("appView.showResult").once();
appController.answer('das');
assertEquals('Score must be decreased', 9, appController._getScore());
});
};
Ok, now we have tests written, but how to run them? Now JsTestDriver part:
- We need to define a “jsTestDriver.conf” file which holds the configuration for JsTestDriver server.
server: http://localhost:10000
load:
- src/main/webapp/resources/js/thirdparty/jquery-1.5.js
- src/main/webapp/resources/js/mine/controller/*.js
- src/main/webapp/resources/js/mine/manager/*.js
- src/test/webapp/resources/js/thirdparty/*.js
- src/test/webapp/resources/js/mine/controller/*.js
- src/test/webapp/resources/js/mine/manager/*.js
Jack is included with the definition <code> – src/test/webapp/resources/js/thirdparty/*.js<code>.
- Define the maven plugin configuration:
<plugin>
<groupId>com.google.jstestdriver</groupId>
<artifactId>maven-jstestdriver-plugin</artifactId>
<version>${jsTestDriver.mavenPlugin.version}</version>
<executions>
<execution>
<id>run-tests</id>
<phase>test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<browser>chromium-browser, /usr/bin/firefox</browser>
<port>10000</port>
<basePath>${project.basedir}</basePath>
</configuration>
</execution>
</executions>
</plugin>
Important things are
- I defined the “basePath” as project base directory, thus I need to specify the resources relative to that folder (shown above in jsTestDriver.conf)
- I gave the same port number in the server’s definition : 10000
Now when I run “mvn test”, JsTestDriver Maven plugin will open the browsers defined in the configuration automatically, binds them to server and closes them (or the opened tabs) when the testing is done.
Here is a sample output:
-------------------------------------------
J S T E S T D R I V E R
-------------------------------------------
................
Total 16 tests (Passed: 16; Fails: 0; Errors: 0) (29.00 ms)
Firefox 5.0 Linux: Run 8 tests (Passed: 8; Fails: 0; Errors 0) (28.00 ms)
Chrome 12.0.742.112 Linux: Run 8 tests (Passed: 8; Fails: 0; Errors 0) (29.00 ms)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
If you take a look at the code here, you will see that I use a Maven profile for Javascript unit tests. I did it because I wanted to trigger it only when I want to. It is really annoying to see browsers opening and closing while doing a install. I put the JsTestDriver execution into the profile called “jsUnitTest” and for triggering Javascript unit tests, you need to run Maven build with parameter “-P jsUnitTest”. Ideally, on your continous build system which has JsTestDriver integrated, you would want to use that profile in default build.
But how to integrate it into a continuous build system? Wait for part 12 (see the plan).
In the next part, I am going to write some view tests which are important often just ignored.