Using Dependency Injection to Write Better Tests
11 August 2016
Testing is given much emphasis in the Ember.js community, and testing tools have showed steady progress to reduce the cost of writing tests of all types.
Lauren Tan wrote a great post about how Dependency Injection (DI) can be used to decouple a parent component from the internals of its child components. One of the gains of doing so is that the parent component becomes more focused and thus easier to test.
In this post, I'm doing something similar, although much simpler. I want to show you how to use DI in a simple helper function to make it easier to test.
Just your ordinary, run-of-the-mill function
Although the helper is an Ember (template) helper, the concepts could be very easily transferred to other frameworks, libraries and even languages.
I recently had to modify a normalizeText
helper function that looked like this:
1// tests/unit/helpers/normalize-text-test.js 2import Ember from 'ember'; 3 4export function normalizeText([text]) { 5 let normalizedEOLs = text.trim().replace(/(?:\r\n|\r|\n)/g, '</p><p>'); 6 let noEmptyParagraphs = normalizedEOLs.replace(/(<p><\/p>)/g, ''); 7 return Ember.String.htmlSafe("<p>" + noEmptyParagraphs + "</p>"); 8}
(I realize the above code does not handle a text value of undefined
or null
. The real code does but I want to keep the code examples to the minimum necessary to get my point across.)
Comparing objects to objects
Its test was quite simple and straightforward:
1// tests/unit/helpers/normalize-text-test.js 2import { normalizeText } from '../../../helpers/normalize-text'; 3import { module, test } from 'qunit'; 4 5module('Unit | Helper | normalize-text'); 6 7test('it works', function(assert) { 8 let normalizedText = normalizeText(["The brown fox\r\njumped over the quick rabbit.\n"]); 9 assert.equal(normalizedText, "<p>The brown fox</p><p>jumped over the quick rabbit.</p>"); 10});
The problem with that test is that we compare two Handlebars.SafeString
instances (returned by Ember.String.htmlSafe
) which are different even if the strings they wrap, their value, is the same:
We're, however, interested in the equality of the strings. If only there was a way to replace that pesky Ember.String.htmlSafe
call from the call site...
DI to the rescue
This is exactly what Dependency Injection can help us do. Instead of hard-coding that "sanitizer" function dependency, the function could take it as a parameter so that callers could inject it. Usually DI examples use (and thus inject) class names or object instances but it is important to realize that the injected param could be very "primitive", like a simple function.
So here is how I rewrote the function:
1// app/helpers/normalize-text.js 2import Ember from 'ember'; 3 4export function normalizeText([text], params={}) { 5 let { sanitizer=Ember.String.htmlSafe } = params; 6 let normalizedEOLs = text.trim().replace(/(?:\r\n|\r|\n)/g, '</p><p>'); 7 let noEmptyParagraphs = normalizedEOLs.replace(/(<p><\/p>)/g, ''); 8 return sanitizer("<p>" + noEmptyParagraphs + "</p>"); 9} 10 11export default Ember.Helper.helper(normalizeText);
Notice how easy ES2015 destructuring makes the assignment of the sanitizer function:
1let { sanitizer=Ember.String.htmlSafe } = params;
If no sanitizer
key was present in params
, then it will have a value of Ember.String.htmlSafe
, the default behavior.
The call from the test can now override the default behavior of sending the normalized text through Ember.String.htmlSafe
by passing in a "no-op" sanitizer function:
1// tests/unit/helpers/normalize-text-test.js 2import { normalizeText } from '../../../helpers/normalize-text'; 3import { module, test } from 'qunit'; 4 5function leaveAsIs(text) { 6 return text; 7} 8 9module('Unit | Helper | normalize-text'); 10 11test('it works', function(assert) { 12 let normalizedText = normalizeText(["The brown fox\r\njumped over the quick rabbit.\n"], { sanitizer: leaveAsIs }); 13 assert.equal(normalizedText, "<p>The brown fox</p><p>jumped over the quick rabbit.</p>"); 14});
We're now comparing simple strings which place nicely with assert.equal
(with ===
), and our test now passes.
Non-testing benefits
Code modifications introduced for the sake of testing usually also improve the non-testing aspect. Here, we made it possible to pass any function before we return the normalized text. We could, for example, use this to replace the <p>
tags with <span>
s, if we so wish.