Optimize your Ionic Testing with Wallaby.js, Bard.js, and WebStorm
Coming from a .NET background one tool I sorely missed when switching primarily to JavaScript was NCrunch, the continuous testing extension for Visual Studio. While Webstorm’s support for Karma is top notch, nothing beats being able to witness instantly, parallel to your code, when a test passes or fails. This increased visibility not only makes development and refactoring a breeze, it also offers the side benefit of not so subtly encouraging you when your test coverage is lacking.
With this in mind, I was extremely excited to hear about Wallaby.js, a continuous test runner plugin for Webstorm and the JetBrains line of code editors (more to come!). Similar to NCrunch, Wallaby gives you instantaneous test feedback within your development environment. For TDD, BDD, or anyone who tests their code in any capacity, this article will show you how to get up and running with Wallaby.js and Webstorm, using an Ionic framework application as an example. We will also discuss using Bard.js, an Angular testing helper library written by Ward Bell, to make your Angular tests much easier to write and maintain.
Install the WallabyJS Plugin
1.) Visit the Wallaby website and sign up for the (currently) free download.
Wallaby Signup
2.) Navigate to the ‘Preferences’ tab under WebStorm’s main menu.
WebStorm > Preferences
3.) In the left menu, select ‘Plugins’ then select ‘Install plugin from disk’.
Install Plugin
4.) Select the ‘wallaby-intellij’ zip file that you downloaded via the email link.
Wallaby Plugin Zip
Setup your Ionic Project
Before we can continue we need to first set up the Ionic project we are going to use for this article. Let’s utilize the Ionic CLI and a few common commands to make this quick and easy. In addition to the core Ionic dependencies, we are also going to bring in angular-mocks for our Angular testing base, Bard.js for more concise, easier to read test setup, and Sinon.js as Bard uses Sinon spies under the covers for it’s mock service functionality.
*Please note the base Sinon package from Bower will not work correctly. For more information, please visit this article on the subject.
|
|
ionic start gitHubHelper blank
cd gitHubHelper
bower install angular-mocks bardjs http://sinonjs.org/releases/sinon-1.12.2.js jasmine-sinon
mkdir specs
cd www
mkdir app
cd app
mkdir views controllers services
|
We now have our base Ionic starter project and file structure ready to go, let’s proceed to our WallabyJS project configuration!
WallabyJS Project Configuration
Now that we have created our Ionic shell, let’s configure the project to utilize Wallaby for continuous testing.
1.) Create a file named wallaby.js at the root of your Ionic project. Please note that in the majority of cases this will be a wallaby.json file. In this particular instance, Bard.js use of Function.prototype.bind has to be polyfilled due to a lack of support from PhantomJS (which Wallaby uses under the covers as a headless browser).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
module.exports = function () {
return {
"files": [
"www/lib/ionic/js/ionic.bundle.js",
"www/lib/angular-mocks/angular-mocks.js",
"www/lib/sinon/index.js",
"www/lib/bardjs/dist/bard.js",
"www/app/**/*.js"
],
"tests": [
"specs/*.js"
],
//required to work with BardJS due to PhantomJS
bootstrap: function () {
// Function.prototype.bind polyfill
if (!Function.prototype.bind) {
Function.prototype.bind = function (oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
fNOP = function () {
},
fBound = function () {
return fToBind.apply(this instanceof fNOP && oThis
? this
: oThis,
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// test this.prototype in case of native functions binding:
if (this.prototype)
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
};
}
}
};
};
|
2.) In the top right corner of your WebStorm window, select ‘Edit Configurations’.
Edit Configurations
3.) Next, select Wallaby.js and click ‘Add New Configuration’.
Add New Configuration
4.) Specify the path of your wallaby.js file and click ‘Apply’.
Apply Config
5.) Last, click on the run button in the top right corner of WebStorm.
Run Wallaby
If all went correctly you will notice a short download as Wallaby completes a one-time install of it’s dependencies. The console will then open at the bottom of your IDE and Wallaby is now watching our project and spec files!
Wallaby up and running!
Putting Wallaby to Work!
The application we are building to demonstrate Wallaby is a small, one screen Ionic application to quickly look up the details of your friend’s GitHub repositories. This will consist of one controller for our only view and a service to talk to the GitHub public API. Let’s begin by writing an initial spec for both our service and controller and see what Wallaby gives us!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
describe('The GitHubController', function(){
var GitHubController;
beforeEach(function(){
bard.appModule('githubHelper')
bard.inject('$controller');
GitHubController = $controller('GitHubController');
});
it('should exist', function(){
expect(GitHubController).toBeDefined();
});
});
|
|
|
describe('The GitHubService', function(){
beforeEach(function(){
bard.appModule('githubHelper');
bard.inject('GitHubService');
});
it('should exist as a service', function(){
expect(GitHubService).toBeDefined();
})
});
|
As you write these tests you will notice two code-level additions to your normal WebStorm experience. First, any code evaluation that causes a test failure will be annotated next to the test line in red. This gives quick visibility into why a particular test is failing. Next, to the left of each line of code you will find a red or green box indicating whether the line was executed successfully. Clicking on these boxes gives additional details as to why a test failed, even linking to the particular line of code where the exception occurred. Using this, you can quickly diagnose bugs, correct issues, and immediately be informed when your tests begin to pass or fail. As you can imagine, this tight feedback loop proves invaluable when debugging and refactoring your code base.
WebStorm Integration
Additional Test Details
Test Coverage
Now that we have written some failing tests, let’s implement our controller and service and see what sort of information Wallaby provides inside our code. For the sake of brevity I am going to show the controller and service in their completed form. Optimally, with the sort of feedback Wallaby offers, a test-first approach would be extremely beneficial.
First, let’s write our service to communicate with the GitHub API. Our service will have one public member, allowing the user to enter a search term and receive details regarding that users contributions. If the user entered does not exist we will return an error message.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
(function(){
angular
.module('githubHelper')
.factory('GitHubService', GitHubService);
GitHubService.$inject = ['$http', '$q'];
function GitHubService($http, $q){
var baseUrl = 'https://api.github.com/users/';
return{
getBaseInfoByUsername: getBaseInfoByUsername
};
function getBaseInfoByUsername(userName){
return $http.get(baseUrl + userName)
.then(onSuccess)
.catch(onFailure);
}
function onSuccess(response){
return response.data;
}
function onFailure(){
return $q.reject('User not found!');
}
}
})();
|
Before we update our tests, you can see Wallaby informing us that we have no coverage of this service.
GitHub Service No Coverage
As you can see, Wallaby lets us know next to each service method that we do not have tests covering these lines. Let’s update our specs and watch these indicators turn green.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
describe('The GitHubService', function(){
var sampleSearchTerm = 'BTroncone',
errorMessage = "User not found!",
mockResponse = [{}];
beforeEach(function(){
bard.appModule('githubHelper');
bard.inject('GitHubService', '$q', '$httpBackend');
//hack to prevent template caching from failing test
//http://stackoverflow.com/questions/29424792/why-does-httpbackend-flush-result-in-unexpected-request
$httpBackend.whenGET(/^\app\//).respond(200, '');
});
it('should exist as a service', function(){
expect(GitHubService).toBeDefined();
});
it('should call https://api.github.com/users/ + username when getBaseInfoByUsername is called', function(){
$httpBackend.when('GET', 'https://api.github.com/users/' + sampleSearchTerm)
.respond(200, mockResponse);
GitHubService.getBaseInfoByUsername(sampleSearchTerm).then(function(data){
expect(data.length).toBe(mockResponse.length);
});
$httpBackend.flush();
});
it('should return an error message of "User not found!" when nothing is returned', function(){
$httpBackend.when('GET', 'https://api.github.com/users/' + sampleSearchTerm)
.respond(500, {description: errorMessage});
GitHubService.getBaseInfoByUsername(sampleSearchTerm).catch(function(error){
expect(error).toBe(errorMessage);
});
$httpBackend.flush();
});
});
|
GitHubService Passing
Let’s now add our GitHubController and specs to complete the core functionality of our Ionic application.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
(function(){
angular
.module('githubHelper')
.controller('GitHubController', GitHubController);
GitHubController.$inject = ['GitHubService', '$ionicPopup'];
function GitHubController(GitHubService, $ionicPopup){
var vm = this;
vm.searchTerm = "";
vm.userInfo = null;
vm.getUserInfo = getUserInfo;
function getUserInfo(){
GitHubService.getBaseInfoByUsername(vm.searchTerm)
.then(success, failure);
function success(userInfo){
vm.userInfo = userInfo;
}
function failure(error){
var alertPopup = $ionicPopup.alert({
title: error
});
alertPopup.then(function() {
console.log('Popup dismissed!');
});
}
}
}
})();
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
describe('The GitHubController', function(){
var GitHubController,
sampleSearchTerm = 'BTroncone',
mockUser = [{}];
beforeEach(function(){
bard.appModule('githubHelper');
bard.inject('$controller', '$q', '$rootScope', '$ionicPopup', 'GitHubService', '$httpBackend');
bard.mockService(GitHubService,{
getBaseInfoByUsername: $q.when(mockUser)
});
GitHubController = $controller('GitHubController');
});
it('should exist', function(){
expect(GitHubController).toBeDefined();
});
describe('the search process', function(){
beforeEach(function(){
GitHubController.searchTerm = sampleSearchTerm;
//hack to prevent template caching from failing test
//http://stackoverflow.com/questions/29424792/why-does-httpbackend-flush-result-in-unexpected-request
$httpBackend.whenGET(/^\app\//).respond(200, '');
});
it('should call the GitHubService with the appropriate search term', function(){
GitHubController.getUserInfo();
expect(GitHubService.getBaseInfoByUsername).toHaveBeenCalledWith(GitHubController.searchTerm);
});
it('should populate the userDetails object with the results of the search', function(){
GitHubController.getUserInfo();
$rootScope.$apply();
expect(GitHubController.userInfo.length).toBe(mockUser.length)
});
});
});
|
Notice how Bard.js makes the test setup process near trivial. Without Bard, Angular test setup has a tendency to become complicated to understand and overly verbose. Bard eliminates this pain and makes mocking services as easy as a few lines of code.
Create our View
Now that our controller and service is complete and our tests are passing, all that’s left is to add a view to display our GitHub data.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
<ion-view>
<ion-content>
<div class="bar bar-header item-input-inset">
<label class="item-input-wrapper">
<i class="icon ion-ios-search placeholder-icon"></i>
<input type="search" placeholder="Enter a GitHub Username" ng-model="vm.searchTerm">
</label>
<button class="button button-clear" ng-click="vm.getUserInfo()">
Search
</button>
</div>
<ion-list ng-show="vm.userInfo">
<a class="item item-icon-left" href="#">
<i class="icon ion-person"></i>
Name
<span class="item-note">
{{vm.userInfo.name}}
</span>
</a>
<a class="item item-icon-left" href="#">
<i class="icon ion-ios-people"></i>
Followers
<span class="item-note">
{{vm.userInfo.followers}}
</span>
</a>
<a class="item item-icon-left" href="#">
<i class="icon ion-person-stalker"></i>
Following
<span class="item-note">
{{vm.userInfo.following}}
</span>
</a>
<a class="item item-icon-left" href="#">
<i class="icon ion-social-github"></i>
Repo Count
<span class="item-note">
{{vm.userInfo.public_repos}}
</span>
</a>
<a class="item item-icon-left" href="#">
<i class="icon ion-social-github-outline"></i>
Gists Count
<span class="item-note">
{{vm.userInfo.public_gists}}
</span>
</a>
<a class="item item-icon-left" href="#">
<i class="icon ion-person-add"></i>
Hireable?
<span class="item-note">
{{vm.userInfo.hireable ? 'Yes' : 'No'}}
</span>
</a>
</ion-list>
</ion-content>
</ion-view>
|
Let’s see our final product!
Final App
Conclusion
In conclusion, introducing Wallaby and continuous testing to your development process can greatly increase test visibility, feedback, and your overall coding experience. I highly suggest giving it a try on a current or new project! Please drop me a message on Twitter or in the comments and let me know what you think. The entire codebase for this article can be found on GitHub. Until next time, happy coding!
[출처] http://briantroncone.com/?p=463