Test-Driven Meteor: A Very Basic Tutorial
Welcome to your first day at the Meteor Widget factory! Today we will get started building our widget app. Specs include clicking a button and seeing "New Widget!" appear on the screen. Most importantly, we will develop this feature test-first.
We will use just a few tools: Meteor, Velocity & Jasmine.
Let's start by exploring the Velocity test framework with a simple button-clicking test.
Create a new app
Let's create our app and change into the new directory:
meteor create widget_maker
cd widget_maker
We are going to keep this app as simple as possible. We will wait to add features like routing and user logins. But we will add 2 packages, one for style and one for testing.
First package: bootstrap css. So we can prototype with some style.
meteor add twbs:bootstrap
Next, we will add the testing package
About Velocity
There is much you will want to know about Velocity, in addition to how it works. It is not developed by the Meteor Development Group and it is not the only test framework available. But it is the most popular and, like most open source packages, community members are building it, for free, in their own time. Learn more at their github repo: (https://github.com/meteor-velocity/velocity)
*Note: * As of this writing, the current release of Velocity is 0.6.3.
One key point: Velocity runs your tests, but it is not itself the tests. It runs Meteor on a separate server, executes fancy things like mirroring in the background, and allows your tests to run against a real, live Meteor application, that is separate from the instance that you typically interact with.
Add Jasmine
You will write tests using your chosen JavaScript testing framework. For this tutorial, we will use Jasmine.
Other options include Mocha, Jasmine and Cucumber. Perhaps someday we will hash out the differences, weigh the pros and cons, and make a more deliberate choice. But, most people agree: Jasmine works great when you know how to use it.
Let's learn how to use it. Start by adding the package:
meteor add sanjo:jasmine
This packages adds Velocity, among other dependencies, but it does not add the 'test reporter', the thing that actually displays test results on the screen. So we add another package:
meteor add velocity:html-reporter
Maybe someday you will want to choose a different reporter; there are several. We might want to keep our tests confined to our terminal using the console-reporter. But people commonly start with the HTML reporter. We can see it working when we start our app:
meteor
Open your browser and go to (http://localhost:3000). You should see this:
Add sample tests
0 tests passed because we have written 0 tests. But Velocity offers us a unique feature: It will add sample tests for us. Click the button 'add jasmine-client-integration tests'. Wait a few moments. Velocity will create some new files:
Add our own test
Let's wipe out those samples and write our own test. When we ran 'meteor create', it gave us a tiny little app for free, with a button. Let's test that button.
Delete the file tests/jasmine/client/integration/sample/spec/PlayerSpec.js
Make a new file: tests/jasmine/client/integration/sample/spec/buttonSpec.js
And start with this code:
describe('clicking the button', function () {
// reset the counter before each test
beforeEach(function() {
Session.set('counter', 0);
});
it('should show how many times the button was pressed', function () {
// Get the text we are testing
var text = $('p#buttonText').text();
// click the button
$('button').click();
// assert that we see 'You've pressed the button times.'
expect(text).toEqual("You've pressed the button 1 times.");
});
});
// tests/jasmine/client/integration/sample/spec/buttonSpec.js
And you should see the results after you save:
In our browser, we see that our app is running on localhost:3000
, and the text still says "You've pressed the button 0 times." That's a good thing.
Velocity runs our test app separately from our development app. We do not want our test app to interfere with our development app. In fact, we want our test app to run in a completely separate test environment. This becomes especially important when we start making collections and saving data to the database. In the test environment, we will create & delete documents willy-nilly, again and again. We do not want to touch our development database at all.
What we've done
In this first step, we created a new app, added Velocity and Jasmine, and created a simple test. We ran the sample 'button-clicking' app that Meteor gave us, and tested that clicking the button shows the correct text.
Next, let's delete all this code and get started on our real app.
Test first feature
This app is all about making widgets. We will put off discussing the nature and value of widgets. We won't even persist our widget to the database until the next lesson. For now, we know of one feature: When we click a button, we want to see a new widget represented by a div on the page saying 'New Widget'.
First, wipe out the boilerplate code in widget_maker.js
. The test we wrote previously will fail. Delete the entire directory tests/jasmine/client/integration/sample/
.
Create a new directory in it's place: /tests/jasmine/client/integration/widgets/
and in that folder, start a new file:
describe('creating a widget', function () {
beforeEach(function() {
});
it('clicking the button adds a div', function () {
// test here
});
});
// /tests/jasmine/client/integration/widgets/widgetSpec.js
In widget_maker.html
, clean up the HTML. We start with a single template, called within the body
tag:
<head>
<title>Widget Maker</title>
</head>
<body>
{{> widgets }}
</body>
<template name="widgets">
<h1>Your Widgets</h1>
</template>
<!-- widget_maker.html -->
And also, clear out widget_maker.js
. We will start fresh with a single 'widgets' template:
if (Meteor.isClient) {
Template.widgets.events({});
Template.widgets.helpers({});
}
And now, back to the test. We want to click a button, and we want that event to create a new div on the page. We don't actually have the button, or any other code, so after we write the test, Velocity will run it and show us that it failed.
In widgetSpec.js
, we add to our test:
it('clicking the button adds a div', function () {
// click the button
$('button#createWidget').click();
// gather the newly created divs
$newDivs = $('div.widget');
// assert the number of new divs
expect($newDivs.length).toEqual(1);
});
And in our browser, we see our failing test:
You might get fatigued scanning the stack trace. Most of those lines refer to the innards of the Jasmine package; only one line indicates where our actual test failed:
at Object.<anonymous> (http://localhost:60492/tests/jasmine/client/integration/widgets/widgetSpec.js?ac0ef60468124c2c3c2e31aa29a0f62dd0059c60:14:29)
Something else is missing: we tried to click a non-existent button but the test did not tell us, "the button you tried to click does not exist."
We understand, though. That's our starting place. Let's add the button:
<template name="widgets">
<h1>Your Widgets</h1>
<button id="createWidget" class="btn btn-success">Create a New Widget</button>
</template>
<!-- widget_maker.html -->
And next, let's handle the event: clicking that button should add a widget. This is our first opportunity to write actual code and we can do anything we want so long as it makes our test pass. For the purposes of learning, we will start with a non-Meteor style, using jQuery to append the div. Afterwards, we will refactor.
Template.widgets.events({
'click button#createWidget': function (event, template) {
$("body").append("<div class='widget'>New Widget</div>");
}
});
// widget_maker.js
Back in the browser, our test passes. Again, notice that we did not click the button ourselves, and we don't see any 'New Widget' divs. But, Velocity ran the app in a separate instance, clicked the button, and told us the good news in green.
Our test ensures clicking the button does indeed add a div to to body. But-- we haven't actually created a widget. More importantly, we are working with Meteor and we do not want to do things like append divs using jQuery. We would rather leverage Meteor's reactivity.
Clicking that widget-creating button should actually create a brand new widget, and the template should react accordingly.
We need to refactor. Test driven development demands that we refactor, so that we end up with the best possible code, not just our first grasp at whatever causes the test to pass.
The test will now tell us, whatever happens, whether clicking that button causes 'New Widget' to appear.
In the next part, we will refactor our code so that clicking the button will insert a new widget into the Widgets collection.
Widgets Collection
We want to create widgets, not just append text to the HTML. After we click that button, we want to persist a widget to the database.
But we will let the tests guide us. For instance, we can write a test that, after clicking the button, the WidgetsCollection should include a new document. Notice, we have not yet created the WidgetsCollection. As an exercise, we will write the tests first and follow the trail from red to green.
Red means Success
In this test, we will start by asserting that we start with 0 documents in the WidgetsCollection.
it('clicking the button saves a new Widget', function () {
// First, check that we have 0 documents
var widget = WidgetsCollection.findOne({});
expect(widget).toBe(undefined);
)};
// /tests/jasmine/client/integration/widgets/widgetSpec.js
The test has successfully notified us that we need to define the WidgetsCollection.
We will add the WidgetsCollection code and see what the test tells us next.
WidgetsCollection = new Mongo.Collection('widgets');
// widget_maker.js
Yep, the test has changed to passing!
We can assure ourselves that this test is actually working but causing it to fail again, this time testing that we get 1 document instead of 0. Change 1 line in the test:
it('clicking the button saves a new Widget', function () {
// First, check that we have 0 documents
var widget = WidgetsCollection.findOne({});
// Test that, instead of 'undefined', we get an object of some sort. This should fail.
expect(!!widget).toBe(true);
)};
// /tests/jasmine/client/integration/widgets/widgetSpec.js
Good-- the test fails. We know that it's querying the WidgetsCollection and not finding any documents, as we expect. We can move on to clicking the button and actually creating a widget.
Note: Revert that 1 line, so it expects 0 documents:
// First, check that we have 0 documents
var widget = WidgetsCollection.findOne({});
expect(widget).toBe(undefined);
Create the widget
Add some lines to that test: clicking the button should produce a new widget, size 'medium'.
it('clicking the button saves a new Widget', function () {
// First, check that we have 0 documents
var widget = WidgetsCollection.findOne({});
expect( widget ).toBe(undefined);
// click the button
$('button#createWidget').click();
// This time, we should have a widget, and its size should be 'medium'
var widget = WidgetsCollection.findOne({});
expect( widget.size ).toBe("medium");
});
// /tests/jasmine/client/integration/widgets/widgetSpec.js
The failing test tells us we did not get a document, we only got 'undefined', and 'undefined' doesn't have a size.
Time to get back into our application and make the test pass. Clicking the button should actually insert a document. We could use a Meteor method, but let's stay simple: In Template.widgets.events
, in the 'click button' event, we will insert a document with size: 'medium'
into the widgets collection.
But after adding this code, we will encounter a tricky testing gotcha.
Template.widgets.events({
'click button#createWidget': function (event, template) {
WidgetsCollection.insert({
size: "medium"
});
$("body").append("<div class='widget'>New Widget</div>");
}
});
Look at the failing test and guess what happened:
Managing the test database
The test says that it found an actual document, but it was expecting to find nothing at all. Here's the thing about testing: sometimes you end up testing the tests instead of the application. These tests are running in their own magical world where anything goes, so long as we will it. And if we don't will it, it might go awry.
In this world, we want to start every test with an empty database and then see if our code causes the changes we expect.
But here, this test inserts a new document into the database every time it runs. Then, the next time it runs, it fails, because it expects to find nothing, but instead it finds the document from the previous run.
If we were building a Ruby on Rails application, we might take the time to implement 'DatabaseCleaner', a package to help manage this problem. Scan the readme for extra credit.
In Node, we can explore similar packages: node-database-cleaner
Keeping a kosher test environment can be tricky. Specifically with the test database, different people will pursue different paths, maybe using fixtures or some other strategy, but in our case, we will remove all documents before running each test.
describe('creating a widget', function () {
beforeEach(function() {
WidgetsCollection.find().forEach( function (widget) {
WidgetsCollection.remove({_id: widget._id});
});
});
...
});
Another trick: we cannot use WidgetsCollection.remove({})
because we are using Meteor, and we are testing client-side code. Meteor doesn't allow the client to remove documents willy-nilly, so we need to laboriously remove each document by id. In our actual application, we won't be wiping the database.
But, here we are in the 'test' environment and we can establish whatever state of the app we want. In the future, we may decide to seed the database with different widgets-- small, medium, and large. But, for now, when the test runs against an empty database, our test passes.
Testing by hand
We can use our own two hands to manually test that, yes, in the browser, clicking the button saves a new widget.
Next Steps
First, we wrote a test that failed and then the test told us why it failed: some code was missing or incorrect. Next, we added some code and the test passed. Last, we refactored our code, checking to see the test stayed green while we improved the app. As we went, we managed our test environment.
What about next steps? Users might want to select 'small', 'medium' or 'large'. This same process can guide us:
First we write a test: After selecting 'small' and clicking the button, we get a 'small widget'
. That test fails, so we add the select box and the new event handler. The test passes. Then, we refactor to improve the UX and add more options like color: 'blue'
.
Summary
Please note: if you are like me, you will find that testing Meteor apps is challenging. Discovering best practices is difficult. Developing your test suite will take time.
It might even make sense to practice these techniques separately from your app, while you wait for the Meteor testing ecosystem to mature.
But, keep your eye on the ball. Eventually, you might realize great profits from your tests. As your widget application becomes increasingly popular and you add more features, the tests can guide your code and you will feel confident that everything works.