March 24, 2019
Your tests for that unit pass, but then a bunch of unit tests in some random component fail!
To fix it you will need to change those tests in those unrelated units, and that’s a big problem. Your tests need to be immutable to refactoring and the above scenario is a Test Smell
.
There are lots of benefits to TDD, but one that doesn’t get as much airtime is that:
A good test-suite gives you confidence about the behaviour of your system.
However that confidence is NOT guaranteed by tooling, it comes from the fact that you followed TDD. You wrote a test, it failed, then you wrote some code and it passed. Therefore, that code works.
That means that the only reason you trust your codebase is because you trust your fellow developers to have also followed TDD. Which is to say, that relying on your test-suite is not a matter of Fact, but a matter of Trust.
Hence once a test-suite exists, we need to protect our Trust in it by minimizing changes to it.
Coming back to our hypothetical situation from earlier, Refactoring (by definition) is not supposed to be a change in behaviour. If you’re now forced to update test-code in unrelated units, you now need to manually validate those units and (all their usages) before trust is restored in the system.
For example, your unit originally set an internal state’s canProceed
and you refactored it to shouldProceed
. These other units are failing because they were asserting on the value of canProceed
.
If you go into those failing tests and update the tests with the new assumption it’s highly likely that an error will get made and you might assert false
instead of true
or any number of other issues. You might even accidentally delete a test (in more complex scenarios). The worst part of it is that your test suite can’t save you from these mistakes because it can’t self-test; the Light going Green doesn’t mean anything if the test implementation is giving you a false-positive.
The only reason to change a test should be if the required behaviour of the system changes. Either:
Tests don’t guarantee bug-free code, they’re about enabling faster feedback.
In all other cases, we expect the tests to be Immutable
i.e. Refactoring should not require a change in tests.
Since we don’t have a test-suite for our test-suite, there’s increased pressure to follow clean-code guidelines while implementing it. Once tests are written the intention is that they should be encased in ember, so we should aim to get it right the first time.
Here are some suggestions:
Bad: test('should set correct headers')
Good: test('should set the 'device' header to 'mobile')
If you’re writing the test name and it’s running a bit lengthy, you might be trying to test too many things or might have too many state-values to list out.
That’s okay, favour an explicit name over a short sentence. Atleast it’s clear. And anyway, you can fix that in the next point.
$1.00 - $2000.00 = No Interest
Loans of $2000 and below are interest-free
expected
resulttest('$2001.00 Loan Amount', () => {
assert.strictEqual(
calculateInterest('$2001.00' /*actual*/, '$0.09' /*expected*/)
)
})
test('A loan of $2001.00 owes $0.09 interest', () => {
assert.strictEqual(
calculateInterest('$2001.00' /*actual*/, '$0.09' /*expected*/)
)
})
Your unit tests also serve as documentation; make them as readable as possible for another developer who might not have the context you have right now.
Read up on RSpec
describe
to denote the behaviour you’re testing,context
to group tests by common assumptions or world-statesdescribe('launch the rocket', () => {
context('all ready', () => {
test('should launch rocket')
})
context('not ready', () => {
test('should do nothing')
})
})
Your tests statements should ideally read like documentation,
Breaking things up in this way also avoids long test-names
assert
per testIf multiple asserts are “necessary”, you’re probably trying to check multiple things
You might be tempted to start defining common objects and reusing them across tests, resist that urge.
if something is really so generic that it can be included across tests, make a library out of it
Take the following code as an example:
describe('createRequest', () => {
test('set correct headers', () => {
const actual = createRequest(
{ _csrf: 'abcd', __csrf: 'pqrst' }, // cookies
{
// params
url: 'AAA',
variables: { A: 1, B: 2 },
}
)
const expected = {
'X-CSRF': 'pqrst',
'content-type': 'application/json',
device: 'pwa',
}
assert.deepEqual(actual.headers, expected)
})
})
“This looks fine”, I hear you say, “What’s the Issue?“. Well, this is basically just a hidden version of having multiple asserts per test.
Here there’s three asserts:
X-CSRF
header,content-type
header, anddevice
.Refactor your tests to something like this:
describe('createRequest', () => {
describe('headers', () => {
test('should set X-CSRF header correctly', () => {
const actual = createRequest(
{_csrf: 'abcd', __csrf: 'pqrst'}, //cookies
{
//params
...
}
)
const expected = 'pqrst'
assert.equal(actual.headers['X-CSRF'], expected)
})
test('should set `content-type` correctly', () => {
const actual = createRequest(
{}, //cookies
{
//params
...
}
)
const expected = 'application/json'
assert.equal(actual.headers['content-type'], expected)
})
test('should set `device` header correctly', () => {
const actual = createRequest(
{}, //cookies
{
//params
...
}
)
const expected = 'pwa'
assert.equal(actual.headers.device, expected)
})
})
})
Your tests basically contain all the information about the design of your new component
If the testing code is solid, well written, and exhaustive, refactoring Application Code will be trivial!
Ultimately maintaining the test-suite is a matter of Discipline, Code-Guidelines and good Code-Review process as well as a reliable set of Team-mates in a trustful environment.
As individual developers I claim that if we’re going to be a craftsmen about anything, we should apply ourselves to the testing code. Our future selves will thank us for it.
Good luck out there. :)
More Typing, Less Testing | TDD With Static Types
Add Types
step into the Red / Green / Refactor cycleWritten by Joel Louzado who lives and works in Mumbai, India building fun things. Say hi on Twitter