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 memorymodule. 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 testimport 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

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?

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.

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.

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.