tisdag 10 november 2015

Test friendly JavaScript modules - without Dependency Injection

A while ago, I was trying to figure out how to use test driven development with ES2015 (also known as ES6 at the time). It turned out to be a nice experience, and I wrote a blog post about it: Can I test it?

However, I had some concerns about the import feature and how to deal with module dependencies. I wrote a blog post about that too: Is the ES6 import feature an anti pattern?

Recently, I have been learning Python and was surprised by the similarities with JavaScript - in coding style, philosophy and language features. I guess Python has been a source of inspiration for the new ES2015 stuff. I have quickly become a fan of Python and I think it is a very nice language.

 At first, as a Python newbie, I stumbled upon the same questions on how to unit test modules containing a bunch of import statements. Luckily, at work I am surrounded with great programmers in general, that also are experts in the Python language. With Python, the answer to my questions was both simple and obvious. Python is a dynamic language: load the dependency in the unit test, then just override it. The code under test will use the already loaded in memory module. Simple, huh? It seems to me that there is no need for c#/java like dependency injections. I guess it was my typed language mindset that had guided me to IoC and DI patterns.

What about JavaScript?
I think it is (almost) as simple! Modules and imports in JavaScript are similar to (but not exactly like) the Python way. In JavaScript, I guess it also depends on the module loader used in the current environment. I have experimented with this. My setup contain ES2015 modules and unit tests, that are transpiled with Babel to RequireJS modules, runs in a browser or with PhantomJS. With this setup, I have managed to override module dependencies with fakes from a unit test. The module under test will actually run the fake dependency, and there is no need for custom Dependency Injection.

An example: a unit test
import foo from 'modules/foo';
import bar from 'modules/bar';

const fakeMessage = 'a fake message.';

const original = bar.getMessage;
const fake = () => fakeMessage;

QUnit.module('my example tests', {
    beforeEach() {
        bar.getMessage = fake;
    },
    afterEach() {
        bar.getMessage = original;
    }
});

QUnit.test('can an imported module be mocked?', assert => {
assert.equal(foo.message(), fakeMessage);
});

And the actual code under test:
import bar from 'modules/bar';

let foo = {
    message() {
        return bar.getMessage();
    }
};


export default foo;

I would appreciate your feedback on this!

You will find unit tests, code, settings, project structure and setup at my GitHub page:
https://github.com/DavidVujic/blog/tree/master/es2015-testable-modules

8 kommentarer:

uri sa...

Awesome. It works because whenever you import a module in ES6, it's a singleton instance.

My only question-- what happens if the property you are trying to override is defined as "const"?
Ex:

// bar.js

export const barSqrt = Math.sqrt;

David Vujic sa...

The value of the import (like "bar" in the example code) cannot be changed, it is (as I understand it) read-only. However, the properties of the import (variables, functions) can be changed. Even though they are declared as "const", which I find surprising. I have added a new module (baz) and unit tests (baz-tests) in the Github example code demonstrating the functionality. I used a trick to bypass the readonly state: import * as myModule from... then I can manipulate everything that is exported in the module.

The code isn't run "native", it is transpiled with Babel. The unit tests are run as transpiled es5 code, testing the transpiled versions of the modules. Maybe the behaviour is different when running without transpilation?

Patrick Hund sa...

Interesting article, I wasn't aware you could slip mocks to your modules so easily. I use Sinon mock library for mocking my Mocha tests.

Wolfram Kriesing sa...

what is the advantage over injecting the dependency?
Does your test not grab deep inside the SUT?
Doesn't the test know too much about the implementation details?

David Vujic sa...

I don't know if it is an advantage, but I think it is a simple alternative to dependency injection and IoT. You can use the native way of importing, instead of wiring up dependencies (usually means a lot more code). The trade off is that the test has more knowledge of the implementation, I agree with you. But I also think the benefits are simplicity, less code (less cost) and a solution for teams with people using TDD (like myself) and the ones that don't.

Robert Sabiryanov sa...

David, here is three problems:
1) you change methods by assigning them some code instead of using any mocking framework like Sinon.js. And then you will have problems if you want to test behavior, for example if you'd like to be shure that some method was called just once.
2) you try to change behavior in beforeEach section. In real world test cases should define behavior.
3) you have to know all imported modules tree to run test. In real application these tree can be deep.
But DI pattern allow you test only one application "class" and mock other dependencies.

David Vujic sa...

Thank you for sharing your feedback!

I agree with you on 1):
sinon is probably better to use than faking objects manually. In this example I didn't want to risk leaving focus from the idea, by not using any frameworks beside the unit testing framework.

With 2) I think it is spot on about the differences between DI and this approach.

3): I am not sure that I agree, because the module(s) being faked is faked and controlled, just as if mocked and injected using DI.

Marcelo Lazaroni sa...

Nice post! Thanks for that.