Angularjs
Jasmine
Nodejs
Karma
Grunt
With AngularJS, you get Dependency Injection - duh, great post so far…
You love DI so you start cranking out tests injecting and mocking everything under the sun. You wake up 3 months later and you have a nasty code smell: Copy Paste Monsters are everywhere
Solution: Try your best not to be so damn lazy…I fight it every 2 seconds…
Not Ideal
No, you haven’t won anything for actually reading this BUT pay close attention to the customerOrder object being used in each of the tests. Please remember to always reset your mocked objects in a beforeEach clause.
Why?
Because objects are reference types, each change you make to the object will affect other tests - assuming you are referencing the same object in multiple tests: BAD. Using the beforeEach clause will ensure each test starts out pristine.
You love DI so you start cranking out tests injecting and mocking everything under the sun. You wake up 3 months later and you have a nasty code smell: Copy Paste Monsters are everywhere
Solution: Try your best not to be so damn lazy…I fight it every 2 seconds…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
"use strict"; | |
//NOT IDEAL | |
describe("Contact controller:", function() { | |
var scope, | |
orderModel, | |
customerOrder; | |
//load up order application or appropriate module for reducing scope in test | |
beforeEach(module("sampleOrderApp")); | |
//reset any mock data to be used for each test | |
beforeEach(function() { | |
customerOrder = { | |
orderId: 4, | |
lineItems: [{ sku: "2345L", qty: 2, price: 43.21 }, | |
{ sku: "G432", qty: 1, price: 100.00 }, | |
{ sku: "YHL323", qty: 1, price: 10.23 }], | |
customer: { firstName: "foo", lastName: "bar", email: "foo@bar.com"} | |
}; | |
}); | |
//begin the tests | |
it("validateContact scope method is called and returns true", function() { | |
inject(["$rootScope", "$controller", "clientOrderModel", | |
function($rootScope, $controller, clientOrderModel) { | |
clientOrderModel.order = customerOrder; | |
$controller("ContactCtrl", {$scope: $rootScope}); //create instance of controller | |
//validate contact, should already have email with default setup | |
var isValid = $rootScope.validateContact(); | |
//assert | |
expect(isValid).toBeTruthy(); | |
}]); | |
}); | |
it("validateContact scope method is called and returns false", function() { | |
inject(["$rootScope", "$controller", "clientOrderModel", | |
function($rootScope, $controller, clientOrderModel) { | |
clientOrderModel.order = customerOrder; | |
clientOrderModel.order.customer.email = undefined; | |
$controller("ContactCtrl", {$scope: $rootScope}); //create instance of controller | |
//validate contact, this time change the order model to remove email | |
var isValid = $rootScope.validateContact(); | |
//assert | |
expect(isValid).not.toBeTruthy(); | |
}]); | |
}); | |
}); | |
}()); |
- you can see this spec really contains 2 simple tests, each injecting dependencies into the test ($rootScope, $controller, clientOrderModel)
- the controller probably displays contact information and validation is needed for the contact email (we’re just checking if it’s present, not real email validation: have a heart)
- you created the first test and went through your normal: red -> green -> refactor…you refactored the controller/service/thingy but forgot to refactor the test…who cares, it’s green - move on already…then time for test two - reach for your friend the copy paste monster and you’re flying high
- at first glance this might not seem like a big deal but over the span of 3 months, each couple of sprints adding new developers who themselves want to copy/paste from the previous guy because their too afraid to mess something up…and BAM: you have way too much code everywhere and you forgot to stay DRY
- tests are harder to maintain, read, etc. - same old story presented a million times by testing authors over the last 15 years
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
"use strict"; | |
//BETTER | |
describe("Contact controller:", function() { | |
var scope, | |
orderModel, | |
customerOrder; | |
//load up order application or appropriate module for reducing scope in test | |
beforeEach(module("sampleOrderApp")); | |
//inject all services used across all tests, reset any mock data to be used for each test | |
beforeEach(function() { | |
customerOrder = { | |
orderId: 4, | |
lineItems: [{ sku: "2345L", qty: 2, price: 43.21 }, | |
{ sku: "G432", qty: 1, price: 100.00 }, | |
{ sku: "YHL323", qty: 1, price: 10.23 }], | |
customer: { firstName: "foo", lastName: "bar", email: "foo@bar.com"} | |
}; | |
inject(["$rootScope", "$controller", "clientOrderModel", | |
function($rootScope, $controller, clientOrderModel) { | |
scope = $rootScope; | |
orderModel = clientOrderModel; | |
orderModel.order = customerOrder; | |
$controller("ContactCtrl", {$scope: scope}); //create instance of controller | |
}]); | |
}); | |
//begin the tests | |
it("validateContact scope method is called and returns true", function() { | |
//validate contact, should already have email with default setup | |
var isValid = scope.validateContact(); | |
//assert | |
expect(isValid).toBeTruthy(); | |
}); | |
it("validateContact scope method is called and returns false", function() { | |
//validate contact, this time change the order model to remove email | |
orderModel.order.customer.email = undefined; | |
var isValid = scope.validateContact(); | |
//assert | |
expect(isValid).not.toBeTruthy(); | |
}); | |
}); | |
}()); |
- you can see we’ve moved each of the inject statements into 1 beforeEach, we’re also able to do this because each test requires the same dependencies
- you can also see each of the tests is much easier to read, really only a couple of lines
- a nice caveat to this approach: folks coming in after you will be more willing to write tests too, especially if they’re less complex, easier to write & understand, blah…
No, you haven’t won anything for actually reading this BUT pay close attention to the customerOrder object being used in each of the tests. Please remember to always reset your mocked objects in a beforeEach clause.
Why?
Because objects are reference types, each change you make to the object will affect other tests - assuming you are referencing the same object in multiple tests: BAD. Using the beforeEach clause will ensure each test starts out pristine.