Testing Javascript from a
Frontend perspective
Frederic Cabassut - Campanda
The basics of test-driven development
TDD Cycle - Rule #1
You must write a failing test
before you write any production
code.
TDD Cycle - Rule #2
You must not write more of a
test than is sufficient to fail, or
fail to compile.
TDD Cycle - Rule #3
You must not write more
production code than is
sufficient to make the currently
failing test pass.
TDD Cycle
// tests // production code
TDD Cycle
// tests
var expected = "Hello, Fred!";
// production code
TDD Cycle
// tests
var expected = "Hello, Fred!";
// production code
✓
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
// production code
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
// production code
Uncaught ReferenceError: getMyObj is not defined
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
// production code
function getMyObj() {
}
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
// production code
function getMyObj() {
}
Uncaught TypeError: Cannot read property 'greet' of undefined
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
// production code
function getMyObj() {
return {};
}
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
// production code
function getMyObj() {
return {};
}
Uncaught TypeError: getMyObj(...).greet is not a function
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
// production code
function getMyObj() {
return {
greet: function() {
}
};
}
}
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
// production code
function getMyObj() {
return {
greet: function () {
}
};
}
✓
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
assert.equal(expected, actual);
// production code
function getMyObj() {
return {
greet: function() {
}
};
}
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
assert.equal(expected, actual);
// production code
function getMyObj() {
return {
greet: function() {
}
};
}
AssertionError: "Hello, Fred!" == undefined
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
assert.equal(expected, actual);
// production code
function getMyObj() {
return {
greet: function() {
return "Hello, Fred!";
}
};
}
TDD Cycle
// tests
var expected = "Hello, Fred!";
var actual = getMyObj().greet("Fred");
assert.equal(expected, actual);
// production code
function getMyObj() {
return {
greet: function() {
return "Hello, Fred!";
}
};
}
✓
Why Testing ?
- Increase code quality
- Deploy with confidence
- Self-explaining documentation
- Easy to refactor
What do we have to test ?
- User interaction / events
- Manipulating the DOM
- Client-side business logic
- Ajax request (retrieve/send data)
My favorite testing stack:
- Mochajs https://mochajs.org/
- Chaijs http://chaijs.com/
- Sinonjs: http://sinonjs.org/
- My browser (+ livereload)
Let’s create a DatePicker!
Let’s create a DatePicker!
Let’s create a DatePicker!
Let’s create a DatePicker!
Let’s create a DatePicker!
User interaction / events
// tests
it('should initialize a datepicker with
a given element', function() {
var input =
document.createElement('input');
DatePicker.init(input);
});
// production code
window.DatePicker = {
init: function() {}
}
User interaction / events
// tests
it('should initialize a datepicker with
a given element', function() {
var input =
document.createElement('input');
DatePicker.init(input);
});
// production code
window.DatePicker = {
init: function() {}
}
✓
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
});
// production code
window.DatePicker = {
init: function() {}
}
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
var input =
document.createElement('input');
DatePicker.init(input);
});
// production code
window.DatePicker = {
init: function() {}
}
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
var input =
document.createElement('input');
DatePicker.init(input);
});
// production code
window.DatePicker = {
init: function() {}
}
✓
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
[...]
});
// production code
window.DatePicker = {
init: function() {}
}
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
[...]
var event = new Event('focus');
input.dispatchEvent(event);
});
// production code
window.DatePicker = {
init: function() {}
}
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
[...]
var event = new Event('focus');
input.dispatchEvent(event);
});
// production code
window.DatePicker = {
init: function() {}
}
✓
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
[...]
var event = new Event('focus');
input.dispatchEvent(event);
var element =
document.getElementById('datePicker');
expect(element).to.be.ok;
});
// production code
window.DatePicker = {
init: function() {}
}
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
[...]
var event = new Event('focus');
input.dispatchEvent(event);
var element =
document.getElementById('datePicker');
expect(element).to.be.ok;
});
// production code
window.DatePicker = {
init: function() {}
}
AssertionError: expected null to be truthy
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
[...]
var event = new Event('focus');
input.dispatchEvent(event);
var element =
document.getElementById('datePicker');
expect(element).to.be.ok;
});
// production code
window.DatePicker = {
init: function(input) {
[...]
}
}
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
[...]
var event = new Event('focus');
input.dispatchEvent(event);
var element =
document.getElementById('datePicker');
expect(element).to.be.ok;
});
// production code
[...]
input.addEventListener('focus',
function() {
var element =
document.createElement('div');
element.id = 'datePicker';
document.body.appendChild(element);
}
[...]
User interaction / events
// tests
it('should show a datepicker on input
focus', function() {
[...]
var event = new Event('focus');
input.dispatchEvent(event);
var element =
document.getElementById('datePicker');
expect(element).to.be.ok;
});
// production code
[...]
input.addEventListener('focus',
function() {
var element =
document.createElement('div');
element.id = 'datePicker';
document.body.appendChild(element);
}
[...]
✓
User interaction / events
// tests
it('should stop event propagation',
function() {
});
// production code
[...]
input.addEventListener('focus',
function() {
var element =
document.createElement('div');
element.id = 'datePicker';
document.body.appendChild(element);
}
[...]
User interaction / events
// tests
it('should stop event propagation',
function() {
[...]
var event = new Event('focus');
var spy = sinon.spy(event,
'stopPropagation');
input.dispatchEvent(event);
expect(spy).to.have.been.calledOnce;
});
// production code
[...]
input.addEventListener('focus',
function() {
var element =
document.createElement('div');
element.id = 'datePicker';
document.body.appendChild(element);
}
[...]
User interaction / events
// tests
it('should stop event propagation',
function() {
[...]
var event = new Event('focus');
var spy = sinon.spy(event,
'stopPropagation');
input.dispatchEvent(event);
expect(spy).to.have.been.calledOnce;
});
// production code
[...]
input.addEventListener('focus',
function() {
var element =
document.createElement('div');
element.id = 'datePicker';
document.body.appendChild(element);
}
[...]
expected stopPropagation to have been called exactly once ...
User interaction / events
// tests
it('should stop event propagation',
function() {
[...]
var event = new Event('focus');
var spy = sinon.spy(event,
'stopPropagation');
input.dispatchEvent(event);
expect(spy).to.have.been.calledOnce;
});
// production code
[...]
input.addEventListener('focus',
function(e) {
e.stopPropagation();
var element =
document.createElement('div');
element.id = 'datePicker';
document.body.appendChild(element);
}
[...]
User interaction / events
// tests
it('should stop event propagation',
function() {
[...]
var event = new Event('focus');
var spy = sinon.spy(event,
'stopPropagation');
input.dispatchEvent(event);
expect(spy).to.have.been.calledOnce;
});
// production code
[...]
input.addEventListener('focus',
function(e) {
e.stopPropagation();
var element =
document.createElement('div');
element.id = 'datePicker';
document.body.appendChild(element);
}
[...]
✓
Manipulating the DOM
// tests
it('should add class "selected" on a day when it is clicked',
function(){
});
Manipulating the DOM
// tests
it('should add class "selected" on a day when it is clicked',
function(){
[...]
var day = document.querySelector('#datePicker .day');
day.click();
});
Manipulating the DOM
// tests
it('adds class "selected" on a day when clicked', function() {
[...]
var day = document.querySelector('#datePicker .day');
day.click();
var i = day.className.indexOf('selected');
expect(i > -1).to.be.true;
});
Mocking Time, tick tick tick
// tests
Mocking Time
// tests
it('should hide datePicker after 300ms', function(){
[...]
var clock = sinon.useFakeTimers();
day.click();
clock.tick(299);
expect(datePicker.className.indexOf('hidden') > -1).to.be.false;
clock.tick(2);
expect(datePicker.className.indexOf('hidden') > -1).to.be.true;
clock.restore();
});
Mocking Browser methods
// tests
Mocking Browser methods
// tests
beforeEach(function() {
this.dateNowStub = sinon.stub(Date, 'now', function() {
return new Date('2016-01-13').getTime();
});
});
afterEach(function(){
this.dateNowStub.restore();
});
Testing Javascript for the Browser
In a browser
Take it to the next level
Headless browser testing (phantomjs, zombie, electron...)
Continuous Integration: automate your tests in jenkins, travis…
A few more tips
- Find a BUG ? Write a TEST!
- Do TDD while PAIRING
- Use the Web APIs in your TESTS
- Don’t try to test the browser
- You don’t need to test libraries
THANK YOU
And HAPPY Testing!

Testing javascript in the frontend

  • 1.
    Testing Javascript froma Frontend perspective Frederic Cabassut - Campanda
  • 2.
    The basics oftest-driven development
  • 3.
    TDD Cycle -Rule #1 You must write a failing test before you write any production code.
  • 4.
    TDD Cycle -Rule #2 You must not write more of a test than is sufficient to fail, or fail to compile.
  • 5.
    TDD Cycle -Rule #3 You must not write more production code than is sufficient to make the currently failing test pass.
  • 6.
    TDD Cycle // tests// production code
  • 7.
    TDD Cycle // tests varexpected = "Hello, Fred!"; // production code
  • 8.
    TDD Cycle // tests varexpected = "Hello, Fred!"; // production code ✓
  • 9.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); // production code
  • 10.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); // production code Uncaught ReferenceError: getMyObj is not defined
  • 11.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); // production code function getMyObj() { }
  • 12.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); // production code function getMyObj() { } Uncaught TypeError: Cannot read property 'greet' of undefined
  • 13.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); // production code function getMyObj() { return {}; }
  • 14.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); // production code function getMyObj() { return {}; } Uncaught TypeError: getMyObj(...).greet is not a function
  • 15.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); // production code function getMyObj() { return { greet: function() { } }; } }
  • 16.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); // production code function getMyObj() { return { greet: function () { } }; } ✓
  • 17.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); assert.equal(expected, actual); // production code function getMyObj() { return { greet: function() { } }; }
  • 18.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); assert.equal(expected, actual); // production code function getMyObj() { return { greet: function() { } }; } AssertionError: "Hello, Fred!" == undefined
  • 19.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); assert.equal(expected, actual); // production code function getMyObj() { return { greet: function() { return "Hello, Fred!"; } }; }
  • 20.
    TDD Cycle // tests varexpected = "Hello, Fred!"; var actual = getMyObj().greet("Fred"); assert.equal(expected, actual); // production code function getMyObj() { return { greet: function() { return "Hello, Fred!"; } }; } ✓
  • 21.
    Why Testing ? -Increase code quality - Deploy with confidence - Self-explaining documentation - Easy to refactor
  • 22.
    What do wehave to test ? - User interaction / events - Manipulating the DOM - Client-side business logic - Ajax request (retrieve/send data)
  • 23.
    My favorite testingstack: - Mochajs https://mochajs.org/ - Chaijs http://chaijs.com/ - Sinonjs: http://sinonjs.org/ - My browser (+ livereload)
  • 24.
    Let’s create aDatePicker!
  • 25.
    Let’s create aDatePicker!
  • 26.
    Let’s create aDatePicker!
  • 27.
    Let’s create aDatePicker!
  • 28.
    Let’s create aDatePicker!
  • 29.
    User interaction /events // tests it('should initialize a datepicker with a given element', function() { var input = document.createElement('input'); DatePicker.init(input); }); // production code window.DatePicker = { init: function() {} }
  • 30.
    User interaction /events // tests it('should initialize a datepicker with a given element', function() { var input = document.createElement('input'); DatePicker.init(input); }); // production code window.DatePicker = { init: function() {} } ✓
  • 31.
    User interaction /events // tests it('should show a datepicker on input focus', function() { }); // production code window.DatePicker = { init: function() {} }
  • 32.
    User interaction /events // tests it('should show a datepicker on input focus', function() { var input = document.createElement('input'); DatePicker.init(input); }); // production code window.DatePicker = { init: function() {} }
  • 33.
    User interaction /events // tests it('should show a datepicker on input focus', function() { var input = document.createElement('input'); DatePicker.init(input); }); // production code window.DatePicker = { init: function() {} } ✓
  • 34.
    User interaction /events // tests it('should show a datepicker on input focus', function() { [...] }); // production code window.DatePicker = { init: function() {} }
  • 35.
    User interaction /events // tests it('should show a datepicker on input focus', function() { [...] var event = new Event('focus'); input.dispatchEvent(event); }); // production code window.DatePicker = { init: function() {} }
  • 36.
    User interaction /events // tests it('should show a datepicker on input focus', function() { [...] var event = new Event('focus'); input.dispatchEvent(event); }); // production code window.DatePicker = { init: function() {} } ✓
  • 37.
    User interaction /events // tests it('should show a datepicker on input focus', function() { [...] var event = new Event('focus'); input.dispatchEvent(event); var element = document.getElementById('datePicker'); expect(element).to.be.ok; }); // production code window.DatePicker = { init: function() {} }
  • 38.
    User interaction /events // tests it('should show a datepicker on input focus', function() { [...] var event = new Event('focus'); input.dispatchEvent(event); var element = document.getElementById('datePicker'); expect(element).to.be.ok; }); // production code window.DatePicker = { init: function() {} } AssertionError: expected null to be truthy
  • 39.
    User interaction /events // tests it('should show a datepicker on input focus', function() { [...] var event = new Event('focus'); input.dispatchEvent(event); var element = document.getElementById('datePicker'); expect(element).to.be.ok; }); // production code window.DatePicker = { init: function(input) { [...] } }
  • 40.
    User interaction /events // tests it('should show a datepicker on input focus', function() { [...] var event = new Event('focus'); input.dispatchEvent(event); var element = document.getElementById('datePicker'); expect(element).to.be.ok; }); // production code [...] input.addEventListener('focus', function() { var element = document.createElement('div'); element.id = 'datePicker'; document.body.appendChild(element); } [...]
  • 41.
    User interaction /events // tests it('should show a datepicker on input focus', function() { [...] var event = new Event('focus'); input.dispatchEvent(event); var element = document.getElementById('datePicker'); expect(element).to.be.ok; }); // production code [...] input.addEventListener('focus', function() { var element = document.createElement('div'); element.id = 'datePicker'; document.body.appendChild(element); } [...] ✓
  • 42.
    User interaction /events // tests it('should stop event propagation', function() { }); // production code [...] input.addEventListener('focus', function() { var element = document.createElement('div'); element.id = 'datePicker'; document.body.appendChild(element); } [...]
  • 43.
    User interaction /events // tests it('should stop event propagation', function() { [...] var event = new Event('focus'); var spy = sinon.spy(event, 'stopPropagation'); input.dispatchEvent(event); expect(spy).to.have.been.calledOnce; }); // production code [...] input.addEventListener('focus', function() { var element = document.createElement('div'); element.id = 'datePicker'; document.body.appendChild(element); } [...]
  • 44.
    User interaction /events // tests it('should stop event propagation', function() { [...] var event = new Event('focus'); var spy = sinon.spy(event, 'stopPropagation'); input.dispatchEvent(event); expect(spy).to.have.been.calledOnce; }); // production code [...] input.addEventListener('focus', function() { var element = document.createElement('div'); element.id = 'datePicker'; document.body.appendChild(element); } [...] expected stopPropagation to have been called exactly once ...
  • 45.
    User interaction /events // tests it('should stop event propagation', function() { [...] var event = new Event('focus'); var spy = sinon.spy(event, 'stopPropagation'); input.dispatchEvent(event); expect(spy).to.have.been.calledOnce; }); // production code [...] input.addEventListener('focus', function(e) { e.stopPropagation(); var element = document.createElement('div'); element.id = 'datePicker'; document.body.appendChild(element); } [...]
  • 46.
    User interaction /events // tests it('should stop event propagation', function() { [...] var event = new Event('focus'); var spy = sinon.spy(event, 'stopPropagation'); input.dispatchEvent(event); expect(spy).to.have.been.calledOnce; }); // production code [...] input.addEventListener('focus', function(e) { e.stopPropagation(); var element = document.createElement('div'); element.id = 'datePicker'; document.body.appendChild(element); } [...] ✓
  • 47.
    Manipulating the DOM //tests it('should add class "selected" on a day when it is clicked', function(){ });
  • 48.
    Manipulating the DOM //tests it('should add class "selected" on a day when it is clicked', function(){ [...] var day = document.querySelector('#datePicker .day'); day.click(); });
  • 49.
    Manipulating the DOM //tests it('adds class "selected" on a day when clicked', function() { [...] var day = document.querySelector('#datePicker .day'); day.click(); var i = day.className.indexOf('selected'); expect(i > -1).to.be.true; });
  • 50.
    Mocking Time, ticktick tick // tests
  • 51.
    Mocking Time // tests it('shouldhide datePicker after 300ms', function(){ [...] var clock = sinon.useFakeTimers(); day.click(); clock.tick(299); expect(datePicker.className.indexOf('hidden') > -1).to.be.false; clock.tick(2); expect(datePicker.className.indexOf('hidden') > -1).to.be.true; clock.restore(); });
  • 52.
  • 53.
    Mocking Browser methods //tests beforeEach(function() { this.dateNowStub = sinon.stub(Date, 'now', function() { return new Date('2016-01-13').getTime(); }); }); afterEach(function(){ this.dateNowStub.restore(); });
  • 54.
    Testing Javascript forthe Browser In a browser
  • 55.
    Take it tothe next level Headless browser testing (phantomjs, zombie, electron...) Continuous Integration: automate your tests in jenkins, travis…
  • 56.
    A few moretips - Find a BUG ? Write a TEST! - Do TDD while PAIRING - Use the Web APIs in your TESTS - Don’t try to test the browser - You don’t need to test libraries
  • 57.