So You Want to Build an App in ES6, Part Five
In the previous article, we built a controller, a service, and a directive in AngularJS. While that will get you started in developing a robust application, you cannot really build large-scale applications without testing.
This article is going to go over unit testing using Karma and Jasmine. This article will not go over integration testing or regression testing. Those topics are too large for this short series and they require considerable forethought and planning in order to get the best bang for the buck.
Unit Testing with Karma and Jasmine
There are a lot of unit testing frameworks available. We will be using Karma and Jasmine as these are both fairly simple to configure and use and both have good track records.
If you are following along and using my baseline build system, then you already have Karma and Jasmine installed and configured. You can view the configuration file here: karma.conf.js.
Why Unit Tests
The main purpose of unit tests is to assure that your application functions as you expect it to. This means that you will set up your tests in such a way as to make sure that you cover all sections of your code.
Building unit tests with AngularJS can seem daunting. It's a little more difficult than just building a normal unit test. You have to remember to inject your controllers and services correctly, and make sure that its dependency injection is handled correctly. That being said, once you have your unit tests up and running, it's just a matter of running the tests whenever your code changes. You can automate this behind your gulp watch task, or you can create a separate watch task to do this. The choice is yours.
Unit tests with Jasmine start off with the describe()
function. This lets you describe your test suite. Use the it()
function to describe what specific sections of code should do during execution. The beforeEach()
function allows you to bootstrap your tests by including applications and modules, setting up services or controllers, and other startup tasks.
Test Driven Development
There is a school of thought that says "Start with your tests, then build your application to make them pass." This is known as test driven development. The examples here are going to show how we'd set up a TDD environment, and what we would do to make our tests pass.
We have to first remove all of our source files, and then rebuild our application using a blank source/index.js file for this example.
Let's start by writing our tests. In AngularJS, we have different modules that allow us to do different things. They control all aspects of your application, and as such deserve to be tested to make sure that they work.
/test/app.spec.js
describe('Our Application: ', function () {
var application;
// Startup
beforeEach(module('OurApplication'));
beforeEach(function () {
application = angular.module('OurApplication');
});
// Tests
it('should instantiate.', function () {
expect(application).not.toBeNull();
});
});
If we run this test right now with gulp karma
, we will end up with this result (your PhantomJS/OS may be different):
Suites and tests results:
- Our Application: :
* should instantiate. : failed
Browser results:
- PhantomJS 1.9.8 (Windows 7): 1 tests
- failed
This means that our unit test has failed. We knew that this was going to happen, since we removed our entire application. Let's build our application back up, using TDD.
/source/index.js
angular.module('OurApplication', []);
I've created this file, and rebuilt it using gulp browserify
. After running gulp karma
, I get the following results:
Suites and tests results:
- Our Application: :
* should instantiate. : ok
Browser results:
- PhantomJS 1.9.8 (Windows 7): 1 tests
- ok
This tells us that we have created a valid test, and if this is all our application needed to do, then we would have it working 100%. We obviously want our applications to do more than just instantiate. We need them to do things, like call REST API's or modify the DOM or allow user interaction. With unit testing, we can show that our applications will do exactly what we expect them to.
Let's continue with a small service. Our service is going to be simple, and perform the following actions:
- Double a number
- Add two numbers together
- Greet a person with "Hello, {name}!"
We will start by writing our tests for this little service:
/test/app.spec.js
describe('Our Application', function () {
var application;
// Startup
beforeEach(module('OurApplication'));
beforeEach(function () {
application = angular.module('OurApplication');
});
// Tests
it('should instantiate.', function () {
expect(application).not.toBeNull();
});
});
describe('Our Service', function () {
var application, service;
// Startup
beforeEach(module('OurApplication'));
beforeEach(function () {
application = angular.module('OurApplication');
});
// Injecting services
beforeEach(inject(function(OurService) {
service = OurService;
}));
// Tests
it ('should instantiate.', function () {
expect(application).not.toBeNull();
});
it('should have the OurService service', function () {
expect(service).not.toBeNull();
});
it('should double a number.', function () {
expect(service.Double(2)).toBe(4);
expect(service.Double('abc')).toThrow(); });
it('should add two numbers.', function () {
expect(service.Add(1, 2)).toBe(3);
expect(service.Add(1)).toThrow();
expect(service.Add('a', 'b')).toThrow();
});
it('should greet a person.', function () {
expect(service.Greet('Jim')).toBe('Hello, Jim!');
});
});
When we run gulp karma
now, we get the following results:
Suites and tests results:
- Our Application :
* should instantiate. : ok
- Our Service :
* should instantiate. : failed
* should have the OurService service : failed
* should double a number. : failed
* should add two numbers. : failed
* should greet a person. : failed
Browser results:
- PhantomJS 1.9.8 (Windows 7): 6 tests
- 1 ok, 5 failed
Let's go ahead and build our service. We start with this as our base to build off of.
/source/our-service/index.js
var OurServiceModule = angular.module('OurServiceModule', []);
class OurService {
constructor () {}
}
OurService.$inject = [];
OurServiceModule.service('OurService', OurService);
export default OurServiceModule;
We know we need this service to do three things:
- Double a number. If you don't pass a number, we expect that this function should throw an error.
- Add two numbers. If you only pass one argument, we expect this function to throw an error. If you pass non-numeric values, we expect this function to also throw an error.
- Greet a person.
/source/our-service/index.js
var OurServiceModule = angular.module('OurServiceModule', []);
class OurService {
constructor () {}
Double (number) {
if (this.isNaN(number)) {
throw new Error('You must pass a number in order to call this function. You passed {0}'.replace('{0}', number));
}
return number * 2;
}
Add (number1, number2) {
if (number2 === void 0) {
throw new Error('You must pass two numbers in order to call this function. You passed {0} and {1}'.replace('{0}', number1).replace('{1}', number2));
}
if (this.isNaN(number1) || this.isNaN(number2)) {
throw new Error('Both suppleid arguments must be numeric.');
}
return number1 + number2;
}
isNaN (number) {
return isNaN(number - 0) || number === null || number === "" || number === false || number === void 0;
}
Greet (person) {
return "Hello, {0}!".replace('{0}', person);
}
}
OurService.$inject = [];
OurServiceModule.service('OurService', OurService);
export default OurServiceModule;
We now have coverage of all of our tests. We still need to inject this into our application, so let's do that now:
/source/index.js
import OurServiceModule from './our-service';
angular.module('OurApplication', [
OurServiceModule.name
]);
Let's run gulp karma and see what happens.
Suites and tests results:
- Our Application :
* should instantiate. : ok
- Our Service :
* should instantiate. : ok
* should have the OurService service : ok
* should double a number. : failed
* should add two numbers. : failed
* should greet a person. : ok
Browser results:
- PhantomJS 1.9.8 (Windows 7): 6 tests
- 4 ok, 2 failed
Looking at our logs, we see that we are getting an error whenever we are passing a number. This is due to the way jasmine handles errors being thrown. We are going to change our tests slightly, in order to handle throwing errors:
/tests/app.spec.js
it('should double a number.', function () {
expect(service.Double(2)).toBe(4);
expect(function () {
service.Double('abc');
}).toThrow();
});
it('should add two numbers.', function () {
expect(service.Add(1, 2)).toBe(3);
expect(function () {
service.Add(1);
}).toThrow();
expect(function () {
service.Add('a', 'b');
}).toThrow();
});
These changes now allow our service and application to pass all tests.
Suites and tests results:
- Our Application :
* should instantiate. : ok
- Our Service :
* should instantiate. : ok
* should have the OurService service : ok
* should double a number. : ok
* should add two numbers. : ok
* should greet a person. : ok
Browser results:
- PhantomJS 1.9.8 (Windows 7): 6 tests
- 6 ok
To The Future
In this series, we learned the basics of using a build system to transpile our application from ES6 to ES5. We learned how to structure our application in order to allow us to quickly and easily extend and maintain our application long-term. We learned how to set up our unit tests in order to build applications that won't fail us as we add new functionality or change existing functionality.
Stay tuned for more articles on all of these topics and more.