A Review of Mocking Dependencies in Ember.js and Elixir

By: Scott Newcomer
Bikes within car framework

Mocks and Testing Your Application

Mocks are a nice utility to test either code that is not present at test runtime or when the object in question is non-deterministic. If you have ever had functions or objects that behave erratically under test, you might need to reach for a mock. I’d like to present an argument for and against mocks. I won’t get into specific definitions of mocks vs. stubs as there are plenty of articles discussing this topic. But I’ll look at specific examples in ecosystems DockYard focuses on: Ember.js and Elixir. We’ll use a made up Shape-Shifter library that we pull in as a dependency to our application. Let’s set the stage and see what our example looks like on the Ember.js side of things using plain old JavaScript (note not Typescript).

// utils/shape-shifter.js

export default function shapeShifter() {
  // ShapeShifter is found on the window object after including it's script tag <script src="https://cdn.shape-shifter.js">
  return window.ShapeShifter;
}
// services/shape-shifter.js

import shapeShifter from '../utils/shape-shifter';

const {
  configure = () => {},
} = shapeShifter();

export default Service.extend({
  init() {
    this._super(...arguments);

    let instance = configure({
      ...
    });

    set(this, 'api', instance);
  }
});
// tests/helpers/setup-mock.js

function assignShapeShifterToWindow(mock = {}) {
  let defaultMock = {};

  let shapeShifterMock = { ...defaultMock, ...mock };

  let ShapeShifter = {
    configure(setup) {
      return { ...shapeShifterMock, ...setup };
    }
  }

  window.ShapeShifter = ShapeShifter;
}

export function setupShapeShifterMock(hooks) {
  hooks.beforeEach(function() {
    assignShapeShifterToWindow();
  });

  hooks.afterEach(function() {
    window.ShapeShifter = null;
  });
}

Great. We can stub out a secret key method that previously would make a request to an API service on init of our identity service. Now in our test, we can mock this specific service so we don’t try hitting the real shape shifter API, which would most likely result in an error.

// tests/unit/shapeshifter-test.js

import { setupShapeShifterMock } from 'app/tests/helpers/setup-mock'

module('Unit | Service | indentity', function(hooks) {
  setupTest(hooks);
  setupShapeShifterMock(hooks);

  test('it updates the identity', function(assert) {
    ...
  });

However, the shape-shifter library is moving fast. They decided to add authorization of the shape-shifter to make sure the clone is an authorized clone. And they added event listeners. This may or may not break our tests anywhere we use the shape-shifter service. Let’s say only on certain routes do we stamp out an instance of our shape-shifter service. For those tests that rely on getting a secret key and authorizing the shape-shifter service, those routes will be broken. Most likely, the specific error would indicate some method is undefined, but would only be found after running the tests. So we need to keep shape with our mocked system. Time to update.

// services/shapeshifter.js

import ShapeShifter from '../utils/shape-shifter';

const {
  configure = () => {},
} = shapeShifter();

export default Service.extend({
  init() {
    this._super(...arguments);

    let instance = configure({
      ...
    });

    set(this, 'api', instance);

    this.handleShapeChanged = this.handleShapeChanged.bind(this);
    instance.addEventListenter('shape-changed', this.handleShapeChanged);
  },

  authorize(token) {
    this.api.authorize(token);
  }
});
// tests/helpers/setup-mock.js

function assignShapeShifterToWindow(mock = {}) {
  let defaultMock = {
    isAuthorized: false,
    authorize() {
      this.isAuthorized = true;
    },
    addEventListener() {},
    removeEventListener() {}
  };

  let shapeShifterMock = { ...defaultMock, ...mock };

  let ShapeShifter = {
    configure(setup) {
      return { ...shapeShifterMock, ...setup };
    }
  }

  window.ShapeShifter = ShapeShifter;
}

Dynamically Typed Languages

There is a stark difference between dynamic typing and static languages. It has been argued that static languages produce less mocking issues than a dynamically typed language. As you can see above, changes to the shape-shifter library resulted in necessary changes to our mock. The real implementation may continue to diverge, even in cases where an update to the underlying mock is not necessary.

Elixir, although a dynamically typed language, has strict type checking between primitives as well as the ability to gradually build in typing. For example, try adding a number and a string together in Elixir (hint: an error). In our example below, we will add implicit compile time checking via interfaces. So when we update the API for our service, we also have to update the interface. Let’s look at what mocking a service in Elixir looks like.

Let Us See the Difference

We will use mox to show how adhering to an interface can make mocking much easier.

First, define the mock in the test helper file.

# test_helper.exs

Mox.defmock(ShapeShifter.API.Mock, for: ShapeShifter.API)

Second, define the interface the API should conform to with the @callback directive. Moreover, to specify the API implements a given behavior, add the @behaviour attribute.

defmodule ShapeShifter.API do
  @callback get_clones(url :: String.t(), source_id :: String.t()) :: {:ok, map}
end
defmodule ShapeShifter.API.HTTP do
  @moduledoc """
  API surface for fetching clones
  """

  @behaviour ShapeShifter.API
  def get_clones(url, source_id) do
    ...
  end

Lastly, write your test.

defmodule ShapeShifter.APITest do
  import Mox

  alias ShapeShifter.API.Mock, as: ShapeShifterMock

  @clones [ ... ]

  test "get_clones" do
    ShapeShifter.init([])

    verify_on_exit!()

    expect(ShapeShifterMock, :get_clones, fn (_url, _source_id) -> @clones end)
  end

Now what happens when the API changes? Our @callback directive needs to be updated, since this behavior ensures we implement required function signatures as shown above. The main point here, however, is that updating the @callback directive would be necessary whether we tested this piece of code or not. As a result, our mock is still in sync with the system and we have much faster feedback since a warning will be raised if we compile our application. In our previous case in Ember.js without Typescript, since we did not have an interface to adhere to, we did not get fast feedback, nor is the error likely obvious.

Are Mocks Good for Your Testing Apparatus?

Mocks are imposters. On the inside you know they are simply a clone of something else. As you can see in the above example, as new functionality gets added to the shape-shifter library, we may need to update our mocks, which could be a pain if those mocks are spread across our application testing apparatus.

However, mocks can be a good step to testing a system that otherwise would be impossible to test. Moreover, mocks are necessary when other parts of the application have yet to be implemented or are a work in progress. As a result, always evaluate if the thing you are mocking actually needs to be mocked. In some cases, it is easy enough to simply modify properties on the underlying object instead of mocking the whole object. This would help avoid refactor work on your testing suite while also testing a larger surface area of the actual dependency. In other cases that may involve API requests or a complicated interwoven dependency graph, mocking the object will be your best choice.

Wrapping Up

So, is mocking something you really need? After looking through various scenarios, it becomes obvious that mocks in tests are a phenomenal utility to have in our arsenal. However, our time is measured in not only building the system, but also maintaining it. So to avoid time down the road, if we can avoid mocking, do so. On the other hand, mocking subsystems is a great way to get test coverage where there might not have been any before.

In my next post, I hope to evaluate what this mocked system looks like with Typescript. Stay tuned.

DockYard is a digital product agency offering exceptional user experience, design, full stack engineering, web app development, software, Ember, Elixir, and Phoenix services, consulting, and training.