Skip to content

Instantly share code, notes, and snippets.

@emk
Created April 7, 2013 01:44
Show Gist options
  • Save emk/5328495 to your computer and use it in GitHub Desktop.
Save emk/5328495 to your computer and use it in GitHub Desktop.
Testing Ember.js with Mocha, Sinon and Chai, including unit and integration tests. Runnable version available at: http://jsfiddle.net/ekidd/hCsws/13/
<!-- Mocha test output goes here. -->
<div id="mocha"></div>
<!-- Handlebars templates for our application. -->
<script type="text/x-handlebars">
<h1>Testing Ember.js with Mocha</h1>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="index">
<p>{{#linkTo 'employees'}}Show employees{{/linkTo}}</p>
</script>
<script type="text/x-handlebars" data-template-name="employees">
<h2>Employees</h2>
<ul>
{{#each employee in controller}}
<li>{{#linkTo 'employee' employee}}{{employee.name}}{{/linkTo}}</li>
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="employee">
<h2>{{name}}</h2>
<p>Salary: <span class="salary">${{salary}}</span></p>
<button {{action 'giveRaise'}}>Give Raise</button>
{{#if managedBy}}
<p class="managed-by">
Managed by: {{#linkTo 'employee' managedBy}}{{managedBy.name}}{{/linkTo}}
</p>
{{/if}}
{{#if manages}}
<h3>Manages</h3>
<ul class="manages">
{{#each employee in manages}}
<li>{{#linkTo 'employee' employee}}{{employee.name}}{{/linkTo}}</li>
{{/each}}
</ul>
{{/if}}
<p>{{#linkTo 'employees'}}All employees{{/linkTo}}</p>
</script>
//================================================================================
// Application Code
window.App = Ember.Application.create();
// Our normal data store, which we be overridden when testing.
App.Store = DS.Store.extend({
revision: 12,
adapter: DS.RESTAdapter.create()
});
App.Employee = DS.Model.extend({
name: DS.attr("string"),
salary: DS.attr("number"),
managedBy: DS.belongsTo("App.Employee"),
manages: DS.hasMany("App.Employee")
});
App.Router.map(function () {
this.route("index", { path: "/" });
this.route("employees", { path: "/employees" });
this.route("employee", { path: "/employee/:employee_id" });
});
App.EmployeesRoute = Ember.Route.extend({
model: function (params) {
return App.Employee.find();
}
});
App.EmployeeRoute = Ember.Route.extend({
model: function (params) {
return App.Employee.find(params.employee_id);
}
});
App.EmployeesController = Ember.ArrayController.extend();
App.EmployeeController = Ember.ObjectController.extend({
giveRaise: function () {
this.set("salary", this.get("salary") * 1.10);
}
});
// Declare this explicitly so we can test it.
App.EmployeeView = Ember.View.extend({
// Only needed so we can be called outside of a route by our unit tests.
templateName: 'employee'
});
//================================================================================
// Test Code
// Configure Mocha, telling both it and chai to use BDD-style tests.
mocha.setup("bdd");
chai.should();
// Replace our fixture-based store with a REST-based store for testing, so we
// don't need a server. We disable simulateRemoteResponse so that objects will
// appear to load at the end of every Ember.run block instead of waiting for a
// timer to fire.
App.Store = DS.Store.extend({
revision: 12,
adapter: DS.FixtureAdapter.create({ simulateRemoteResponse: false })
});
// Declare some fixture objects to use in our test application. There's
// nothing like factory_girl or machinist yet.
App.Employee.FIXTURES = [{
id: 1,
name: "Jane Q. Public",
salary: 80000,
managedBy: null,
manages: [2]
}, {
id: 2,
name: "John Q. Public",
salary: 60000,
managedBy: 1,
manages: []
}];
mocha.setup('bdd');
// Run before each test case.
beforeEach(function () {
// Put the application into a known state, and destroy the defaultStore.
// Be careful about DS.Model instances stored in App; they'll be invalid
// after this.
// Currently broken, see: https://github.com/emberjs/data/issues/847
//App.reset();
// Display an error if asynchronous operations are queued outside of
// Ember.run. You need this if you want to stay sane.
Ember.testing = true;
});
// Run after each test case.
afterEach(function () {
Ember.testing = false;
});
// Load associations immediately, instead of waiting for FixtureAdapter's
// asynchronous loads. Basically, all we need to do is access each object
// from inside Ember.run.
// TODO: We can't test this or insert where needed until App.reset() works.
// TODO: Handle hasMany.
function loadAssociations(object /*, paths... */) {
var paths = Array.prototype.slice.call(arguments, 1);
for (var i = 0; i < paths.length; i++) {
var components = paths[i].split(".");
for (var j = 0; j < components.length; j++) {
Ember.run(function () {
var path = components.slice(0, j+1).join(".");
object.get(path);
});
}
}
}
// Sample model test.
describe("App.Employee", function () {
it("has a name", function () {
var jane;
Ember.run(function () {
// Won't actually load until the end of the run-block.
jane = App.Employee.find(1);
});
jane.get("name").should.equal("Jane Q. Public");
});
});
// Sample controller test.
describe("App.EmployeeController", function () {
var model, controller;
beforeEach(function () {
Ember.run(function () {
model = App.Employee.createRecord({ salary: 100000 });
controller = App.EmployeeController.create({ content: model });
});
});
it("can give the employee a raise", function () {
var oldSalary = model.get("salary");
Ember.run(function () {
controller.giveRaise();
});
model.get("salary").should.be(oldSalary * 1.1);
});
});
// Sample view test.
describe("App.EmployeeView", function () {
var controller, view;
beforeEach(function () {
Ember.run(function () {
var model = App.Employee.find(1);
controller = App.EmployeeController.create({
// We need a container to test views with linkTo.
container: App.__container__,
content: model
});
// If for some reason we want to isolate this, we can use
// a sinon stub to intercept certain calls.
sinon.stub(controller, "giveRaise");
view = App.EmployeeView.create({
controller: controller,
context: controller
});
view.append(); // Hook up to our document.
});
});
afterEach(function () {
Ember.run(function () {
view.remove(); // Unhook from our document.
});
});
it("shows the employee's name", function () {
// This uses a chai-jquery assertion.
view.$("h2").should.have.text("Jane Q. Public");
view.$(".manages li").text().should.match(/John/);
});
it("has a button which gives the employee a raise", function () {
view.$("button").click();
// We use a sinon-chai method here.
controller.giveRaise.should.have.been.calledOnce;
});
});
// Sample acceptance test.
describe("Employee features", function () {
it("give John's boss a raise", function () {
$("a:contains('Show employees')").click();
$("a:contains('John')").click();
$(".managed-by a").click();
$(".salary").should.have.text("$80000");
$("button:contains('Give Raise')").click();
$(".salary").should.have.text("$88000");
});
});
// Run all our test suites. Only necessary in the browser.
mocha.run();
@arlarzelere3
Copy link

Any chance that this will be updated to handle one to many or many to many relationships?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment