"I see eyes in my soup": How Delivery Hero implemented the safety system for ...
Intro To JavaScript Unit Testing - Ran Mizrahi
1. Introduction to
{ JavaScript Testing }
Ran Mizrahi (@ranm8)
Founder and CEO @ CoCycles
2. 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.
3. Why Do Software Projects { Fail } ?
Production Maintenance
• Deliver late or over budget.
• Deliver the wrong thing.
• Unstable in production.
• Expensive maintenance.
• Long adjustment to market
needs.
• Long development cycles.
5. Untestable { 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);
});
});
}
6. 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 } ?
7. { Test-Driven Development } To The Rescue
Methodology for using automated unit
tests to drive software design, quality
and stability.
8. { 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.
9. Seems Great But How Much Longer Does { TDD Take } ?
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.
10. The { Three Rules } of TDD
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.
11. { BDD } Behaviour-Driven Development
What exactly are we testing?!
Test-Driven Development
12. { 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...
13. { 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
"""
14. Main { Test Types }
• Unit Testing
• Integration Testing
• Functional Testing
15. { Challenges } Testing JavaScript
• 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.)
16. TDD/BDD Using { Mocha and ChaiJS }
Mocha
Mocha is a feature-rich JavaScript test frameworks running on
node and the browser, making asynchronies tests easy.
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.).
17. TDD/BDD Using { Mocha and ChaiJS }
ChaiJS
Chai is a BDD / TDD assertion library for node and the browser
that can be paired with any JavaScript testing framework.
Main features:
• BDD/TDD style.
• Compatible with all test frameworks.
• Both node.js and browser compatible.
• Standalone assertion library.
• Extendable
18. TDD/BDD Using { Mocha and ChaiJS }
Installing Mocha and Chai
Install mocha globally using npm:
$ npm install mocha -g
Install Chai (Locally):
$ npm install chai
19. TDD/BDD Using { Mocha and ChaiJS }
“Normal” test:
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);
});
});
});
Run it..
$ mocha --reporter spec
Array
#indexOf()
✓ Expect -1 when the value is not present
1 test complete (5 ms)
20. 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...
22. First, Let’s { Write The Tests }
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);
});
});
}
23. 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.
24. 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() {
});
});
});
27. Implement { Our Unit Tests }
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!!!');
});
});
});
29. Running The { Tests }
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.)
30. Benefits of { Testing The Code }
• 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.