How Testability Inspires AngularJS Design / Ran Mizrahi
-
Upload
ran-mizrahi -
Category
Technology
-
view
212 -
download
0
description
Transcript of How Testability Inspires AngularJS Design / Ran Mizrahi
ng-conf 2014 Israel big thanks to our sponsors:
How { Testability } Inspires AngularJS Design
Ran Mizrahi (@ranm8)Founder and CEO @ CoCycles
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.
What is { Testability } ?
“AngularJS - Web Framework Designed with { Testability } in Mind “
How many times have you heard that?
What is { Testability } ?
Why is { Testability } so important???
What is { Testability } ?
“Software testability is the degree to which a software artifact supports testing
in a given test context”— Wikipedia
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.
{ 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:
{ Global } State
“Insanity: doing the same thing over and over again and expecting different results.”
— Einstien
{ 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
{ 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).
{ 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)
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.
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
(-:
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.
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.
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
{ Testability } is Good!
Testability === Code Quality
Polymorphic
Encapsulated
Decoupled
{ Maintainable } and { Predictable } Code
{ Testability } in AngularJS
So, What Makes AngularJS { Testable} ???
{ 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.
{ 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); }
{ 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); }
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>
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 }); });
{ 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)); }); }; });
{ 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'); }); });
{ 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... }]; }
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 }
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 }); }); });
{ 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.
Any { Questions } ?