Testability is a major part of design decision making in Angular`s development.
In this sessions we’ll cover what testability is, how it inspires Angular`s design and why it’s good for us.
2. How { Testability } Inspires
AngularJS Design
Ran Mizrahi (@ranm8)
Founder and CEO @ CoCycles
3. About { Me }
• Founder and CEO of CoCycles.
• Former Open Source Dpt. Leader of CodeOasis.
• Architected and managed the Wix App Market Development.
• Full-stack and hands-on software engineer.
4. What is { Testability } ?
“AngularJS - Web Framework Designed
with { Testability } in Mind “
How many times have you heard that?
5. What is { Testability } ?
Why is { Testability } so
important???
6. What is { Testability } ?
“Software testability is the degree to
which a software artifact supports testing
in a given test context”
— Wikipedia
7. What is { Testability } ?
Can I maintain tests? Can I test the code?
• Unmaintainable tests
makes code untestable.
• Broken tests that now one
never bothers to fix
• One tiny change - many
tests break (inc. unrelated
ones)
• Lots of untested code /
poor code coverage
• Monkey patching.
8. { Global } State
function foo() {
var bar = window.bar;
!
if (bar) {
return null;
}
!
return bar;
}
Testing global states is difficult and forces “monkey
patching” (which is not applicable in all languages)…
function foo(bar) {
!
!
if (bar) {
return null;
}
!
return bar;
}
Use of global state: Injecting the dependency:
9. { Global } State
“Insanity: doing the same thing over and over
again and expecting different results.”
— Einstien
10. { Global } State
describe('foo', function() {
// global
it('should return null since `window.bar` is falsy', function() {
window.bar = false;
expect(foo()).to.be.null;
});
//injected
it('should return bar since window.bar is truthy', function() {
expect(foo(‘hi’)).to.be(‘hi');
});
});
Avoiding global state makes our code more:
• Reliable
• Polymorphic
• Decoupled
11. { Coupling } Units
function doSomethingWithBar(bar) {
return bar.doSomething();
}
!
doSomethingWithBar(new Bar());
doSomethingWithBar(new WalkingBar());
function doSomethingWithBar() {
var bar = new Bar();
return bar.doSomething();
}
Coupled unit:
Decoupled unit:
• Coupled to specific
implementation.
• Can be tested only with
monkey patching.
• (Almost ) Impossible to
test with complicated
implementations.
• Decoupled from specific
implementation.
• Can be easily tested
(replaced with a mock).
12. { Coupling } Units
describe('#doSomethingWithBar()', function() {
var bar;
beforeEach(function() {
bar = {
doSomething: sinon.stub().returns('something')
};
});
it('should call bar.doSomething and return', function() {
var result = doSomethingWithBar(bar);
expect(result).to.equal('something');
expect(bar.doSomething).to.have.been.called;
});
});
And it’s much easier to test (no need to handle the new
operator in our tests)
13. Code { Complexity }
function kill(bill) {
if (bill.talker() != null) {
bill.talk();
return "Bill talked";
}
!
if (bill.isDead()) {
bill.resurrect();
!
return "Bill Resurrected";
}
!
if (bill.isAlive()) {
bill.kill();
!
return "Bill is now dead";
}
!
if (bill.isCool()) {
bill.cool();
!
return "Bill is now cool";
}
!
return "Nothing happened";
}
• This code has five different
possible outcomes.
!
• Hard to predict, debug and
maintain.
!
• Testing it requires lots of code.
• No one else but you, would
dare to maintain this test (-:
• Bad separation of concerns.
• For desert, we need only five
different version of mocks.
14. Code { Complexity }
!
describe('kill', function() {
var bill;
!
beforeEach(function() {
bill = {
talker: sinon.stub(),
isDead: sinon.stub(),
isAlive: sinon.stub()
isCool: sinon.stub()
};
});
it('should make bill talk', function() {
bill.talker.returns(true);
!
expect(kill(bill)).to.equal('Bill talked');
});
!
it('should resurrect bill', function() {
bill.isDead.returns(true);
expect(kill(bill)).to.equal('Bill Resurrected');
});
!
it('should kill bill', function() {
bill.isAlive(true);
!
expect(kill(bill)).to.equal('Bill is now dead');
});
!
it('should notice that nothing happened', function(){
expect(kill(bill)).to.equal('Nothing happened');
});
!
it('should make bill extremely cool', function() {
bill.isCool(true);
!
expect(kill(bill)).to.equal('Bill is now cool');
});
});
Sorry it’s small, but
it makes my point
(-:
15. Object { Dependencies }
function foo(bar, boo, bla, beer) {
function doComplicatedStuff() {
return bla.sayHi() + boo.sayPizza() + bla.sayBla() + beer.drink();
}
return {
getSome: function() {
return doComplicatedStuff();
}
};
}
• Many dependencies === Too many responsibilities.
• Too much mocks to maintain.
• Bad separation of concerns.
16. Object { Dependencies }
describe('foo', function() {
var boo = {},
bar = {},
bla = {},
beer = {};
beforeEach(function() {
bla.sayHi = sinon.stub().returns('hi ');
boo.sayPizza = sinon.stub().returns('pizza ');
bla.sayBla = sinon.stub().returns('bla ');
beek.drink = sinon.stub().returns('glagla ');
});
describe('#getSome()', function() {
it('should call all dependencies explicitly and returns and concatenated
string', function() {
var foo = foo(bar, boo, bla, beer);
expect(foo.getSome()).to.equal('hi pizza bla glagla');
});
});
});
• The more dependencies, the harder to test.
• Too much mocks to maintain.
17. Other { Signs }
Other signs that makes code untestable:
!
• Constructor does heavy work.
• Object are passed but never used directly.
• Singletons.
• Unit do too much jobs.
• Side effects
19. { Testability } in AngularJS
So, What Makes AngularJS
{ Testable} ???
20. { Angular’s } Approach
• Angular approach is declarative and not imperative.
• Separates controller-logic from DOM using directives and
data binding.
• Less code === less to test.
21. { Dependency } Injection
• Angular inject the requested service by the function argument
names (declarative approach).
• Can also be done with an array.
!
• Once requested Angular’s injector would instantiate the
requested service and inject it.
angular.module('myModule')
.controller('MyCtrl', MyCtrl);
function MyCtrl($http) {
$http.get('http://google.com').then(getTheMonkey);
}
22. { Dependency } Injection
• Allows minifiers to preserve argument names for the
dependency injection to work with.
• More flexible - Separates dependency declaration from your
unit.
angular.module('myModule')
.controller('MyCtrl', ['$http', MyCtrl]);
function MyCtrl($http) {
$http.get('http://google.com').then(getTheMonkey);
}
23. DOM - Controller { Separation }
angular.module('myModule')
.controller('MyCtrl', ['$scope', MyCtrl]);
function MyCtrl($scope) {
$scope.array = ['one', 'two', 'three'];
$scope.addAnotherOne = function(string) {
$scope.array.push(string);
}
$scope.removeFirst = function() {
$scope.array.shift();
}
}
<div ng-controller="MyCtrl">
<ul>
<li ng-repeat="property in array">{{ property }}</li>
</ul>
<a ng-click="removeFirst()">Remove the first property<a>
</div>
24. DOM - Controller { Separation }
• Two-way data-binding leaves your controller clean from DOM
manipulation and makes it easier to test.
• Less code.
• Decouples the view from the controller.
!
!
describe('MyCtrl', function() {
var createController,
scope;
beforeEach(inject(function($rootScope, $controller) {
scope = $rootScope.$new();
createController = function() {
return $controller('MainCtrl', {
$scope: scope
})
}
}));
it('should remove one property from the array', function() {
var controller = createController();
// test your controller
});
});
25. { Directives }
• Directives handles the responsibility of DOM
manipulation.
• They separate the DOM from your code by avoiding the
use of CSS selectors.
• Easy to reuse across different applications and contexts.
app.directive('sampleOne', function (){
return function(scope, elm, attrs) {
elm.bind('click', function(){
elm.text(scope.$eval(attrs.sampleOne));
});
};
});
26. { Directives }
describe('Testing sampleOne directive', function() {
var scope,
elem,
directive,
compiled,
html;
beforeEach(function (){
html = '<div sample-one="foo"></div>';
inject(function($compile, $rootScope) {
scope = $rootScope.$new();
elm = angular.element(html);
compiled = $compile(elm)(scope);
scope.$digest();
});
});
!
it('Should set the text of the element to whatever was passed.', function() {
scope.foo = 'bar';
expect(elem.text()).toBe('');
elm[0].click();
expect(elem.text()).toBe('bar');
});
});
27. { Providers/Services/Factories }
• Providers allows you to separate configuration phase from
run phase.
• Separate your code to small and reusable units.
• Providers can be easily isolated and tested.
angular.module('myModule')
.provider('myHttp', myHttp);
function myHttp() {
var baseUrl;
this.baseUrl = function(value) {
if (!value) {
return baseUrl;
}
baseUrl = value;
};
this.$get = ['$q', function() {
// myHttp service implementation...
}];
}
28. Configuration Phase
Run Phase
• Runs before any service was instantiated.
!
• Only providers can be injected.
!
• Each provider is injected with the “Provider” suffix (e.g.
$locationProvider)
!
• Allows to purely configure the services state.
• Services state should be not be changed now (already
configured during run phase).
!
• Providers now cannot be injected.
{ Providers/Services/Factories }
29. Testing { Providers/Services/Factories }
describe('myHttp', function() {
!
var mockQ = {
then: function(){}
},
http;
beforeEach(module(function($provide) {
$provide.value('$q', mockQ);
}));
beforeEach(inject(function(myHttp) {
http = myHttp;
}));
describe('#get()', function() {
it('should return a promise', function() {
// test your code here
});
});
});
30. { Wrappers }
angular.module('myModule')
.provider('someProvider', ['$window', someProvider]);
function someProvider($window, $) {
this.$get = function() {
$window.alert('hey there');
}
}
!
• Angular provides wrappers to common global objects.
• It allows to easily test global properties without having to
monkey patch the window object.
!
• Wrappers are injected with dependency injection.