Intro To JavaScript Unit Testing - Ran Mizrahi
-
Upload
ran-mizrahi -
Category
Technology
-
view
778 -
download
2
Transcript of Intro To JavaScript Unit Testing - Ran Mizrahi
Introduction to { JavaScript Testing }
Ran Mizrahi (@ranm8)Founder and CEO @ CoCycles
About { Me }
• Founder and CEO of CoCycles.
• Former Open Source Dpt. Leader of CodeOasis.
• Architected and lead the development of the Wix App Market.
• Full-stack and hands-on software engineer.
• Deliver late or over budget.
• Deliver the wrong thing.
• Unstable in production.
Production Maintenance
• Expensive maintenance.
• Long adjustment to market needs.
• Long development cycles.
Why Do Software Projects { Fail } ?
Why Do Software Projects { Fail } ?
function createUser(properties) { var user = { firstName: properties.firstName, lastName: properties.lastName, username: properties.username, mail: properties.mail }; var fullName = User.firstName + ' ' + User.lastName;
// Make sure user is valid if (!user.firstName || !user.lastName) { throw new Error('First or last name are not valid!'); } else if(typeof user.mail === 'string' && user.mail.match(new RegExp(/^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/)) === null) { throw new Error('Mail is not valid'); } else if (!user.username) { throw new Error('Username is not valid'); }
$.post('/user', { fullName: fullName, userName: user.username, mail: user.mail }, function(data) { var message; if (data.code === 200) { message = 'User saved successfully!'; } else { message = 'Operation was failed!'; }
$('#some-div').animate({ 'margin-left': $(window).width() }, 1000, function() { $(this).html(message); }); }); }
Untestable { Code }…
The problems with untestable code:
• Tightly coupled.
• No separation of concerns.
• Not readable.
• Not predictable.
• Global states.
• Long methods.
• Large classes/objects.
>
• Hard to maintain.
• High learning curve.
• Stability issues.
• You can never expect problems before they occur
Why Test Your { Code } ?
Methodology for using automated unit tests to drive software design, quality
and stability.
{ Test-Driven Development } To The Rescue
How it’s done :
• First the developer writes a failing test case that defines a desired functionality to the software.
• Makes the code pass those tests.
• Refactor the code to meet standards.
{ Test-Driven Development } To The Rescue
My experience:• Initial progress will be slower.• Greater consistency.• Long tern cost is drastically
lower• After getting used to it, you
can write TDD faster (-:
Studies:• Takes 15-30% longer.• 45-80% less bugs.• Fixing bugs later on is
dramatically faster.
Seems Great But How Much Longer Does { TDD Take } ?
Rule #1 Your code should always fail before you implement the code
Rule #2 Implement the simplest code possible to pass your tests.
Rule #3 Refactor, refactor and refractor - There is no shame in refactoring.
The { Three Rules } of TDD
Test-Driven Development
What exactly are we testing?!
{ BDD } Behaviour-Driven Development
• Originally started in 2003 by Dan North, author of JBehave, the first BDD tool.
• Based on the TDD methodology.
• Aims to provide tools for both developers and business (e.g. product manager, etc.) to share development process together.
• The steps of BDD :• Developers and business personas write specification together.• Developer writes tests based on specs and make them fail.• Write code to pass those tests.• Refactor, refactor, refactor...
{ BDD } Behaviour-Driven Development
Feature: ls In order to see the directory structure As a UNIX user I need to be able to list the current directory's contents
Scenario: List 2 files in a directory Given I am in a directory "test" And I have a file named "foo" And I have a file named "bar" When I run "ls" Then I should get: """ bar foo """
{ BDD } Behaviour-Driven Development
• Unit Testing
• Integration Testing
• Functional Testing
Main { Test Types }
• Async tests:
• Testing async methods can be tricky.• Define tests timeout.• Indicate when test is completed in callback.• Assert on callback.
• DOM:
• Testing DOM is a difficult task.• The key is to separate your controller and model logic from
DOM and test those only.• Testing DOM is done using functional testing (e.g. WebDriver,
etc.)
{ Challenges } Testing JavaScript
Mocha is a feature-rich JavaScript test frameworks running on node and the browser, making asynchronies tests easy.
Mocha
Main features:• Supports both TDD and BDD styles.
• Simple async testing.
• Both browser and node support.
• Proper exit status for CI support.
• node.js debugger support.
• Highly flexible, choose and join the pieces yourself (spy library, assertion library, etc.).
TDD/BDD Using { Mocha and ChaiJS }
Chai is a BDD / TDD assertion library for node and the browser that can be paired with any JavaScript testing framework.
ChaiJS
Main features:
• BDD/TDD style.
• Compatible with all test frameworks.
• Both node.js and browser compatible.
• Standalone assertion library.
• Extendable
TDD/BDD Using { Mocha and ChaiJS }
Installing Mocha and Chai
$ npm install mocha -g
$ npm install chai
Install mocha globally using npm:
Install Chai (Locally):
TDD/BDD Using { Mocha and ChaiJS }
var expect = require(‘chai').expect;
describe('Array', function() { describe('#indexOf()', function() { it('expect -1 when the value is not present', function() { var array = [1, 2, 3]; expect(array.indexOf(4)).to.be(-1); }); }); });
“Normal” test:
Run it..$ mocha --reporter spec Array #indexOf() ✓ Expect -1 when the value is not present
1 test complete (5 ms)
TDD/BDD Using { Mocha and ChaiJS }
Async test:var expect = require(‘chai').expect;
function asyncCall(val ,callback) { var prefix = ' - ';
setTimeout(function() { var newString = val + prefix + 'OK';
callback(newString); }, 500); }
describe('asyncCall', function() { it('Add suffix that prefixed with - to the given string', function(done) { var testVal = 'Foo';
asyncCall(testVal, function(response) { expect(response).to.contain(testVal + ' - OK'); done(); }); }); });
Let’s run it...
TDD/BDD Using { Mocha and ChaiJS }
Back To { Our Code }
function createUser(properties) { var user = { firstName: properties.firstName, lastName: properties.lastName, username: properties.username, mail: properties.mail }; var fullName = User.firstName + ' ' + User.lastName;
// Make sure user is valid if (!user.firstName || !user.lastName) { throw new Error('First or last name are not valid!'); } else if(typeof user.mail === 'string' && user.mail.match(new RegExp(/^\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,3}$/)) === null) { throw new Error('Mail is not valid'); } else if (!user.username) { throw new Error('Username is not valid'); }
$.post('/user', { fullName: fullName, userName: user.username, mail: user.mail }, function(data) { var message; if (data.code === 200) { message = 'User saved successfully!'; } else { message = 'Operation was failed!'; }
$('#some-div').animate({ 'margin-left': $(window).width() }, 1000, function() { $(this).html(message); }); }); }
First, Let’s { Write The Tests }
What to test in our case:
• Full name concatenation.
• API call data.
• Request callback.
What not to test :
• DOM manipulations - Functional testing (e.g. WebDriver).
• API requests - Integration testing.
First, Let’s { Write The Tests }
First, Let’s { Write The Unit Tests }
describe('#saveUser()', function() {
it('should call http service with method POST, path /user, and the user object', function() { });
it('should compose the full name in to the user object', function() { });
it('should only return the payload from the response object', function() { }); }); });
The { Implementation }
function userService($http, baseUrl) { baseUrl = baseUrl || 'http://google.com/api'; function composeFullName(firstName, lastName) { return firstName + ' ' + lastName; } function returnPayload(response) { return response.payload; } function execute(path, body, method) { return $http({ url: baseUrl + path, method: method || 'GET', data: body }); } return { saveUser: function(user) { user.fullName = composeFullName(user.firstName, user.lastName); return execute('/user', user, 'POST').then(returnPayload); } }; }
Implement { Our Unit Tests }
describe('user service', function() {
var userService, httpMock, thenFunc;
function createHttpMock() { thenFunc = sinon.stub(); httpMock = sinon.stub().returns({ then: thenFunc }); }
beforeEach(function() { createHttpMock();
userService = UserService(httpMock); });
function getUser() { return { firstName: 'Ran', lastName: 'Mizrahi' }; }
describe('#saveUser()', function() { var user;
beforeEach(function() { user = getUser(); userService.saveUser(user); });
it('should call http service with method POST, path /user, and the user object', function() { expect(httpMock).to.have.been.calledWith({ url: 'http://google.com/api/user', method: 'POST', data: user }); });
it('should compose the full name in to the user object', function() { expect(user.fullName).to.equal('Ran Mizrahi'); });
it('should only return the payload from the response object', function() { var returnPayload = thenFunc.args[0][0];
expect(returnPayload({ payload: 'Hello!!!' })).to.equal('Hello!!!'); }); }); });
Implement { Our Unit Tests }
Run Those { Tests Again }
mocha tests can run in different environments and formats:
• Browser - using mocha.js (see example)• For CI automation use JSTestDriver.
• CLI - as demonstrated before using the “mocha” command.
• CI (e.g. xunit) - $ mocha test/asyncTest.js --reporter xunit.
• Many other formats (JSON, HTML, list, Spec, etc.)
Running The { Tests }
• Short feedback/testing cycle.
• High code coverage of tests that can be at run any time to provide feedback that the software is functioning.
• Provides detailed spec/docs of the application.
• Less time spent on debugging and refactoring.
• Know what breaks early on.
• Enforces code quality and simplicity.
• Helps separating units to responsibilities.
Benefits of { Testing The Code }
Questions?Thank you!