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, and over to React). Mithril has proven to not only make development simple, but also testing, especially within a Rails app. This post details some ideas behind testing Mithril components using Jasmine within our Ruby on Rails app.

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 with: 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, then check out the documentation!

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

Setting up our Mithril test files

Testing our Mithril components requires loading all dependencies used to make the components work in production. We have a number of core dependencies, so we created a spec_helper.js file, which we place at the top of a test file. Here's an example from our promotions.spec.js file. (Note: the paths are simplified for this post)

// --- Vendor Dependencies ---
//= require moment

// --- Spec Helper Dependencies ---
//= require ../spec_helper

// --- Shared Components ---
//= require _shared/components/scope
//= require_directory ./app/assets/javascripts/_shared/components/sizes
//= require_directory ./app/assets/javascripts/_shared/components/select
//= require_directory ./app/assets/javascripts/_shared/components/sizes
//= require_directory ./app/assets/javascripts/_shared/components/modal

// --- Promotions Components ---
//= require ./app/assets/javascripts/admin/promotions/scope.js
//= require_directory ./app/assets/javascripts/admin/promotions/_shared
//= require_directory ./app/assets/javascripts/admin/promotions/table
//= require_directory ./app/assets/javascripts/admin/promotions/sidebar

Writing tests for our Mithril components

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

vhxm.components.button.ui = {  
  controller: function() {
    this.createNew = function() {
      vhxm.components.sidebar.state.isOpen(true);
      m.route('/admin/promotions/new');
    };
  },
  view: function(ctrl) {
    if (!vhxm.models.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 vhxm.models.promos() functions are actually Mithril props, which is a getter-setter factory utility. It returns a function that stores information.

In the component view, we check the to see if vhxm.models.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() {  
  var 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 anything inside of it before each test runs. Next is the test itself:

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

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

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

Let's break down each line;

vhxm.models.promos({ data: 'hello!' });

This line ensures that vhxm.models.promos() returns a value.

var ctrl = new vhxm.components.button.ui.controller();

Here we create an instance of our component controller.

m.render(node, vhxm.components.button.ui.view(ctrl));

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.

var 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 create another test to make sure that the loader is rendered if the promos model doens't have any data:

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

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

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

Now let's look at two tests for our controllers createNew function:

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

    expect(vhxm.components.sidebar.state.isOpen()).toBe(true);
  });

  it('should route to admin/promotions/new when called', function() {
    var table = new vhxm.components.button.ui.controller();
    table.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! At first, testing can seem tedious, but it will ultimately help you catch bugs, refactor your logic, and keep your components smaller. I've come to love testing our front-end code, and will later include posts on testing our React code with Mocha/Chai, as well as end-to-end testing practices with testcafe.