How to Write Smoke Tests for an Ember Rails Stack

Written by: Micah Woods

The following story shows the importance of smoke tests when writing an app using the Ember+Rails stack. It covers the most basic example, and even then things go awry. In addition, it shows an example of an end-to-end test and some of the difficulty in setting up an environment for smoke tests. Finally, it mentions exhaust, a library that can ease this pain.

Imagine, you're ready to start writing your next great application. It's like TodoMVC meets Twitter meets eBay: Users will write down their To Do lists and auction off tasks to other users in 140 characters or less. Investors are very excited. You're very excited. High-fives all around. You're going to change the world, and you're going to do that using all the right technologies: Ember for the client side and Rails for the backend!

Here is your first user story:

cucumber
Given I am a user
When I go to the todos page
Then I should see a list of all todos

Getting Started with Ember Rails

Piece of cake! You start by installing the latest Ember:

> npm install -g ember bower
> ember new todo-frontend
> cd todo-frontend

"I don't care what @dhh says," you think out loud. "I'm a rockstar developer, so I'm gonna test-drive it like I stole it!" You don't have an API to test yet, so you start by mocking it. You install pretender and generate your first acceptance test:

> ember install ember-cli-pretender
> ember generate acceptance-test todos
// tests/acceptance/todos.js
import Ember from 'ember';
import { module, test } from 'qunit';
import startApp from 'emberbook/tests/helpers/start-app';
import Pretender from 'pretender';
let TODOS = [
  {id: 1, title: "write a blog post"},
  {id: 2, title: "let people read it"},
  {id: 3, title: "... profit"}
];
module('Acceptance | todos', {
  beforeEach: function() {
    this.application = startApp();
    this.server = new Pretender(function() {
      this.get('/todos', function(){
        var json = {
          todos: TODOS
        };
        return [200, {}, JSON.stringify(json)];
      });
    });
  },
  afterEach: function() {
    Ember.run(this.application, 'destroy');
    this.server.shutdown();
  }
});
test('visiting /todos', function(assert) {
  visit('/todos');
  andThen(function() {
    var title = find('h1');
    assert.equal(title.text(), 'Todo List');
    var todos = find('.todo-item');
    assert.equal(todos.length, TODOS.length);
    assert.equal(currentURL(), '/todos');
  });
});

Time to fire up the test runner and watch it all burn:

ember test --serve

Yep, it's red like the devil. First error, no route.

// app/router.js
import Ember from 'ember';
import config from './config/environment';
var Router = Ember.Router.extend({
  location: config.locationType
});
Router.map(function() {
  this.route("todos");
});
export default Router;

Now the test can't find the text "Todo List." You fix this as well:

{{!- app/templates/todos.hbs }}
# Todo List
{{#each model as |todo|}}
  <p class='todo-item'>
    {{todo.title}}
  </p>
{{/each}}

Now the test breaks because there are no To Do items on the page. You move forward:

// app/routes/todos.js
import Ember from 'ember';
export default Ember.Route.extend({
  model() {
    return this.store.findAll("todo");
  }
});

Can't find the "todo" model. One more fix:

// app/models/todo.js
import DS from 'ember-data';
export default DS.Model.extend({
  title: DS.attr(),
  complete: DS.attr("boolean")
});

Whoa, it's green! You pat yourself on the back. But you can't actually deliver the story until there's an API to back it up. Time to grab the latest version of Rails.

Testing an Ember Rails Application

This is going to be an API rather than a full-fledged application, so you only need a few gems. You spend several minutes pondering a post you just read about building Rails API apps and deciding whether to use the Rails 5 --api flag. However, you want this product to hit the market "yesterday," so you just roll with what's stable:

> gem install rails
> rails new todo_api --skip-test-unit --skip-bundle
> cd todo_api

You also skipped test-unit because you'd rather use Rspec's eloquent DSL for this project's tests.

Time to boil the Gemfile down to the few gems you really need. This API only needs to serve JSON, so out with anything related to the asset pipeline:

# Gemfile
source 'https://rubygems.org'
gem 'rails', '4.2.3'
gem 'sqlite3'
gem 'active_model_serializers'
group :development, :test do
  gem 'rspec-rails'
end

Bundle it and get ready to write some specs!

> bundle install
> rails generate rspec:install

Time to bang out the first request spec:

# spec/requests/todos_spec.rb
require "rails_helper"
RSpec.describe "Todos", :type => :request do
  # letBANG because `before {expected_todos}` looks funny
  let!(:expected_todos) do
    3.times.map do |i|
      Todo.create(title: "Todo #{i+1}")
    end
  end
  it "lists all todos" do
    get "/todos"
    todos = extract_key_from_response("todos", response)
    expect(todos.count).to eq expected_todos.count
  end
  def extract_key_from_response(key, response)
    json_response = JSON.parse(response.body)
    expect(json_response).to have_key key
    json_response[key]
  end
end

You run the test. Watch it go red! First gripe: no model. You fix that:

class Todo < ActiveRecord::Base
end

It's migration time!

<code class="language-bash rails generate migration CreateTodos title:string</code>
# db/migrate/#{timestamp}\_create\_todos.rb
class CreateTodos < ActiveRecord::Migration
  def change
    create_table :todos do |t|
      t.string :title
      t.timestamps null: false
    end
  end
end
> rake db:migrate

And of course, it needs a route.

# config/routes.rb
Rails.application.routes.draw do
  resources :todos, only: :index
end

The test is still red, because there is no controller. Next fix:

# controllers/todos_controller.rb
class TodosController < ApplicationController
  def index
    render json: todos
  end
  private
  def todos
    Todo.all
  end
end

All green! Time to boot up the app(s) and bask in your own creative genius. You run the following commands in different terminals:

> rails server
> ember server

You can't wait to see your baby running. You open "http://local:4200/todos"... and you get a blank screen. Now that was disappointing. Both test suites are green. What could have gone wrong?

After some inspection into the problem, you notice a GET http://localhost:4200/todos 404 (Not Found) in your logs. Well, crap. Ember doesn't know the API is on a different domain. Well, that's an easy fix. You just need to specify a host in the application adapter. And since you are attractive and intelligent, you know better than to hardcode the host. After all, it will change for staging and production. So you open config/environment.js in your Ember app, and you add the following:

// config/environment.js                                                                                             if (environment === 'test') {
  ENV.API_HOST = ''
} else {
  ENV.API_HOST = (process.env.API_HOST || 'http://localhost:3000')
}

You set the test environment's API_HOST to an empty string, so that the current tests keep working. All other environments use the Rails' default http://localhost:3000 unless an API_HOST environment variable is set. Now you can create an application adapter and fix the bug:

// app/adapters/application.js
import DS from 'ember-data';
import config from '../config/environment';
export default DS.RESTAdapter.extend({
  host: config.API_HOST
});

W00T! Fixed that problem. Time to run the tests. Once again, you start the Rails and Ember apps in two different terminals.

> rails server
> ember server

Dang it. The app still doesn't work. You didn't set up CORS so that modern browsers can talk to the Rails API. You add rack-cors to the Gemfile, bundle it, and add the simplest policy you can think of to get things working. Allow everything!

# Gemfile
gem 'rack-cors', :require => 'rack/cors'
bundle install
# config/application.rb
config.middleware.insert_before 0, "Rack::Cors", :debug => true, :logger => (-> { Rails.logger }) do
  allow do
    origins '*'
    resource '*',
      :headers => :any,
      :methods => [:get, :post, :delete, :put, :options, :head],
      :max_age => 0
  end
end

You cross your fingers, run the tests, and start the servers. When you visit the todos route, you see your app is working. Phew! Even though your app is working now, you aren't 100 percent happy. You spent the last hour test driving an application that didn't work, even though all tests were green. And you didn't even focus unit tests -- all of your tests were integration tests. Shouldn't integration tests prevent things like this?

Writing Smoke Tests for an Ember Rails App

At this point, you remember a post on the Hashrocket blog about test driving elixir apps with cucumber. You decide to try these techniques in practice and write some smoke tests. The type of integration tests that were just written are great for testing application(s) in isolation. These isolated integration tests run very fast. And because they run fast, the majority of your tests should be either request specs or Ember integration. There is no reason to have full coverage (testing every possibility) with smoke tests.

However, in order to ensure the app actually works, there should be at least one smoke test for every API endpoint. Knowing that cucumber-rails already loads your test environment, you think of the simplest solution you can to try writing a smoke test. Simply add cucumber-rails, manually start Ember and Rails, then overwrite how capybara works. This is the solution you come up with. You add cucumber to the Gemfile and install it.

# Gemfile
group :development, :test do
  gem 'cucumber-rails', require: false
  gem 'database_cleaner'
  gem 'capybara-webkit' # ...
end
> bundle install
> rails generate cucumber:install

Then you overwrite capybara's behavior.

# features/support/env.rb
Capybara.configure do |config|
  # Don't start rails
  config.run_server = false
  # Set capybara's driver. Use your own favorite
  config.default_driver = :webkit
  # Make all requests to the Ember app
  config.app_host = 'http://localhost:4200'
end

And you write the first feature and step definitions.

# features/todo.feature
Feature: Todos
  When a user visits "/todos", they should see all todos
  Scenario: User views todos
    Given 4 todos
    When I visit "/todos"
    Then I should see "Todo List"
    And I see 4 todos
# features/steps/todo.rb
Given(/^(\d+) todos$/) do |num|
  num = num.to_i
  num.times do |i|
    Todo.create(title: "Title #{i}")
  end
end
When(/^I visit "(.*?)"$/) do |path|
  visit path
end
Then(/^I should see "(.*?)"$/) do |text|
  expect(page).to have_text(text)
end
Then(/^I see (\d+) todos$/) do |num|
  expect(all(".todo-item").length).to eq num.to_i
end

Now in separate terminals you run both Rails and Ember. This time Rails is started in the test environment so that Ember requests hit the Rails test database.

> rails server --environment test
> ember server

You run the test suite with rake, and voilà! You see that the test suite passes. You know that both Ember and Rails are communicating end-to-end. And even though you are super proud of yourself, you're not happy. "The servers should start automatically. Why can't I just type rake?" you think to yourself. Here is the first solution you come up with.

# features/support/env.rb
ember_pid = fork do
  puts "Starting Ember App"
  Dir.chdir("/your/user/directory/todo-frontend") do
    exec({"API_HOST" => "http://localhost:3001"}, "ember server --port 4201 --live-reload false")
  end
end
rails_pid = fork do
  puts "Starting Rails App"
  Dir.chdir(Rails.root) do
    exec("rails server --port 3001 --environment test")
  end
end
sleep 2
at_exit do #kill the Ember and Rails apps
  puts("Shutting down Ember and Rails App")
  Process.kill "KILL", rails_pid
  Process.kill "KILL", ember_pid
end
Capybara.configure do |config|
  config.run_server = false
  config.default_driver = :webkit
  config.app_host = 'http://localhost:4201'
end

This solution works okay. Rails and Ember run on separate ports, so the development versions of the two servers can keep running. The only problem is the sleep 2. As the Rails and Ember apps grow, so will the time needed to wait for the servers to start. Also, this number is magic; it might take longer on another machine. How long will it take on the CI server?

What you really want to do is halt the tests until you know that Rails and Ember are running. However, after some investigation, you realize there is no way to know if Ember is running successfully. But then you notice how EmberCLI smoke tests itself.

  it('ember new foo, server, SIGINT clears tmp/', function() {
    return runCommand(path.join('.', 'node_modules', 'ember-cli', 'bin', 'ember'), 'server', '--port=54323','--live-reload=false', {
        onOutput: function(string, child) {
          if (string.match(/Build successful/)) {
            killCliProcess(child);
          }
        }
      })
      .catch(function() {
        // just eat the rejection as we are testing what happens
      });
  });

Now you know that Ember has booted when it outputs "Build successful" and have some insight as to how you might wait for Rails.

# features/support/env.rb
begin
  DatabaseCleaner.strategy = :truncation
rescue NameError
  raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it."
end
ember_server = nil
rails_server = nil
Dir.chdir("/Users/mwoods/hashrocket/todo-frontend") do
  ember_server = IO.popen([{"API_HOST" => "http://localhost:3001"}, "ember", "server", "--port", "4201", "--live-reload", "false", :err => [:child, :out]])
end
Dir.chdir(Rails.root) do
  rails_server = IO.popen(['rails', 'server', '--port', '3001', '--environment', 'test', :err => [:child, :out]])
end
# Before timeout loop to prevent orphaning processes
at_exit do
  Process.kill 9, rails_server.pid, ember_server.pid
end
# if it takes longer than 30 seconds to boot throw an error
Timeout::timeout(30) do
  # wait for the magic words from ember
  while running = ember_server.gets
    if running =~ /build successful/i
      break
    end
  end
  # when rails starts logging, it's running
  while running = rails_server.gets
    if running =~ /info/i
      break
    end
  end
end
Capybara.configure do |config|
  config.run_server = false
  config.default_driver = :webkit
  config.app_host = 'http://localhost:4201'
end

This is not a perfect solution, but it's good enough for day one. On day two, you might decide to move the smoke tests to their own repository. After all, they aren't Rails-specific, so they probably should be a separate project.

Problems like these and many others have happened to me. That's the reason I'm introducing exhaust, a new gem that hopefully alleviates some of the headache associated with smoke testing an Ember+Rails stack. Enjoy!

Stay up to date

We'll never share your email address and you can opt out at any time, we promise.