At VHX, we decided to move our front-end codebase from an in-house jQuery solution to a component-based architecture with Mithril.js (edit: As of late, we’ve been moving away from Mithril over to React). Mithril has proven to make development and testing simple, especially within a Rails app. This post details some ideas behind testing Mithril components using Jasmine within our Ruby on Rails app.

Update 01-20-2018: This post was written when Mithril was at v0.23. The API has changed since then, so you may see some discrepancies between this post and Mithril’s current version.

Jasmine Setup

First, add the following to your Gemfile:

gem 'jasmine-rails'
gem 'jasmine-jquery-rails'

Then run bundle install to install these gems.

You’ll also want to install PhantomJS if you plan on running your tests in your console. I did this via Homebrew: brew install phantomjs

Jasmine-Rails has it’s own generator for necessary configuration files, so make sure you run this with rails generate jasmine_rails:install. Make sure to check out the documentation!

Finally, we placed all of our Mithril related tests under spec/javascripts/mithril.

Writing tests for our Mithril components

First, let’s take a look at a simplified component:

import m from 'mithril';
import { sidebar } from '@vhx/quartz';

export const button = {
  controller: function() {
    this.createNew = function() {
      sidebar.isOpen(true);
      m.route('/admin/promotions/new');
    };
  },
  view: function(ctrl) {
    if (!promos()) {
      return m('.loader', 'Loading...');
    }

    return m('.container', [
             m('button.btn-teal.btn--medium'), {
               onclick: function() {
                 ctrl.createNew();
               }
             }, 'Create Promo')
           ]);
  }
};

Here we have a button component. The component controller contains a function which sets the sidebar isOpen state to true and sets our URL route to /admin/promotions/new. The isOpen() and promos() functions are actually a Mithril prop, which is a getter-setter factory utility. It returns a function that stores information.

In the component view, we check the to see if promos() returns a null value, which will then render a loader. Otherwise, it renders a button in the view, with a click event that calls the controller’s createNew function. Using Mithril’s render function, we can test what the view will return.

Let’s set up our main describe and it blocks:

describe('Button', function() {
  let node;

  beforeEach(function() {
    node = document.createElement('div');
  });

  describe('button view', function() {
    it('does not display a loader if data is loaded', function() {
    });
  });
});

The above shows that we’re testing our Button component; within that we’ll specifically test our Button view. Describe blocks can be nested, which allow you to break your tests up as granularly as you’d like. The it block will be where our actual test resides. The beforeEach function will run before each test runs. Next is the test itself:

it('should not display a loader if data is loaded', function() {
  promos({ data: 'hello!' });
  const controller = new button.controller();
  m.render(node, button.view(controller));

  const elem = $(node).find('.container');

  expect(elem).toBeDefined;
  expect(elem.length).toEqual(1);
});

Let’s break down each line;

promos({ data: 'hello!' });

This line ensures that promos() contains and returns a value.

const controller = new button.controller();

Here we create an instance of our component controller.

m.render(node, button.view(controller));

Here’s the Mithril render function in action. The first param is the DOM element created in the beforeEach function. The second param is our component view, with the controller instance passed into it.

const elem = $(node).find('.container');

This is where we use jQuery to find what’s been rendered. We could also use native DOM functions, but this could get lengthy if you’re looking for something that’s deeply nested.

expect(elem).toBeDefined;
expect(elem.length).toEqual(1);

Here we have two test expectations. The first checks to see if the .container element has been found. The second verifies that there’s only one instance of this element. Similarly, we can make sure that the loader is rendered accordingly with a test like this:

it('should display a loader if no data is loaded', function() {
  promos(null);
  const controller = new button.controller();
  m.render(node, button.view(controller));

  const loader = $(node).find('.loader');

  expect(loader).toBeDefined();
  expect(loader.length).toEqual(1);
});

Now let’s look at two tests for our the createNew function:

import m from 'mithril';
import { button } from 'button.js';
import { sidebar } from '@vhx/quartz';

describe('createNew', function() {
  it('should set the sidebar to open when createNew is called', function() {
    const button = new button.controller();
    button.createNew();

    expect(sidebar.isOpen()).toBe(true);
  });

  it('should route to admin/promotions/new when called', function() {
    const button = new button.controller();
    button.createNew();

    expect(m.route()).toMatch('/admin/promotions/new');
  });
});

In this case, we don’t have to create our view. We’re just going to make sure that the controller function is setting the route and sidebar state as expected.

That’s it! Testing can be tedious, but it’s already helped us catch bugs, refactor our logic, and keep our components smaller.