The discovery of unit testing and test-driven development was one of the most important parts of my growth as a developer. The ability to write simple, small pieces of code that could verify the behavior of my application was in itself quite useful. And the ability to refactor without fear, just by running the test suite, changed how I program. But the real benefits come in how unit tests shape your application code: more testable code is often more well thought-out, more decoupled, and more extensible.
In this talk, I'll give a whirlwind introduction to unit testing as a concept and as a practice. I want you fully convinced it's the best thing to happen to software development, if you aren't already. Once we're on the same page there, I'll take a deep dive into what makes a good unit test. This involves testing tools such as spies, stubs, and mocks, concepts like code coverage, and practices like dependency injection that shape your application code. The most important lesson will be on how to focus on singular, isolated units of code in your testing, as this guides you toward building modular, flexible, and comprehensible applications.
10. q: what is a unit?
a: a single function or method
@domenic
11. A unit test is an automated piece of code
that invokes a function and then checks
assumptions about its logical behavior.
@domenic
12. // Arrange
var excerpt = "A unit test is an automated piece of code.";
var highlights = [{ start: 2, length: 4, color: "yellow" }];
// Act
var result = highlight(excerpt, highlights);
// Assert
expect(result).to.equal('A <span class="highlight yellow">' +
'unit</span> test is an automated ' +
'piece of code.'); @domenic
13. q: how big should a unit be?
a: about ten lines
@domenic
18. the most compelling reasoning i’ve
seen comes from this guy
http://butunclebob.com/ArticleS.UncleBob.TheSensitivityProblem @domenic
19. “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
20. “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
21. “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
22. ok, but why unit test all the things?
@domenic
23. 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
}
24. more generally:
A C E
Input Output
B D F
http://stackoverflow.com/a/11917341/3191
@domenic
29. the most important thing to remember:
your tests should only test your code.
@domenic
30. corollary: in the end, it’s all about
managing dependencies
@domenic
31. 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
32. 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
33. 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
34. 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
@domenic
expect(spy).to.have.been.calledWith(book);
35. bdd: an even gentler introduction
https://gist.github.com/3399842
@domenic
36. 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
37. 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
39. dependency injection to the rescue!
function Downloader(isOnline) {
this.download = function (book, cb) {
if (!isOnline()) {
cb(new Error("No internet!"));
return;
}
// ...
};
}
@domenic
40. app code becomes:
var downloader = new Downloader(function () { return navigator.onLine; });
@domenic
41. test code becomes:
// Arrange
function isOnline() { return false; }
var downloader = new Downloader(isOnline);
var book = { id: "123" };
// …
@domenic
49. 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
}
50. when given a highlight and an excerpt
it should return the excerpt with highlighting tags inserted
@domenic
51. 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
52. ✓ 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
✓ }
53. q: how can we achieve 100% coverage?
a: use test-driven development
@domenic
54. 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
60. 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
61. ✓ 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
62. 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
63. ✓ 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
✓ }
65. ✓ 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
66. 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
67. 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