how testability inspires angularjs design / ran mizrahi

31
ng-conf 2014 Israel big thanks to our sponsors:

Upload: ran-mizrahi

Post on 10-May-2015

212 views

Category:

Technology


0 download

DESCRIPTION

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.

TRANSCRIPT

Page 1: How Testability Inspires AngularJS Design / Ran Mizrahi

ng-conf 2014 Israel big thanks to our sponsors:

Page 2: How Testability Inspires AngularJS Design / Ran Mizrahi

How { Testability } Inspires AngularJS Design

Ran Mizrahi (@ranm8)Founder and CEO @ CoCycles

Page 3: How Testability Inspires AngularJS Design / Ran Mizrahi

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.

Page 4: How Testability Inspires AngularJS Design / Ran Mizrahi

What is { Testability } ?

“AngularJS - Web Framework Designed with { Testability } in Mind “

How many times have you heard that?

Page 5: How Testability Inspires AngularJS Design / Ran Mizrahi

What is { Testability } ?

Why is { Testability } so important???

Page 6: How Testability Inspires AngularJS Design / Ran Mizrahi

What is { Testability } ?

“Software testability is the degree to which a software artifact supports testing

in a given test context”— Wikipedia

Page 7: How Testability Inspires AngularJS Design / Ran Mizrahi

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.

Page 8: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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:

Page 9: How Testability Inspires AngularJS Design / Ran Mizrahi

{ Global } State

“Insanity: doing the same thing over and over again and expecting different results.”

— Einstien

Page 10: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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

Page 11: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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).

Page 12: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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)

Page 13: How Testability Inspires AngularJS Design / Ran Mizrahi

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.

Page 14: How Testability Inspires AngularJS Design / Ran Mizrahi

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

(-:

Page 15: How Testability Inspires AngularJS Design / Ran Mizrahi

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.

Page 16: How Testability Inspires AngularJS Design / Ran Mizrahi

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.

Page 17: How Testability Inspires AngularJS Design / Ran Mizrahi

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

Page 18: How Testability Inspires AngularJS Design / Ran Mizrahi

{ Testability } is Good!

Testability === Code Quality

Polymorphic

Encapsulated

Decoupled

{ Maintainable } and { Predictable } Code

Page 19: How Testability Inspires AngularJS Design / Ran Mizrahi

{ Testability } in AngularJS

So, What Makes AngularJS { Testable} ???

Page 20: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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.

Page 21: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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); }

Page 22: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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); }

Page 23: How Testability Inspires AngularJS Design / Ran Mizrahi

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>

Page 24: How Testability Inspires AngularJS Design / Ran Mizrahi

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 }); });

Page 25: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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)); }); }; });

Page 26: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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'); }); });

Page 27: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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... }]; }

Page 28: How Testability Inspires AngularJS Design / Ran Mizrahi

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 }

Page 29: How Testability Inspires AngularJS Design / Ran Mizrahi

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 }); }); });

Page 30: How Testability Inspires AngularJS Design / Ran Mizrahi

{ 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.

Page 31: How Testability Inspires AngularJS Design / Ran Mizrahi

Any { Questions } ?