This project was generated using:
$ rails plugin new rails_engine --mountable
$ bundle install
Returns 200 (OKAY).
reachability.js - Provides a basic health check, on a timer, and fires events on success/failure.
Add these dependencies to your Gemfile.
group :development, :test do
gem 'teaspoon-jasmine'
end
$ bundle install
teaspoon provides a Rails generator, but it won't run in a Rails engine's root. We'll take advantage of the dummy application. Also, I like Rspec more than TestUnit, so I'm going to move the dummy application that was created for us.
$ mkdir spec
$ mv test/dummy spec/dummy
$ cd spec/dummy
$ rails g teaspoon:install
We'll need to modify/move a few files so that we can run our tests from the project's root, but use the dummy app as a test harness.
$ mv spec/* ../
$ echo "require '../teaspoon_env.rb'" >> teaspoon_env.rb
# rails_engine/spec/teaspoon_env.rb
unless defined?(Rails)
ENV['RAILS_ROOT'] = File.expand_path('../dummy', __FILE__)
require File.expand_path('../dummy/config/environment', __FILE__)
end
ENV['TEASPOON_RAILS_ENV'] = File.expand_path('../dummy/config/environment', __FILE__)
require 'rails_engine'
Teaspoon.configure do |config|
config.root = RailsEngine::Engine.root
end
# rails_engine/Rakefile
desc "Run the javascript specs"
task :teaspoon => "app:teaspoon"
// rails_engine/spec/dummy/app/assets/javascripts/application.js
//
// When running tests and generating coverage, we want to pull in all the engine's assets
//
//= require rails_engine/application
Javascript coverage is off by default, because it requires istanbul to be installed. If you've installed the package, let's turn coverage on. What we need is currently commented out, let's uncomment and modify a bit.
# spec/teaspoon_env.rb
config.use_coverage = 'default' # run coverage on the default suite
config.coverage do |coverage|
coverage.reports = ['text-summary', 'html']
coverage.output_path = 'coverage'
end
$ bundle exec rake teaspoon
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:61428/teaspoon/default
Finished in 0.00100 seconds
0 examples, 0 failures
=============================== Coverage summary ===============================
Statements : 41.67% ( 10/24 )
Branches : 0% ( 0/4 )
Functions : 0% ( 0/7 )
Lines : 41.67% ( 10/24 )
================================================================================
The GUI needs to be run from within the engine's dummy application
$ cd rails_engine/spec/dummy
$ bundle exec rails s
Let's fix that coverage we see above...
Before we start, it would probably be helpful to familiarize yourself with jasmine's API
Assuming you're in rails_engine root folder
$ echo "pending();" >> spec/javascripts/reachability_spec.js
Now we'll run our tests, and check the output:
$ bundle exec rake teaspoon
=> marked Pending
# jasmine/2.2.0.self-36ccb16d3b8cb028dbb4ba8678e420e521189afd8d74fe536f6f321c86afeb59.js:872 -- pending
# jasmine/2.2.0.self-36ccb16d3b8cb028dbb4ba8678e420e521189afd8d74fe536f6f321c86afeb59.js:3014 -- pending
Finished in 0.00200 seconds
0 examples, 0 failures
=============================== Coverage summary ===============================
Statements : 41.67% ( 10/24 )
Branches : 0% ( 0/4 )
Functions : 0% ( 0/7 )
Lines : 41.67% ( 10/24 )
================================================================================
Great, we've got a placeholder test.. now let's take a look at reachability.js
...
var reachabilitySuccessEvent = 'reachability:success'
var reachabilityErrorEvent = 'reachability:error'
var reachabilityHTTPMethod = 'HEAD'
var reachabilityEndpoint = '/health'
var reachabilityTimeout = 5000;
...
Let's write an easy expectations first
describe('reachability', function() {
describe('reachabilitySuccessEvent', function() {
it('equals reachability:success', function() {
expect(reachabilitySuccessEvent).toEqual('reachability:success');
});
});
});
and re-run tests:
$ bundle exec rake teaspoon
Starting the Teaspoon server...
Teaspoon running default suite at http://127.0.0.1:64783/teaspoon/default
.
Finished in 0.00400 seconds
1 example, 0 failures
=============================== Coverage summary ===============================
Statements : 41.67% ( 10/24 )
Branches : 0% ( 0/4 )
Functions : 0% ( 0/7 )
Lines : 41.67% ( 10/24 )
================================================================================
Coverage didn't change, but that's okay.. we'll get there. Can you write the rest of the tests for our other variables above?
The sendEvent
API dispatches an event that can be bound to, let's test event dispatching.
Phantom-JS does not have the CustomEvent API that we're using, so let's add a shim to ensure the API is avilable.
// spec/javascripts/spec_helper.js
// Shim required for PhantomJS
var CustomEvent = function(event, params) {
var evt;
params = params || {
bubbles: false,
cancelable: false,
};
evt = document.createEvent("CustomEvent");
evt.initCustomEvent(event, params.bubbles, params.cancelable);
return evt;
};
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
var sendEvent = function(eventName) {
document.dispatchEvent(new CustomEvent(eventName, {
bubbles: true,
cancelable: true
}));
};
Looking at the javascript we'll want to test that calling this API dispatches an event. Let's add a new test to reachability_spec.js
.
describe('sendEvent', function() {
it('dispatches an event', function() {
sendEvent('foobar');
pending();
});
});
To ensure that an event was dispatched, we can spyOn
document.dispatchEvent
and expect it toHaveBeenCalled
.
describe('sendEvent', function() {
it('dispatches an event', function() {
spyOn(document, 'dispatchEvent');
sendEvent('foobar');
expect(document.dispatchEvent).toHaveBeenCalled();
});
});
Now let's re-run our tests:
$ bundle exec rake teaspoon
.....sendEvent
.
Finished in 0.00600 seconds
6 examples, 0 failures
=============================== Coverage summary ===============================
Statements : 50% ( 12/24 )
Branches : 0% ( 0/4 )
Functions : 14.29% ( 1/7 )
Lines : 50% ( 12/24 )
================================================================================
We've increased coverage, and we can see our console.info('sendEvent')
. We can do better though, can you write a spy for CustomEvent
to make sure the right eventName
is passed?
When you're testing AJAX calls, you'll want to ensure that you've tested all your branching logic, but who wants to stand up a webserver to run real integration tests?
Let's start by adding another describe
to reachability_spec.js
describe('checkReachability', function() {
beforeEach(function() {
// helps with test isolation
stopReachability();
});
it('makes an ajax call', function() {
pending();
});
it('dispatches reachabilitySuccessEvent on success', function() {
pending();
});
it('dispatches reachabilityErrorEvent on error', function() {
pending();
});
it('calls startReachability on complete', function() {
pending();
});
});
Since we're using jQuery in our dummy app for the first time, we'll need to add the dependency:
# Gemfile
group :development, :test do
gem 'jquery-rails'
end
We'll also need to expose it in our spec_helper.js
//= require jquery
The first thing we'll want to test, is that the ajax call is even being made. We'll create a spy on $
rather than window
since this API makes use of JQuery, and let's not forget that we'll want to ensure our internal API also gets hit, using spyOn(window, 'stopReachability')
.
Using Jasmine, we can also assert that the ajax call is made with the right configuration:
it('makes an ajax call', function() {
spyOn(window, 'stopReachability');
spyOn($, "ajax");
checkReachability();
expect(window.stopReachability).toHaveBeenCalled();
expect($.ajax.calls.mostRecent().args[0].url).toEqual(reachabilityEndpoint);
expect($.ajax.calls.mostRecent().args[0].type).toEqual(reachabilityHTTPMethod);
expect($.ajax.calls.mostRecent().args[0].cache).toEqual(false);
expect($.ajax.calls.mostRecent().args[0].timeout).toEqual(reachabilityTimeout);
});
Now let's re-run our tests:
$ bundle exec rake teaspoon
.....sendEvent
.stopReachability
checkReachability
.
Finished in 0.00700 seconds
7 examples, 0 failures
=============================== Coverage summary ===============================
Statements : 75% ( 18/24 )
Branches : 0% ( 0/4 )
Functions : 42.86% ( 3/7 )
Lines : 75% ( 18/24 )
================================================================================
Coverage has been increased, but we're still missing our branching logic - we can mock the ajax responses so that we can test each branch:
it('dispatches reachabilitySuccessEvent on success', function() {
spyOn(window, 'sendEvent');
spyOn($, "ajax").and.callFake(function(e) {
e.success({});
});
checkReachability();
expect(window.sendEvent).toHaveBeenCalledWith(reachabilitySuccessEvent);
});
By combining spyOn
with .and.callFake
we're given context to the function that was called and explicitly call the method that we want - in this case, success
. Can you write the two other tests for error
and complete
?
Let's open our coverage report, located @ rails_engine/coverage/default/index.html
If we click through, to reachability.js
, we'll see that stopReachability
looks entirely covered. Jasmine and teaspoon are no different when it comes to coverage - we didn't actually write any tests for that method, but it's been hit by other tests many times.
Can you add the tests for startReachability
and stopReachability
?
describe('startReachability', function() {
beforeEach(function() {
stopReachability();
});
it('assigns reachabilityInterval when unset', function() {
pending();
});
it('does not assign reachabilityInterval when set', function() {
pending();
});
});
describe('stopReachability', function() {
it('invalidates the reachabilityInterval', function() {
pending();
});
});