Javscript Test Driven Development
Travelers • Scott Sauyet • June 7, 2011
Javscript Test Driven Development
=================================
### Scott Sauyet ###
How to effectively test Javascript applications. Introducing tools and
techniques to make test-driven development easier in Javascript.
What we'll Discuss
==================
* Limitations of YUI
* Other Test Frameworks, especially JSTestDriver
* Writing tests for existing code
* Writing tests as part of developing code
* Using test suites to comfortably refactor more complex code
* Useful Javascript techniques that differ from other common languages
Not Discussed:
* Functional / Integration Testing
Continuing Example
==================
We're going to investigate one piece of code in depth, looking at issues
with the current implementation, and, noting that it has no unit test,
first writing a test suite. Then we'll do some refactoring and/or
rewriting of the code
`Validators.PolicyEffectiveDate` has a number of problematic features.
We're not picking on the developer in question (who isn't even here today.)
We're just looking at some patterns in development, and how testing may help
us find and clean up code smells.
(iteration 090)
Validators in our Framework
===========================
The Validation framework uses objects with the following method as validators:
> {Object} validate(val)
Such validator objects might also have several other properties explaining when
they are to run or are not to run. The return value should be an object that
has a Boolean `status` property. If that value is false, the object should also
contain a `msg` property for the error message. It can also contain several
other properties not at issue here.
The `Validators` object contains a number of static factory functions to create
such validators. `PolicyEffectiveDate` is one of these. Note that those
validators with zero or one parameter are cached.
Problems with the Existing Code - Question
==========================================
What problems do we see with the existing code?
(see iteration 090 src/Validators.js)
Some Problems with the Existing Code
====================================
* Far too many Date objects constructed on each call
* Many Validator objects are created on each call
* Strings are passed into those Validators only to be discarded
* Validation status objects are created only to be discarded
* Many function calls:
* Four Date constructor calls
* Three calls to Date.setDate
* One call to ui.get
* Four calls to different Validator constructors
* Between one and four calls to various `validate` functions, each
triggering
* One call to compareDate, which triggers
* Another Date constructor call.
The Biggest Problem with the Existing Code
==========================================
**It's confusing!**
* It relies on a number of other Validator objects whose function might or might
not be clear from their names
* It calls these validator objects, but then reverses the sense of their
response without even using a `not` operator.
* It tests for Leap Day on an existing date by calling another of these
Validators rather than just checking month and day.
* The only easy way to understand its behavior is by reading the error messages
it generates.
* There are no unit tests to help us refactor it or to serve as an API guide.
The Plan of Attack
==================
1. Write some unit tests to capture existing functionality
2. Build whatever helper functions are needed to make it easier to rewrite
3. Refactor the code
4. Make sure the tests continue to pass
Along the way, we should learn a fair bit about test-driven development with
Javascript.
----------
A secondary goal of this is to introduce other Javsascript testing tools and
demonstrate how they can be used in place of, or in conjunction with, our
YUI Test code. We're going to start with this.
JsTestDriver
============
* A test harness that makes it easy to test in multiple browsers.
* Works as a little web server serving up the pages and listening for results
from the browsers attached to it. Can be used for many browsers simultaneously.
* Easy to operate in batch, so can quickly be linked in to our build process.
* Can run as a plug-in to WebStorm.
* Has its own assertion framework, but can easily be adapted to QUnit, YUI, or
others.
(iteration 100)
Using JsTestDriver
==================
* Batch mode -- demo
* Plug-in to WebStorm -- demo
Differences between YUI Test and JsTestDriver
=============================================
* Different syntax for assertions: `assertTrue` versus `Y.Assert.isTrue`
* Differences in setup: HTML files for Test Suites or even Test Cases in YUI
versus one config file per high-level setup in JsTestDriver
* Run environments: YUI is manual, but can be automated. JsTestDriver is
automated (in many environments), and is more complicated to run manually,
although the plug-in is straightforward and fast enough to run many tests
quickly.
**Note**: these can be used interchangeably, running YUI-style tests inside
JSTD, and running JSTD-style tests inside a YUI file. We'll discuss this at
the end.
Test-Driven Development
=======================
* Write the test first, before any code needed to pass the test
* Run the test -- make this a habit, even though you know it'll fail.
Sometimes a test will fail for a different reason than you expect.
* Once the test actually fails as expected, write the minimal amount of
code necessary to pass it.
* Write another test
(iterations 110 - 120)
What To test
============
The public API of your code. If there are complexities hidden behind
the public API, those should be factored out into their own subsystems
and tested separately.
* Fundamentals:
* Does this object exist?
* Is this object of the correct type?
* Does this object have the correct properties?
* Simple behavior:
* Does this object behave as expected under normal circumstances?
* Does this object properly handle error conditions?
* Edge cases: all obvious boundary conditions, and others you discover
* Regressions: anything that once works and then stopped
What Not To Test
================
* Implementation details: one big reason for a good test suite is to
allow yourself the flexibility to change implmentation without breaking
your codebase.
* Code too simple to fail, or for which failure is as likely to affect
your tests themselves as production code.
> // Don't bother!
> var a = 10;
> assertEquals(10, a);
* Interrelated components. As best you can, test each component in
isolation, stubbing out as much of the rest of the system as is practical.
How to add Tests to Existing Code
=================================
This is trickier than writing tests first. You can either:
* Go back to the original specs and write tests to match them. The problem
with this is that often the specs have changed, but have not been kept
up-to-date.
* Examine the code and capture its behavior in tests. The problem with this
is that any code of even moderate complexity is hard to analyze.
We'll try the second approach with Validators.MaxLength, to demonstrate how
to do this with JsTestDriver. (iteration 130)
Starting to Write Tests for our Target
======================================
The first test (iteration 140) only checks that PolicyEffectiveDate has the
proper interface. Not everyone does this. Some find it overkill, and they
have persuasive arguments. But I find it a nice way to ease into testing
my code.
That one succeeds because the code is already written. Now we try our first
test of the actual API (iteration 150). The test is easy to write. It fails,
though because one of our dependencies is not available.
Stubbing
========
Here is the first thing we try that is significantly different than much of
how we've done JavaScript testing with YUI. Rather than include our
depencencies in the test harness HTML file, we simply stub them out.
(iteration 160)
Stubs are a key feature of robust unit testing. You need to test your components
in isolation, which isn't so easy if you need a large collection of other
components for every set-up.
Features of Stubs
=================
* Quickly written and extremely simple. You don't want to get into "testing
the test".
* Can be easily introduced and easily removed, either in the test functions or
in the test case set-up and tear-down.
* Should be as stupid as possible
* Used to force certain code paths, both happy-paths and problem-paths
Additional Stubbing Features in Javascript
==========================================
As well as stubbing out application code, in Javascript, you can often stub
out native code.
Our tests are date-sensitive, and they're related to the current date. To
handle this properly, we really need the current date to be fixed in our
tests. The alternative is to duplicate huge amounts of code to add and remove
certain number of days; and then our test code is too complex to verify by
simple inspection.
So here we simply overwrite the native Date constructor. (iterations 170 - 200)
There is an alternative to doing this, and sometimes it's the only choice: Add a
parameter to our factory function to make testing easier. Here we could pass a
Date into the PolicyEffectiveDate validator. We will try that in a little bit,
but remember the major issue with this: **You're putting test code into your
production system.**
What to put in a Single Test
============================
There is a great deal of debate about how much should be included in a single
test. But the two choices seem to come down to:
* A single assertion per test. This makes it eminently clear **what** failed
in a failing test.
* Any collection of tests with similar functionality and which rely on the same
setup. This helps keep your tests **DRY**.
While I have a lot of respect for the first view, I've chosen here (and ususally
end up choosing) to go with the second. I'd rather have less duplication even in
my test code at the expense of being slightly less explicit. Besides, if you only
**add** one assertion at a time, you will likely know what caused the issue.
(iteration 210)
Stubs can Contain Minimal State
===============================
You don't want your stub objects to hold much state, but it's fine to add a small
amount of local state if it makes testing easier. A good example is a field that
can only hold a few values; you might add code to set that value.
In our case, we're stubbing out acordConstants and ui.get. We're only using the
latter to get the field PolicyTerm, which can only have the values "06" and "12".
This is an easy place to add a little state to our stub. (iteration 220 - 230)
Testing Boundary Conditions
===========================
It's very important to test boundary conditions, especially to check for off-by-one
and fencepost issues.
(iterations 240 - 260)
Keep Your Tests DRY
===================
**D**on't **R**epeat **Y**ourself is one of the most valuable lessons to learn in
software development. And it applies to test code as well. Our tests look like this:
> "test name of test": function() {
> var dateStub = stubDate("06/01/2011");
> var uiStub = stubUi("06");
> // test setup and assertions here
> uiStub.restore(); dateStub.restore();
> },
but should look like this:
> "test name of test": function() {
> // test setup and assertions here
> },
(iteration 270)
Writing Code Test-First
=======================
We'll go through a few iterations of writing test-first code for something we
want to use in the refactoring of PolicyEffectiveDate test. No individual
slides for these.
(iterations 280 - 310)
**Note** though that as we're calling the native Date constructor, we want to
make sure our date stub used elsewhere is not interfering.
Reimplementing the Code in Question
===================================
We'll do this over the next several iterations (320 - 330).
The important point to remember now is that you can be fearless in your
refactoring. If your code passes the tests, you know it's working.
Making JsTestDriver code work inside YUI
========================================
This is very nice for the developer, but if it doesn't fit inside our testing
framework, it's of limited use. In the next set of changes, we try to get
these tests working as part of our YUI setup.
In the end, what we really want from this is a way to take out JsTestDriver
tests exactly as they are and somehow run them from within the YUI test
harness.
We do this over several steps (iterations 340 - 410)
Keep your powder (and your tests) DRY
=====================================
Even little things add up. All our test names start with "test". Why? because
the test framework uses that to determine what functions are tests to run. But
in our case, the only things in these objects that are not test functions are
`setUp` and `tearDown`. We can use that fact to simplify our code a bit more.
(iteration 420)
Simplifitying the YUI boilerplate
=================================
Again, there would be significant duplication in the YUI test files if we had
many of them. But much of that duplication is totally unnecessary for our
purposes. We extend the shim to take care of as much of this as possible.
(iteration 430)
There is a case to be made to push this to the logical extreme of removing all
the boilerplate. Perhaps the test file can consist of three script tags, one for
YUI, one for our bridge, and one that calls the bridge function using a list of
library files, a list of test files, and the test names. I'm not sure if it
would work, and it's left as an excercise for you, if you're interested.
Running YUI code inside JsTestDriver code
=========================================
We can shim in either direction. If we decide to switch to JSTestDriver,
we can use a shim like this to quickly port over all YUI code.
(iteration 440)
Automating Testing
==================
Obviously with our YUI tests we can automate testing as we've been doing. But
there are additional options with JsTestDriver. The tests can be called in
batch, and the output can be nicely formatted for easily drill-down to test
failures and errors.
Moreover, this can simultaneously test mutliple browsers.
(iteration 450)
Final Notes
===========
- Testing should not be seen as a chore. It's one of the best ways to ensure
correct code, and, in fact, often feels liberating.
- Testing should be part of the **rhythm** of coding: Write a failing test,
watch the test fail, write code to pass the test, watch it pass, write
another failing test.
- Tests should only be written for the public behavior of a component. If
there are implementation details to be tested, move them to their own
component, and test them there.
- Test code needs to be dead simple. If you can't see what's going on with a
glance at your test code, then the test is almost useless.
- User interfaces are hard to test, and Javsascript ones are no exception, but
most of your code is still easily testable. Test it!