Unit Testing for Great Justice by Domenic Denicola @domenic
Jan 15, 2015
Unit Testing for Great Justiceby Domenic Denicola
@domenic
Domenic Denicola@domenic
https://npmjs.org/profile/domenicdenicola
https://github.com/domenic
https://github.com/NobleJS
q: how do you know your code works?
a: it doesn’t.
@domenic
to make sure something works,
you need to test it.
@domenic
but not manually
@domenic
two levels of automated testing
integration testing
unit testing
@domenic
@domenic
today we’re talking about unit testing:
what
why
how
when
@domenic
what is a unit test?
@domenic
q: what is a unit?
a: a single function or method
@domenic
A unit test is an automated piece of code
that invokes a function and then checks
assumptions about its logical behavior.
@domenic
var excerpt = "A unit test is an automated piece of code.";
var highlights = [{ start: 2, length: 4, color: "yellow" }];
var result = highlight(excerpt, highlights);
expect(result).to.equal('A <span class="highlight yellow">' +
'unit</span> test is an automated ' +
'piece of code.');
// Arrange
// Act
// Assert
@domenic
q: how big should a unit be?
a: about ten lines
@domenic
unit tested functions will:
do one thing
do it correctly
@domenic
q: what code should you unit test?
a: all the code (that has logic)
@domenic
@domenic
why unit test all the things?
@domenic
the most compelling reasoning i’ve
seen comes from this guy
http://butunclebob.com/ArticleS.UncleBob.TheSensitivityProblem @domenic
“Software is a very sensitive domain. If a single bit of a
100MB executable is wrong, the entire application can
be brought to it's knees. Very few other domains suffer
such extreme sensitivity to error. But one very important
domain does: accounting. A single digit error in a
massive pile of spreadsheets and financial statements
can cost millions and bankrupt an organization.”
@domenic
“Accountants solved this problem long ago. They use a
set of practices and disciplines that reduce the
probability that errors can go undetected. One of these
practices is Dual Entry Bookkeeping. Every transaction is
entered twice; once in the credit books, and once in the
debit books. The two entries participate in very different
calculations but eventually result in a final result of zero.
That zero means that the all the entries balance. The
strong implication is that there are no single digit errors.”
@domenic
“We in software have a similar mechanism that provides
a first line of defense: Test Driven Development (TDD).
Every intention is entered in two places: once in a unit
test, and once in the production code. These two entries
follow very different pathways, but eventually sum to a
green bar. That green bar means that the two intents
balance, i.e. the production code agrees with the tests.”
@domenic
ok, but why unit test all the things?
@domenic
function highlight(excerpt, highlights) {
if (highlights.length === 0) {
return excerpt;
}
if (highlightsOverlap(highlights)) {
highlights = subdivideHighlights(highlights);
}
var tags = makeTags(highlights);
var highlighted = insertTags(excerpt, tags);
return highlighted;
}@domenic
more generally:
Input
E
F
Output
C
D
A
B
http://stackoverflow.com/a/11917341/3191@domenic
you also get
confidence
the ability to refactor without fear
credibility
free documentation
@domenic
https://gist.github.com/305ad492c2fd20c466be
https://github.com/senchalabs/connect/blob/gh-pages/tests.md
@domenic
and most importantly, you get
testable code.
@domenic
how do i write testable code?
@domenic
the most important thing to remember:
your tests should only test your code.
@domenic
corollary: in the end, it’s all about
managing dependencies
@domenic
this is why we use mv* patterns
the model is all your code: no dependencies
the view has no logic: no need to test it
the controller (or whatever) has simple logic and is easy to test using fakes
@domenic
this is why we use layered architecture
the domain model only depends on itself
the domain services only depend on the models
the application services only depend on the domain
the infrastructure code is straightforward translation: easy to test
the ui code just ties together application services and views
@domenic
testing the domain model is easy
// Arrange
var shelf = new Shelf();
var book = { id: "123" };
shelf.addBook(book);
// Act
var hasBook = shelf.hasBook("123");
// Assert
expect(hasBook).to.be.true;
@domenic
spies: a gentle introduction
// Arrange
var shelf = new Shelf();
var book = { id: "123" };
var spy = sinon.spy();
shelf.on("bookAdded", spy);
// Act
shelf.addBook(book);
// Assert
expect(spy).to.have.been.calledWith(book);@domenic
bdd: an even gentler introduction
https://gist.github.com/3399842
@domenic
testing services is harder
downloader.download(book)
when the app is offline
it should callback with a “no internet” error
when the app is online
and the DRM service says the user has run out of licenses
it should callback with a “no licenses left” error
and the DRM service says the user can download on this computer
and the download succeeds
it should callback with no error, and the book text
and the download fails
it should callback with the underlying error
@domenic
when the app is offline, it should
callback with a “no internet” error
// Arrange
var downloader = new Downloader();
var book = { id: "123" };
// ??? how to set up "app is offline"?
// Act
downloader.download(book, function (err) {
// Assert
expect(err).to.exist.and.have.property("message", "No internet!");
done();
}); @domenic
untestable Downloader
function Downloader() {
this.download = function (book, cb) {
if (!navigator.onLine) {
cb(new Error("No internet!"));
return;
}
// ...
};
}
@domenic
dependency injection to the rescue!
function Downloader(isOnline) {
this.download = function (book, cb) {
if (!isOnline()) {
cb(new Error("No internet!"));
return;
}
// ...
};
}
@domenic
app code becomes:
var downloader = new Downloader(function () { return navigator.onLine; });
@domenic
test code becomes:
// Arrange
function isOnline() { return false; }
var downloader = new Downloader(isOnline);
var book = { id: "123" };
// …
@domenic
similarly:
function Downloader(isOnline, drmService, doDownloadAjax) {
this.download = function (book, cb) {
// https://gist.github.com/3400303
};
}
@domenic
testing ui is much like testing services, but
now you depend on the dom
@domenic
testing ui code
var TodoView = Backbone.View.extend({
// ... lots of stuff omitted ...
events: {
"dblclick label": "edit"
},
edit: function () {
this.$el.addClass("editing");
this.input.focus();
}
});
https://github.com/addyosmani/todomvc/blob/master/architecture-examples/backbone/js/views/todos.js
@domenic
testing ui code: bad test
setupEntireApplication();
addATodo();
var $todo = $("#todos > li").first();
$todo.find("label").dblclick();
expect($todo.hasClass("editing")).to.be.true;
expect(document.activeElement).to.equal($todo.find(".edit")[0]);
@domenic
testing ui code: good test
var todoView = new TodoView();
todoView.$el = $(document.createElement("div"));
todoView.input = { focus: sinon.spy() };
todoView.edit();
expect(todoView.$el.hasClass("editing")).to.be.true;
expect(todoView.input.focus).to.have.been.called;
@domenic
when should i write my tests?
@domenic
let’s talk about code coverage
@domenic
function highlight(excerpt, highlights) {
if (highlights.length === 0) {
return excerpt;
}
if (highlightsOverlap(highlights)) {
highlights = subdivideHighlights(highlights);
}
var tags = makeTags(highlights);
var highlighted = insertTags(excerpt, tags);
return highlighted;
}@domenic
when given a highlight and an excerpt
it should return the excerpt with highlighting tags inserted
@domenic
var excerpt = "A unit test is an automated piece of code.";
var highlights = [{ start: 2, length: 4, color: "yellow" }];
var result = highlight(excerpt, highlights);
expect(result).to.equal('A <span class="highlight yellow">' +
'unit</span> test is an automated ' +
'piece of code.');@domenic
function highlight(excerpt, highlights) {
if (highlights.length === 0) {
return excerpt;
}
if (highlightsOverlap(highlights)) {
highlights = subdivideHighlights(highlights);
}
var tags = makeTags(highlights);
var highlighted = insertTags(excerpt, tags);
return highlighted;
}
✓
◌
✗
✓
✓
◌
✗
✓
✓
✓
✓
✓
✓
✓@domenic
q: how can we achieve 100% coverage?
a: use test-driven development
@domenic
the three rules of tdd
You are not allowed to write any production code unless
it is to make a failing unit test pass.
You are not allowed to write any more of a unit test than
is sufficient to fail.
You are not allowed to write any more production code
than is sufficient to pass the one failing unit test.
http://butunclebob.com/ArticleS.UncleBob.TheThreeRulesOfTdd@domenic
the three steps of tdd
red
green
refactor
@domenic
when there are no highlights
the excerpt should pass through unchanged
0/1 tests passed @domenic
function highlight(excerpt, highlights) {
if (highlights.length === 0) {
return excerpt;
}
}
@domenic
function highlight(excerpt, highlights) {
if (highlights.length === 0) {
return excerpt;
}
}
✓
◌
✓
✓
✓
@domenic
function highlight(excerpt, highlights) {
return excerpt;
}
✓
✓
✓
1/1 tests passed @domenic
when there are no highlights
the excerpt should pass through unchanged
when there are simple non-overlapping highlights
it should insert tags around those areas
1/2 tests passed @domenic
function highlight(excerpt, highlights) {
if (highlights.length === 0) {
return excerpt;
}
var tags = makeTags(highlights);
var highlighted = insertTags(excerpt, tags);
return highlighted;
}
✓
✓
✓
✓
✓
✓
✓
✓
✓
✓
2/2 tests passed @domenic
when there are no highlights
the excerpt should pass through unchanged
when there are simple non-overlapping highlights
it should insert tags around those substrings
when there are overlapping highlights
it should subdivide them before inserting the tags
2/3 tests passed @domenic
function highlight(excerpt, highlights) {
if (highlights.length === 0) {
return excerpt;
}
if (highlightsOverlap(highlights)) {
highlights = subdivideHighlights(highlights);
}
var tags = makeTags(highlights);
var highlighted = insertTags(excerpt, tags);
return highlighted;
}
✓
✓
✓
✓
✓
✓
✓
✓
✓
✓
✓
✓
✓
✓3/3 tests passed @domenic
@domenic
function highlight(excerpt, highlights) {
if (highlightsOverlap(highlights)) {
highlights = subdivideHighlights(highlights);
}
var tags = makeTags(highlights);
var highlighted = insertTags(excerpt, tags);
return highlighted;
}
✓
✓
✓
✓
✓
✓
✓
✓
✓
✓
3/3 tests still passing! @domenic
summary
Unit tests are automated tests that verify your application’s logic by
breaking it up into small units.
Unit testing is like double-entry bookkeeping. It gives you the ability
to refactor without fear.
Writing unit tests will lead to writing testable code, which is
decoupled via dependency injection and thus becomes more
modular, flexible, and comprehensible.
The best way to write unit tests is with test-driven development,
which has three steps: red, green, refactor. Make these steps as
small as possible.
@domenic
unit-testing tools i like
Mocha test runner: http://mochajs.com
Chai assertion library: http://chaijs.com
Sinon.JS spy/stub/mock library: http://sinonjs.org
Sandboxed-Module environment faker: http://npm.im/sandboxed-module
Cover code coverage tool: http://npm.im/cover
My Chai plugins:
Sinon–Chai: http://npm.im/sinon-chai
Chai as Promised: http://npm.im/chai-as-promised
@domenic