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:

1let s1 = Ember.String.htmlSafe("sid transit gloria mundi");
2let s2 = Ember.String.htmlSafe("sid transit gloria mundi");
3s1 === s2 // => false

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.

Share on Twitter