Testing With Jasmine and Maven 3.0
This article will show you how to overcome the testing challenges for a Spring Boot application using Jasmine and Maven 3.0.
Join the DZone community and get the full member experience.
Join For FreeThere are still a lot of companies who cannot easily migrate or upgrade some technologies, especially ones which are the core of technology stack, and we may need to deal with them for a while. So, I’d like to share my experience in creating a single web application on top of a Spring Boot application (v 1.5) and Maven 3.0.4.
It may be easy, but I faced a few interesting challenges which may be useful in other cases, or the same one, if you have to.
Road to the Code
I really like the approach which starts from the mockup, so first thing first, start from what we really need and how it will be used by the end user. So, first step done, mockup and flow definition:
Then it’s time for the architecture and technology stack. We have taken into consideration many aspects, like simplicity, possible evolution, team knowledge, and time.
We ended up with something really basic and easy:
Single web page app
HTML5
JQuery
Jasmine
And of course, Maven 3.0.5 as the building framework.
Obviously, the option to upgrade Maven version was discarded. The official response was: out of scope and effort above our current “firepower” (time and team resources); but I think it was fear (it is always there in case of technology update/changes), which could be reasonable in a company with a really large business.
Now that we have all the necessary information to start, it’s time for testing!
The Testing Challenges
In order to execute the test, I have added a Maven plugin, which is able to execute Javascript tests written with Jasmine and executed with many drivers (e.g. PhantomJS): Jasmine Maven Plugin.
<build>
...
<plugins>
<plugin>
<groupId>com.github.searls</groupId>
<artifactId>jasmine-maven-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
<configuration>
<jsSrcDir>${project.basedir}/src/main/resources/public/js</jsSrcDir>
<jsTestSrcDir>${project.basedir}/src/test/javascript</jsTestSrcDir>
</configuration>
</plugin>
</plugins>
...
</build>
;et’s try to execute the Maven test phase, with no tests yet, and there we go: the first problem.
[ERROR] Failed to execute goal com.github.searls:jasmine-maven-plugin:2.2:test (default) on project jasmine-old-mvn: The plugin com.github.searls:jasmine-maven-plugin:2.2 requires Maven version 3.1.0 -> [Help 1]
OK, I cannot use the latest version, so I have to use a previous one, but which one? Not many choices - the most recent version which supports Maven 3.0 is 1.3.1.6.
Unfortunately, this means we are losing a lot of improvements added in version 2.0 (from official website):
Upgraded Jasmine version to 2.3.0.
Version of the plugin is no longer kept in sync with the Jasmine version.
Jasmine is now brought in as a WebJar.
Added ability to override the version of Jasmine used. See the documentation for more information.
Deprecated configuration parameters are no longer supported.
Upgraded Selenium version to 2.45 as well as upgraded many other dependencies.
Execution time is written to the test report if available. See #271
PhantomJs is now used by default.
Uses core of the phantomjs-maven-plugin to automatically download and install PhantomJs.
The browserVersion configuration parameter has been deprecated. Use webDriverCapabilitiesinstead.
I want to use JQuery, and there is a fantastic library which helps to write tests in Jasmine with JQuery: Jasmine-Jquery, so our pom.xml will be something like:
<build>
...
<plugins>
<plugin>
<groupId>com.github.searls</groupId>
<artifactId>jasmine-maven-plugin</artifactId>
<version>1.3.1.6</version>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
<configuration>
<jsSrcDir>${project.basedir}/src/main/resources/public/js</jsSrcDir>
<jsTestSrcDir>src/test/javascript</jsTestSrcDir>
<preloadSources>
<sorurce>${project.basedir}/src/test/javascript/support/jasmine-jquery.js</sorurce>
</preloadSources>
</configuration>
</plugin>
</plugins>
...
</build>
And our first test, the login form:
describe('Login form', function () {
var spyEvent;
it('should execute the login request', function () {
spyEvent = spyOnEvent('#login', 'click');
action.displayLoginForm();
$('#login').trigger( "click" );
expect('click').toHaveBeenTriggeredOn('#login');
expect(spyEvent).toHaveBeenTriggered();
});
});
I expect a failure because there is nothing yet, and in fact I get:
1 failure:
1.) Login form it should execute the login request <<< FAILURE!
* TypeError: Cannot find function addMatchers in object [object Object]. in http://localhost:45079/spec/support/jasmine-jquery.js (line 376)
* TypeError: $ is not a function, it is undefined. in http://localhost:45079/spec/support/jasmine-jquery.js (line 300)
* TypeError: $ is not a function, it is undefined. in http://localhost:45079/spec/support/jasmine-jquery.js (line 94)
Results: 1 specs, 1 failures
The error about the function $ seems to be right because I haven’t added Jquery yet, however, the first one, about addMatchers, is scaring me. Let’s ignore it for now and add JQuery.
Execute again, and:
[ERROR] Failed to execute goal com.github.searls:jasmine-maven-plugin:1.3.1.6:test (default) on project jasmine-old-mvn: The jasmine-maven-plugin encountered an exception:
[ERROR] java.lang.RuntimeException: org.openqa.selenium.WebDriverException: com.gargoylesoftware.htmlunit.ScriptException: TypeError: Cannot find function addEventListener in object [object HTMLDocument]. (http://localhost:38181/spec/support/jquery-3.2.1.min.js#3)
That’s definitely bad! Something is wrong on the driver which executes the Javascript; indeed, HTMLUnit is the default driver and it seems it does not implement some Javascript features which are used by JQuery.
What can we do? Avoid using JQuery, or use an older version? Not a good idea. Let’s try to use another driver, like PhantomJS:
<dependencies>
...
<dependency>
<groupId>com.github.detro.ghostdriver</groupId>
<artifactId>phantomjsdriver</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
...
</dependencies>
<build>
...
<plugins>
<plugin>
<groupId>com.github.searls</groupId>
<artifactId>jasmine-maven-plugin</artifactId>
<version>1.3.1.6</version>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
<configuration>
<jsSrcDir>${project.basedir}/src/main/resources/public/js</jsSrcDir>
<jsTestSrcDir>src/test/javascript</jsTestSrcDir>
<preloadSources>
<sorurce>${project.basedir}/src/test/javascript/support/jquery-3.2.1.min.js</sorurce>
<sorurce>${project.basedir}/src/test/javascript/support/jasmine-jquery.js</sorurce>
</preloadSources>
<webDriverClassName>org.openqa.selenium.phantomjs.PhantomJSDriver</webDriverClassName>
</configuration>
</plugin>
</plugins>
...
</build>
Try again and we’ll get:
[ERROR] Caused by: java.lang.IllegalStateException: The path to the driver executable must be set by the phantomjs.binary.path capability/system property/PATH variable; for more information, see https://github.com/ariya/phantomjs/wiki. The latest version can be downloaded from http://phantomjs.org/download.html
[ERROR] at com.google.common.base.Preconditions.checkState(Preconditions.java:197)
My fault, PhantomJs needs a specific binary file to execute the code. Also, you can find it in the official jasmine-maven documentation, and thankfully, there is also a way to install it automatically as part of build process, with the PhantomJS Maven Plugin.
We usually test and build our application with CI tools like Jenkins and Bamboo, and from a developer perspective, having automation as much as possible is a must, also for the building machine configuration.
In my case, we were using Bamboo to test and build, so let’s try it:
<build>
...
<plugin>
<groupId>com.github.klieber</groupId>
<artifactId>phantomjs-maven-plugin</artifactId>
<version>0.8-SNAPSHOT</version>
<executions>
<execution>
<goals>
<goal>install</goal>
</goals>
</execution>
</executions>
<configuration>
<version>1.9.7</version>
</configuration>
</plugin>
<plugin>
<groupId>com.github.searls</groupId>
<artifactId>jasmine-maven-plugin</artifactId>
<version>1.3.1.6</version>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
<configuration>
<jsSrcDir>${project.basedir}/src/main/resources/public/js</jsSrcDir>
<jsTestSrcDir>src/test/javascript</jsTestSrcDir>
<preloadSources>
<sorurce>${project.basedir}/src/test/javascript/support/jquery-3.2.1.min.js</sorurce>
<sorurce>${project.basedir}/src/test/javascript/support/jasmine-jquery.js</sorurce>
</preloadSources>
<webDriverClassName>org.openqa.selenium.phantomjs.PhantomJSDriver</webDriverClassName>
<webDriverCapabilities>
<phantomjs.binary.path>${phantomjs.binary}</phantomjs.binary.path>
</webDriverCapabilities>
</configuration>
</plugin>
...
</build>
It’s a fantastic plugin which downloads the PhantomJS binary and sets the variable "${phantomjs.binary}" which can be used to configure Jasmine.
But unfortunately, there is no way to make it work; it requires Maven 3.1 minimum.
No problem, then; let’s install this binary in Bamboo and point to the installation directory.
I have no access to the Bamboo server, so I have to ask the sysadmins to do that.
Response: No way! It is not going to work.
Okay, let’s avoid the details of the discussion I had with them (very boring stuff) and find another solution.
After a lot of research, I came back to the first “solution:” HTMLUnit, which unfortunately does not support JQuery. So, what can I do?
Jquery is an external library, and what do we do with external dependencies on unit tests? We mock them!
Mocking this kind of library could be really challenging, so having a good library which simplifies the work is a must. I have chosen to use Sinon.JS. I like it and it's really easy, but you can choose whatever you want.
So, let’s add it to our test configuration and create some base mocks for JQuery:
<build>
...
<plugins>
<plugin>
<groupId>com.github.searls</groupId>
<artifactId>jasmine-maven-plugin</artifactId>
<version>1.3.1.6</version>
<executions>
<execution>
<goals>
<goal>test</goal>
</goals>
</execution>
</executions>
<configuration>
<jsSrcDir>${project.basedir}/src/main/resources/public/js</jsSrcDir>
<jsTestSrcDir>src/test/javascript</jsTestSrcDir>
<preloadSources>
<sorurce>${project.basedir}/src/test/javascript/support/sinon-2.4.1.js</sorurce>
<sorurce>${project.basedir}/src/test/javascript/support/baseMocks.js</sorurce>
</preloadSources>
</configuration>
</plugin>
</plugins>
...
</build>
The file baseMocks.js is my Javascript file, which contains all base mocks and utilities to build the tests in an isolated environment from JQuery.
This is an excerpt; if you are interested in seeing the full file, just let me know in the comments and I’ll provide it. Take into account that the mocks and the utilities are written to simplify my testing logic, so they could not be appropriate for your cases.
//JQuery mock
// $ [selector]
$ = sinon.stub();
function stubSelector() {
return sinon.stub({
val: function(){},
append: function(){},
modal: function(){},
html: function(){},
submit: function(){},
unbind: function(){},
click: function(){},
getAttribute: function(){},
prop: function(){},
parent: function(){},
clone: function(){},
appendTo: function(){},
is: function(){},
remove: function(){},
each: function(){},
find: function(){},
text: function(){},
addClass: function(){},
data: function(){},
hasClass: function(){},
get: function(){},
change: function(){}
});
}
$.returns(stubSelector());
function mockSelectorListWithEach(data) {
return {
each: function(callback) {
data.forEach(function(elem, index) {
callback(index, elem);
});
},
fail: function() {}
};
};
function mockSelectorWithText(textData) {
return {
text: function() {
return textData;
}
};
};
function mockInputElement(elementType, elementId, elementValue, cssClass) {
return {
type: elementType,
id: elementId,
value: elementValue,
val: function() { return elementValue; },
getAttribute: function() { return elementId; },
hasClass: function(requestedClass) { return (cssClass == requestedClass); }
};
};
//*** $.ajax ***
$.ajax = sinon.stub();
function stubAjaxResponse() {
return sinon.stub({
done: function() {},
fail: function() {}
});
};
$.ajax.returns(stubAjaxResponse());
...
Now, let’s back to the test and adjust it according to the new mock functions and implement the base login functionality. I want to see a fantastic green line!
describe('Display Login form', function () {
it('should display the login form and set the submit event', function () {
var htmlStub = stubSelector();
$.withArgs('#main').returns(htmlStub);
var submitStub = stubSelector();
$.withArgs('#login').returns(submitStub);
action.displayLoginForm();
sinon.assert.calledWith($, "#main");
sinon.assert.calledWith(htmlStub.html, widget.loginForm);
sinon.assert.calledWith($, "#login");
sinon.assert.calledOnce(submitStub.submit);
});
});
It may be a bit difficult to read it at the beginning, but you’ll get used to it really quick. This test verifies that the function “displayLoginForm” sets specific HTML content in the div with id “#main” and set the event “submit” in the input element with id “#login”.
You can also rewrite the test in order to verify which function is given to the submit event, and then test that function in another test.
Finally, we got our environment to make a fantastic single web application in TDD/BDD style! It may take a bit more time to create our specific mock and utility function, but it is definitely worth doing it.
Following are a couple of mock functions that can help you:
The JQuery ajax function:
/*** $.ajax ***/
$.ajax = sinon.stub();
function stubAjaxResponse() {
return sinon.stub({
done: function() {},
fail: function() {}
});
};
$.ajax.returns(stubAjaxResponse());
function mockAjaxDoneResponse(data, status) {
return {
done: function(callback) {
callback(data, status, null);
},
fail: function() {}
};
};
function mockAjaxFailResponse(status) {
return {
done: function() {},
fail: function(callback) {
var jqXHR = { responseText: "Error info" };
callback(jqXHR, status, null);
}
};
};
The D3.js functions to create a graph chart:
//d3 mock
function mockD3Force() {
return {
nodes : function(){ return this; },
links : function(){ return this; },
gravity : function(){ return this; },
charge : function(){ return this; },
size : function(){ return this; },
on : sinon.stub(),
start : sinon.stub()
};
}
function mockD3Layout(force) {
return {
force: function(){
return force;
}
};
};
function mockD3(layout) {
return {
layout : layout,
select : function(){ return this; },
selectAll : function(){ return this; },
append : function(){ return this; },
data : function(){ return this; },
enter : function(){ return this; },
attr : function(){ return this; },
call : function(){ return this; },
text : function(){ return this; }
};
};
var forceMock = mockD3Force();
var layoutMock = mockD3Layout(forceMock);
d3 = mockD3(layoutMock);
The Set class! Yes, if you want to use it you have to mock, because it is not implemented in HTMLUnit:
//Set class
function Set() {
return {
values: [],
add: function(element) {
var alreadyInserted = false;
this.values.forEach(function(iel) {
if(iel == element) alreadyInserted = true;
});
if(!alreadyInserted) {
this.values.push(element)
}
},
forEach: function(callback) {
this.values.forEach(function(element) {
callback(element);
});
}
};
}
Conclusion
Hopefully, you’ll never face this problem, but if you have to, please don’t give up TDD and automatic tests; you can always find a way to do it.
Opinions expressed by DZone contributors are their own.
Comments