Chapter 12. Testing in Meteor
In this final chapter, we will discuss how we can test a Meteor app.
Testing is a comprehensive topic and it goes beyond the scope of this chapter. To keep it simple, we will briefly cover two tools available, as they are certainly different, and show a simple example for each.
In this chapter, we will cover the following topics:
- Testing the
reactive-timer
package - Using Jasmine to conduct unit tests on our app
- Using Nightwatch to conduct acceptance tests on our app
Note
If you want to jump right into the chapter and follow the examples, download the code of Chapter 10, Deploying Our App, which contains the finished example app, either from the book's web page at https://www.packtpub.com/books/content/support/17713 or from the GitHub repository at https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter10.
Types of tests
Tests are pieces of code that test other pieces of code or functionality of an app.
We can divide tests into four general groups:
- Unit test: In this test, we test only a small unit of our code. This can, for example, be a function or a piece of code. Unit tests should not call other functions, write to the hard disk or database, or access the network. If such functionality is needed, one should write stubs, which are functions that return the expected value without calling the real function.
- Integrations test: In this test, we combine multiple tests and run them in different environments to make sure that they still work. The difference in this test compared to the unit test is that we are actually running connected functionalities, such as calling the database.
- Functional test: This can be a unit test or tests in the interface, but will only test the functionality of a feature/function without checking for side effects, such as whether or not variables were cleaned up properly.
- Acceptance test: This runs tests on the full system, which can, for example, be a web browser. The idea is to mimic the actual user as much as possible. These tests are very similar to user stories that define a feature. The downside is that they make it hard to track down bugs, as the test occurs on a higher level.
In the following examples, we will mostly write functional tests for simplicity.
Testing packages
In the previous chapter, we built a package out of the ReactiveTimer
object. A good package should always contain unit tests so that people can run them and be sure that changes to that package don't break its functionality.
Meteor provides a simple unit test tool for packages, called TinyTest
, which we will use to test our package:
- To add tests, we need to copy the
meteor-book:reactive-timer
package, which we built in the previous chapter, to themy-meteor-blog/packages
folder of our app. This way, we can make changes to the package, as Meteor will prefer the package in thepackages
folder over one in its package servers. If you removed the package, simply add it back using the following command:$ meteor add meteor-book:reactive-timer
Note
Additionally, we need to make sure we delete the
my-meteor-blog/client/ReactiveTimer.js
file, which we should have if we used the code example from Chapter 10, Deploying Our App, as a basis. - Then we open the
package.js
file from ourpackages
folder and add the following lines of code to the end of the file:Package.onTest(function (api) { api.use('meteor-book:reactive-timer', 'client'); api.use('tinytest', 'client'); api.addFiles('tests/tests.js', 'client'); });
This will include our
meteor-book:reactive-timer
package andtinytest
when running tests. It will then run thetests.js
file, which will contain our unit tests. - Now, we can create the tests by adding a folder called
tests
to our package's folder and create a file calledtests.js
inside.Currently, the
tinytest
package is not documented by Meteor, but it is tiny, which means it is very simple.Basically, there are two functions,
Tinytest.add(test)
andTinytest.addAsync(test, expect)
. They both run a simple test function, which we can pass or fail usingtest.equal(x, y)
,test.isTrue(x)
, ortest.isUndefined(x)
.For our package tests, we will simply test whether
ReactiveTimer._intervalId
is not null after we started the timer, and we will know whether the timer runs or not.
Adding package tests
The test is built by first describing what will be tested.
To test for _intervalId
, we add the following lines of code to our tests.js
file:
Tinytest.add('The timer set the _intervalId property', function (test) { var timer = new ReactiveTimer(); timer.start(1); test.isTrue(timer._intervalId !== null); timer.stop(); });
Then we start a timer and test whether its _intervalId
property is not null anymore. At the end, we stop the timer again to clean up the test.
The next test we will add to our tests.js
file will be asynchronous, as we need to wait for the timer to run at least once:
Tinytest.addAsync('The timer run', function (test, expect) { var run = false, timer = new ReactiveTimer(); timer.start(1); Tracker.autorun(function(c){ timer.tick(); if(!c.firstRun) run = true; }); Meteor.setTimeout(function(){ test.equal(run, true); timer.stop(); expect(); }, 1010); });
Let's take a look at what is happening in this asynchronous test:
- First, we started the timer again with an interval of 1 second and created a variable called
run
. We then switched this variable totrue
only when our reactiveTracker.autorun()
function ran. Note that we usedif(!c.firstRun)
to prevent therun
variable from being set when the function runs the first it's executed, as we only want the "tick" after 1 second to count. - We then used the
Meteor.setTimeout()
function to check whetherrun
was changed totrue
. Theexpect()
tellsTinytest.addAsync()
that the test is over and outputs the result. Note that we also stopped the timer, as we always need to clean up after each test.
Running the package tests
To finally run the test, we can run the following command from our app's root folder:
$ meteor test-packages meteor-book:reactive-timer
This will start a Meteor app and run our package tests. To see them, we navigate to http://localhost:3000
:
Tip
We can also run a test for more than one package at the same time by naming multiple packages separated by spaces:
$ meteor test-packages meteor-book:reactive-timer iron:router
To see if the test works, we will deliberately make it fail by commenting out Meteor.setInterval()
in the my-meteor-book/packages/reactive-timer/ReactiveTimer.js
file, as shown in the following screenshot:
We should always try to make our test fail, as a test could also be written in a way that it never succeeds or fails (for example, when expect()
was never called). This would stop the execution of other tests, as the current one could never finish.
A good rule of thumb is to test functionality as if we are looking at a black box. If we customize our tests too much depending on how a function is written, we will have a hard time fixing tests as we improve our functions.
Adding package tests
The test is built by first describing what will be tested.
To test for _intervalId
, we add the following lines of code to our tests.js
file:
Tinytest.add('The timer set the _intervalId property', function (test) { var timer = new ReactiveTimer(); timer.start(1); test.isTrue(timer._intervalId !== null); timer.stop(); });
Then we start a timer and test whether its _intervalId
property is not null anymore. At the end, we stop the timer again to clean up the test.
The next test we will add to our tests.js
file will be asynchronous, as we need to wait for the timer to run at least once:
Tinytest.addAsync('The timer run', function (test, expect) { var run = false, timer = new ReactiveTimer(); timer.start(1); Tracker.autorun(function(c){ timer.tick(); if(!c.firstRun) run = true; }); Meteor.setTimeout(function(){ test.equal(run, true); timer.stop(); expect(); }, 1010); });
Let's take a look at what is happening in this asynchronous test:
- First, we started the timer again with an interval of 1 second and created a variable called
run
. We then switched this variable totrue
only when our reactiveTracker.autorun()
function ran. Note that we usedif(!c.firstRun)
to prevent therun
variable from being set when the function runs the first it's executed, as we only want the "tick" after 1 second to count. - We then used the
Meteor.setTimeout()
function to check whetherrun
was changed totrue
. Theexpect()
tellsTinytest.addAsync()
that the test is over and outputs the result. Note that we also stopped the timer, as we always need to clean up after each test.
Running the package tests
To finally run the test, we can run the following command from our app's root folder:
$ meteor test-packages meteor-book:reactive-timer
This will start a Meteor app and run our package tests. To see them, we navigate to http://localhost:3000
:
Tip
We can also run a test for more than one package at the same time by naming multiple packages separated by spaces:
$ meteor test-packages meteor-book:reactive-timer iron:router
To see if the test works, we will deliberately make it fail by commenting out Meteor.setInterval()
in the my-meteor-book/packages/reactive-timer/ReactiveTimer.js
file, as shown in the following screenshot:
We should always try to make our test fail, as a test could also be written in a way that it never succeeds or fails (for example, when expect()
was never called). This would stop the execution of other tests, as the current one could never finish.
A good rule of thumb is to test functionality as if we are looking at a black box. If we customize our tests too much depending on how a function is written, we will have a hard time fixing tests as we improve our functions.
Running the package tests
To finally run the test, we can run the following command from our app's root folder:
$ meteor test-packages meteor-book:reactive-timer
This will start a Meteor app and run our package tests. To see them, we navigate to http://localhost:3000
:
Tip
We can also run a test for more than one package at the same time by naming multiple packages separated by spaces:
$ meteor test-packages meteor-book:reactive-timer iron:router
To see if the test works, we will deliberately make it fail by commenting out Meteor.setInterval()
in the my-meteor-book/packages/reactive-timer/ReactiveTimer.js
file, as shown in the following screenshot:
We should always try to make our test fail, as a test could also be written in a way that it never succeeds or fails (for example, when expect()
was never called). This would stop the execution of other tests, as the current one could never finish.
A good rule of thumb is to test functionality as if we are looking at a black box. If we customize our tests too much depending on how a function is written, we will have a hard time fixing tests as we improve our functions.
Testing our meteor app
To test the app itself, we can use Velocity Meteor's official testing framework.
Velocity itself doesn't contain tools for testing, but rather gives testing packages such as Jasmine or Mocha a unified way to test Meteor apps and report their output in the console or the apps interface itself using the velocity:html-reporter
package.
Let's quote their own words:
Velocity watches your tests/ directory and sends test files to the correct testing plugin. The testing plugin performs the tests and sends results for each test back to Velocity as they complete. Velocity then combines the results from all of the testing plugins and outputs them via one or more reporting plugins. When the app or tests change, Velocity will rerun your tests and reactively update the results.
This is taken from http://velocity.meteor.com. Additionally, Velocity adds features such as Meteor stubs and automatic stubbing. It can create mirror apps for isolated testing and run setup code (fixtures).
We will now take a look at unit and integration tests using Jasmine and acceptance tests using Nightwatch.
Testing using Jasmine
To use Jasmine with Velocity, we need to install the sanjo:jasmine
package along with the velocity:html-reporter
package.
To do this, we'll run the following command from inside our apps folder:
$ meteor add velocity:html-reporter
Then we install Jasmine for Meteor using the following command:
$ meteor add sanjo:jasmine
In order that Velocity can find the tests, we need to create the following folder structure:
- my-meteor-blog - tests - jasmine - client - unit - integration - server - unit
Now, when we start the Meteor server using $ meteor
, we will see that the Jasmine package has already created two files in the /my-meteor-blog/tests/jasmine/server/unit
folder, which contains stubs for our packages.
Adding unit tests to the server
Now we can add unit tests to the client and the server. In this book, we will only add a unit test to the server and later add integration tests to the client to stay within the scope of this chapter. The steps to do so are as follows:
- First, we create a file called
postSpecs.js
within the/my-meteor-blog/tests/jasmine/server/unit
folder and add the following command:describe('Post', function () {
This will create a test frame describing what the test inside will be about.
- Inside the test frame, we call the
beforeEach()
andafterEach()
functions, which will run before and after each test, respectively. Inside, we will create stubs for all Meteor functions usingMeteorStubs.install()
and clean them afterwards usingMeteorStubs.uninstall()
:beforeEach(function () { MeteorStubs.install(); }); afterEach(function () { MeteorStubs.uninstall(); });
Note
A stub is a function or object that mimics its original function or object, but doesn't run actual code. Instead, a stub can be used to return a specific value that the function we test depends on.
Stubbing makes sure that a unit test tests only a specific unit of code and not its dependencies. Otherwise, a break in a dependent function or object would cause a chain of other tests to fail, making it hard to find the actual problem.
- Now we can write the actual test. In this example, we will test whether the
insertPost
method we created previously in the book inserts the post, and makes sure that no duplicate slug will be inserted:it('should be correctly inserted', function() { spyOn(Posts, 'findOne').and.callFake(function() { // simulate return a found document; return {title: 'Some Tite'}; }); spyOn(Posts, 'insert'); spyOn(Meteor, 'user').and.returnValue({_id: 4321, profile: {name: 'John'}}); spyOn(global, 'moment').and.callFake(function() { // simulate return the moment object; return {unix: function(){ return 1234; }}; });
First, we create stubs for all the functions we are using inside the
insertPost
method to make sure that they return what we want.Especially, take a look at the
spyOn(Posts, "findOne")
call. As we can see, we call a fake function and return a fake document with just a title. Actually, we can return anything as theinsertPost
method only checks whether a document with the same slug was found or not. - Next, we actually call the method and give it some post data:
Meteor.call('insertPost', { title: 'My Title', description: 'Lorem ipsum', text: 'Lorem ipsum', slug: 'my-title' }, function(error, result){
- Inside the callback of the method, we add the actual tests:
expect(error).toBe(null); // we check that the slug is returned expect(result).toContain('my-title'); expect(result.length).toBeGreaterThan(8); // we check that the post is correctly inserted expect(Posts.insert).toHaveBeenCalledWith({ title: 'My Title', description: 'Lorem ipsum', text: 'Lorem ipsum', slug: result, timeCreated: 1234, owner: 4321, author: 'John' }); }); });
First, we check whether the error object is null. Then we check whether the resultant slug of the method contains the
'my-title'
string. Because we returned a fake document in thePosts.findOne()
function earlier, we expect our method to add some random number to the slug such as'my-title-fotvadydf4rt3xr'
. Therefore, we check whether the length is bigger than the eight characters of the original'my-title'
string.At last, we check whether the
Post.insert()
function was called with the expected values.Note
To fully understand how you can test Jasmine, take a look at the documentation at https://jasmine.github.io/2.0/introduction.html.
You can also find a good cheat sheet of Jasmine functions at http://www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing.
- Finally, we close the
describe(...
function at the beginning:});
If we now start our Meteor app again using $ meteor
, after a while we'll see a green dot appearing in the top-right corner.
Clicking on this dot gives us access to Velocity's html-reporter
and it should show us that our test has passed:
To make our test fail, let's go to our my-meteor-blog/methods.js
file and comment out the following lines:
if(Posts.findOne({slug: postDocument.slug})) postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);
This will prevent the slug from getting changed, even if a document with the same slug already exists, and fail our test. If we go back and check in our browser, we should see the test as failed:
We can add more tests by just adding a new it('should be xyz', function() {...});
function.
Adding integration tests to the client
Adding integration tests is as simple as adding unit tests. The difference is that all the test specification files go to the my-meteor-blog/tests/jasmine/client/integration
folder.
Integration tests, unlike unit tests, run in the actual app environment.
Adding a test for the visitors
In our first example test, we will test to ensure that visitors can't see the Create Post button. In the second test, we will log in as an administrator and check whether we are able to see it.
- Let's create a file named
postButtonSpecs.js
in ourmy-meteor-blog/tests/jasmine/client/integration
folder. - Now we add the following code snippet to the file and save it:
describe('Vistors', function() { it('should not see the create posts link', function () { var div = document.createElement('DIV'); Blaze.render(Template.home, div); expect($(div).find('a.createNewPost')[0]).not.toBeDefined(); }); });
Here we manually create a div
HTML element and render the home
template inside. After that, we check whether the a.createNewPost
link is present.
If we go back to our app, we should see the integration test added and passed:
Tip
In case the test doesn't show up, just quit and restart the Meteor app in the terminal again.
Adding a test for the admin
In the second test, we will first log in as administrator and then check again whether the button is visible.
We add the following code snippet to the same postButtonSpecs.js
file as the one we used before:
describe('The Admin', function() { afterEach(function (done) { Meteor.logout(done); }) it('should be able to login and see the create post link', function (done) { var div = document.createElement('DIV'); Blaze.render(Template.home, div); Meteor.loginWithPassword('johndoe@example.com', '1234', function (err) { Tracker.afterFlush(function(){ expect($(div).find('a.createNewPost')[0]).toBeDefined(); expect(err).toBeUndefined(); done(); }); }); }); });
Here we add the home
template to a div
again, but this time we log in as an admin user, using our admin credentials. After we have logged in, we call Tracker.afterFlush()
to give Meteor time to re-render the template and then check whether the button is now present.
Because this test runs asynchronously, we need to call the done()
function, which we passed as an argument to the it()
function, telling Jasmine that the test is over.
Note
Our credentials inside the test file are secure, as Meteor doesn't bundle files in the tests
directory.
If we now go back to our browser, we should see the two integration tests as passed:
After creating a test, we should always make sure we try to fail the test to see whether it actually works. To do so, we can simply comment out the a.createNewPost
link in my-meteor-blog/client/templates/home.html
.
Note
You can run Velocity tests using PhantomJS as follows:
$ meteor run --test
You first need to install PhantomJS globally with $ npm install -g phantomjs
. Be aware that this feature is experimental at the time of writing this book and might not run all your tests.
Testing using Jasmine
To use Jasmine with Velocity, we need to install the sanjo:jasmine
package along with the velocity:html-reporter
package.
To do this, we'll run the following command from inside our apps folder:
$ meteor add velocity:html-reporter
Then we install Jasmine for Meteor using the following command:
$ meteor add sanjo:jasmine
In order that Velocity can find the tests, we need to create the following folder structure:
- my-meteor-blog - tests - jasmine - client - unit - integration - server - unit
Now, when we start the Meteor server using $ meteor
, we will see that the Jasmine package has already created two files in the /my-meteor-blog/tests/jasmine/server/unit
folder, which contains stubs for our packages.
Adding unit tests to the server
Now we can add unit tests to the client and the server. In this book, we will only add a unit test to the server and later add integration tests to the client to stay within the scope of this chapter. The steps to do so are as follows:
- First, we create a file called
postSpecs.js
within the/my-meteor-blog/tests/jasmine/server/unit
folder and add the following command:describe('Post', function () {
This will create a test frame describing what the test inside will be about.
- Inside the test frame, we call the
beforeEach()
andafterEach()
functions, which will run before and after each test, respectively. Inside, we will create stubs for all Meteor functions usingMeteorStubs.install()
and clean them afterwards usingMeteorStubs.uninstall()
:beforeEach(function () { MeteorStubs.install(); }); afterEach(function () { MeteorStubs.uninstall(); });
Note
A stub is a function or object that mimics its original function or object, but doesn't run actual code. Instead, a stub can be used to return a specific value that the function we test depends on.
Stubbing makes sure that a unit test tests only a specific unit of code and not its dependencies. Otherwise, a break in a dependent function or object would cause a chain of other tests to fail, making it hard to find the actual problem.
- Now we can write the actual test. In this example, we will test whether the
insertPost
method we created previously in the book inserts the post, and makes sure that no duplicate slug will be inserted:it('should be correctly inserted', function() { spyOn(Posts, 'findOne').and.callFake(function() { // simulate return a found document; return {title: 'Some Tite'}; }); spyOn(Posts, 'insert'); spyOn(Meteor, 'user').and.returnValue({_id: 4321, profile: {name: 'John'}}); spyOn(global, 'moment').and.callFake(function() { // simulate return the moment object; return {unix: function(){ return 1234; }}; });
First, we create stubs for all the functions we are using inside the
insertPost
method to make sure that they return what we want.Especially, take a look at the
spyOn(Posts, "findOne")
call. As we can see, we call a fake function and return a fake document with just a title. Actually, we can return anything as theinsertPost
method only checks whether a document with the same slug was found or not. - Next, we actually call the method and give it some post data:
Meteor.call('insertPost', { title: 'My Title', description: 'Lorem ipsum', text: 'Lorem ipsum', slug: 'my-title' }, function(error, result){
- Inside the callback of the method, we add the actual tests:
expect(error).toBe(null); // we check that the slug is returned expect(result).toContain('my-title'); expect(result.length).toBeGreaterThan(8); // we check that the post is correctly inserted expect(Posts.insert).toHaveBeenCalledWith({ title: 'My Title', description: 'Lorem ipsum', text: 'Lorem ipsum', slug: result, timeCreated: 1234, owner: 4321, author: 'John' }); }); });
First, we check whether the error object is null. Then we check whether the resultant slug of the method contains the
'my-title'
string. Because we returned a fake document in thePosts.findOne()
function earlier, we expect our method to add some random number to the slug such as'my-title-fotvadydf4rt3xr'
. Therefore, we check whether the length is bigger than the eight characters of the original'my-title'
string.At last, we check whether the
Post.insert()
function was called with the expected values.Note
To fully understand how you can test Jasmine, take a look at the documentation at https://jasmine.github.io/2.0/introduction.html.
You can also find a good cheat sheet of Jasmine functions at http://www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing.
- Finally, we close the
describe(...
function at the beginning:});
If we now start our Meteor app again using $ meteor
, after a while we'll see a green dot appearing in the top-right corner.
Clicking on this dot gives us access to Velocity's html-reporter
and it should show us that our test has passed:
To make our test fail, let's go to our my-meteor-blog/methods.js
file and comment out the following lines:
if(Posts.findOne({slug: postDocument.slug})) postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);
This will prevent the slug from getting changed, even if a document with the same slug already exists, and fail our test. If we go back and check in our browser, we should see the test as failed:
We can add more tests by just adding a new it('should be xyz', function() {...});
function.
Adding integration tests to the client
Adding integration tests is as simple as adding unit tests. The difference is that all the test specification files go to the my-meteor-blog/tests/jasmine/client/integration
folder.
Integration tests, unlike unit tests, run in the actual app environment.
Adding a test for the visitors
In our first example test, we will test to ensure that visitors can't see the Create Post button. In the second test, we will log in as an administrator and check whether we are able to see it.
- Let's create a file named
postButtonSpecs.js
in ourmy-meteor-blog/tests/jasmine/client/integration
folder. - Now we add the following code snippet to the file and save it:
describe('Vistors', function() { it('should not see the create posts link', function () { var div = document.createElement('DIV'); Blaze.render(Template.home, div); expect($(div).find('a.createNewPost')[0]).not.toBeDefined(); }); });
Here we manually create a div
HTML element and render the home
template inside. After that, we check whether the a.createNewPost
link is present.
If we go back to our app, we should see the integration test added and passed:
Tip
In case the test doesn't show up, just quit and restart the Meteor app in the terminal again.
Adding a test for the admin
In the second test, we will first log in as administrator and then check again whether the button is visible.
We add the following code snippet to the same postButtonSpecs.js
file as the one we used before:
describe('The Admin', function() { afterEach(function (done) { Meteor.logout(done); }) it('should be able to login and see the create post link', function (done) { var div = document.createElement('DIV'); Blaze.render(Template.home, div); Meteor.loginWithPassword('johndoe@example.com', '1234', function (err) { Tracker.afterFlush(function(){ expect($(div).find('a.createNewPost')[0]).toBeDefined(); expect(err).toBeUndefined(); done(); }); }); }); });
Here we add the home
template to a div
again, but this time we log in as an admin user, using our admin credentials. After we have logged in, we call Tracker.afterFlush()
to give Meteor time to re-render the template and then check whether the button is now present.
Because this test runs asynchronously, we need to call the done()
function, which we passed as an argument to the it()
function, telling Jasmine that the test is over.
Note
Our credentials inside the test file are secure, as Meteor doesn't bundle files in the tests
directory.
If we now go back to our browser, we should see the two integration tests as passed:
After creating a test, we should always make sure we try to fail the test to see whether it actually works. To do so, we can simply comment out the a.createNewPost
link in my-meteor-blog/client/templates/home.html
.
Note
You can run Velocity tests using PhantomJS as follows:
$ meteor run --test
You first need to install PhantomJS globally with $ npm install -g phantomjs
. Be aware that this feature is experimental at the time of writing this book and might not run all your tests.
Adding unit tests to the server
Now we can add unit tests to the client and the server. In this book, we will only add a unit test to the server and later add integration tests to the client to stay within the scope of this chapter. The steps to do so are as follows:
- First, we create a file called
postSpecs.js
within the/my-meteor-blog/tests/jasmine/server/unit
folder and add the following command:describe('Post', function () {
This will create a test frame describing what the test inside will be about.
- Inside the test frame, we call the
beforeEach()
andafterEach()
functions, which will run before and after each test, respectively. Inside, we will create stubs for all Meteor functions usingMeteorStubs.install()
and clean them afterwards usingMeteorStubs.uninstall()
:beforeEach(function () { MeteorStubs.install(); }); afterEach(function () { MeteorStubs.uninstall(); });
Note
A stub is a function or object that mimics its original function or object, but doesn't run actual code. Instead, a stub can be used to return a specific value that the function we test depends on.
Stubbing makes sure that a unit test tests only a specific unit of code and not its dependencies. Otherwise, a break in a dependent function or object would cause a chain of other tests to fail, making it hard to find the actual problem.
- Now we can write the actual test. In this example, we will test whether the
insertPost
method we created previously in the book inserts the post, and makes sure that no duplicate slug will be inserted:it('should be correctly inserted', function() { spyOn(Posts, 'findOne').and.callFake(function() { // simulate return a found document; return {title: 'Some Tite'}; }); spyOn(Posts, 'insert'); spyOn(Meteor, 'user').and.returnValue({_id: 4321, profile: {name: 'John'}}); spyOn(global, 'moment').and.callFake(function() { // simulate return the moment object; return {unix: function(){ return 1234; }}; });
First, we create stubs for all the functions we are using inside the
insertPost
method to make sure that they return what we want.Especially, take a look at the
spyOn(Posts, "findOne")
call. As we can see, we call a fake function and return a fake document with just a title. Actually, we can return anything as theinsertPost
method only checks whether a document with the same slug was found or not. - Next, we actually call the method and give it some post data:
Meteor.call('insertPost', { title: 'My Title', description: 'Lorem ipsum', text: 'Lorem ipsum', slug: 'my-title' }, function(error, result){
- Inside the callback of the method, we add the actual tests:
expect(error).toBe(null); // we check that the slug is returned expect(result).toContain('my-title'); expect(result.length).toBeGreaterThan(8); // we check that the post is correctly inserted expect(Posts.insert).toHaveBeenCalledWith({ title: 'My Title', description: 'Lorem ipsum', text: 'Lorem ipsum', slug: result, timeCreated: 1234, owner: 4321, author: 'John' }); }); });
First, we check whether the error object is null. Then we check whether the resultant slug of the method contains the
'my-title'
string. Because we returned a fake document in thePosts.findOne()
function earlier, we expect our method to add some random number to the slug such as'my-title-fotvadydf4rt3xr'
. Therefore, we check whether the length is bigger than the eight characters of the original'my-title'
string.At last, we check whether the
Post.insert()
function was called with the expected values.Note
To fully understand how you can test Jasmine, take a look at the documentation at https://jasmine.github.io/2.0/introduction.html.
You can also find a good cheat sheet of Jasmine functions at http://www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing.
- Finally, we close the
describe(...
function at the beginning:});
If we now start our Meteor app again using $ meteor
, after a while we'll see a green dot appearing in the top-right corner.
Clicking on this dot gives us access to Velocity's html-reporter
and it should show us that our test has passed:
To make our test fail, let's go to our my-meteor-blog/methods.js
file and comment out the following lines:
if(Posts.findOne({slug: postDocument.slug})) postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);
This will prevent the slug from getting changed, even if a document with the same slug already exists, and fail our test. If we go back and check in our browser, we should see the test as failed:
We can add more tests by just adding a new it('should be xyz', function() {...});
function.
Adding integration tests to the client
Adding integration tests is as simple as adding unit tests. The difference is that all the test specification files go to the my-meteor-blog/tests/jasmine/client/integration
folder.
Integration tests, unlike unit tests, run in the actual app environment.
Adding a test for the visitors
In our first example test, we will test to ensure that visitors can't see the Create Post button. In the second test, we will log in as an administrator and check whether we are able to see it.
- Let's create a file named
postButtonSpecs.js
in ourmy-meteor-blog/tests/jasmine/client/integration
folder. - Now we add the following code snippet to the file and save it:
describe('Vistors', function() { it('should not see the create posts link', function () { var div = document.createElement('DIV'); Blaze.render(Template.home, div); expect($(div).find('a.createNewPost')[0]).not.toBeDefined(); }); });
Here we manually create a div
HTML element and render the home
template inside. After that, we check whether the a.createNewPost
link is present.
If we go back to our app, we should see the integration test added and passed:
Tip
In case the test doesn't show up, just quit and restart the Meteor app in the terminal again.
Adding a test for the admin
In the second test, we will first log in as administrator and then check again whether the button is visible.
We add the following code snippet to the same postButtonSpecs.js
file as the one we used before:
describe('The Admin', function() { afterEach(function (done) { Meteor.logout(done); }) it('should be able to login and see the create post link', function (done) { var div = document.createElement('DIV'); Blaze.render(Template.home, div); Meteor.loginWithPassword('johndoe@example.com', '1234', function (err) { Tracker.afterFlush(function(){ expect($(div).find('a.createNewPost')[0]).toBeDefined(); expect(err).toBeUndefined(); done(); }); }); }); });
Here we add the home
template to a div
again, but this time we log in as an admin user, using our admin credentials. After we have logged in, we call Tracker.afterFlush()
to give Meteor time to re-render the template and then check whether the button is now present.
Because this test runs asynchronously, we need to call the done()
function, which we passed as an argument to the it()
function, telling Jasmine that the test is over.
Note
Our credentials inside the test file are secure, as Meteor doesn't bundle files in the tests
directory.
If we now go back to our browser, we should see the two integration tests as passed:
After creating a test, we should always make sure we try to fail the test to see whether it actually works. To do so, we can simply comment out the a.createNewPost
link in my-meteor-blog/client/templates/home.html
.
Note
You can run Velocity tests using PhantomJS as follows:
$ meteor run --test
You first need to install PhantomJS globally with $ npm install -g phantomjs
. Be aware that this feature is experimental at the time of writing this book and might not run all your tests.
Adding integration tests to the client
Adding integration tests is as simple as adding unit tests. The difference is that all the test specification files go to the my-meteor-blog/tests/jasmine/client/integration
folder.
Integration tests, unlike unit tests, run in the actual app environment.
Adding a test for the visitors
In our first example test, we will test to ensure that visitors can't see the Create Post button. In the second test, we will log in as an administrator and check whether we are able to see it.
- Let's create a file named
postButtonSpecs.js
in ourmy-meteor-blog/tests/jasmine/client/integration
folder. - Now we add the following code snippet to the file and save it:
describe('Vistors', function() { it('should not see the create posts link', function () { var div = document.createElement('DIV'); Blaze.render(Template.home, div); expect($(div).find('a.createNewPost')[0]).not.toBeDefined(); }); });
Here we manually create a div
HTML element and render the home
template inside. After that, we check whether the a.createNewPost
link is present.
If we go back to our app, we should see the integration test added and passed:
Tip
In case the test doesn't show up, just quit and restart the Meteor app in the terminal again.
Adding a test for the admin
In the second test, we will first log in as administrator and then check again whether the button is visible.
We add the following code snippet to the same postButtonSpecs.js
file as the one we used before:
describe('The Admin', function() { afterEach(function (done) { Meteor.logout(done); }) it('should be able to login and see the create post link', function (done) { var div = document.createElement('DIV'); Blaze.render(Template.home, div); Meteor.loginWithPassword('johndoe@example.com', '1234', function (err) { Tracker.afterFlush(function(){ expect($(div).find('a.createNewPost')[0]).toBeDefined(); expect(err).toBeUndefined(); done(); }); }); }); });
Here we add the home
template to a div
again, but this time we log in as an admin user, using our admin credentials. After we have logged in, we call Tracker.afterFlush()
to give Meteor time to re-render the template and then check whether the button is now present.
Because this test runs asynchronously, we need to call the done()
function, which we passed as an argument to the it()
function, telling Jasmine that the test is over.
Note
Our credentials inside the test file are secure, as Meteor doesn't bundle files in the tests
directory.
If we now go back to our browser, we should see the two integration tests as passed:
After creating a test, we should always make sure we try to fail the test to see whether it actually works. To do so, we can simply comment out the a.createNewPost
link in my-meteor-blog/client/templates/home.html
.
Note
You can run Velocity tests using PhantomJS as follows:
$ meteor run --test
You first need to install PhantomJS globally with $ npm install -g phantomjs
. Be aware that this feature is experimental at the time of writing this book and might not run all your tests.
Adding a test for the visitors
In our first example test, we will test to ensure that visitors can't see the Create Post button. In the second test, we will log in as an administrator and check whether we are able to see it.
- Let's create a file named
postButtonSpecs.js
in ourmy-meteor-blog/tests/jasmine/client/integration
folder. - Now we add the following code snippet to the file and save it:
describe('Vistors', function() { it('should not see the create posts link', function () { var div = document.createElement('DIV'); Blaze.render(Template.home, div); expect($(div).find('a.createNewPost')[0]).not.toBeDefined(); }); });
Here we manually create a div
HTML element and render the home
template inside. After that, we check whether the a.createNewPost
link is present.
If we go back to our app, we should see the integration test added and passed:
In case the test doesn't show up, just quit and restart the Meteor app in the terminal again.
Adding a test for the admin
In the second test, we will first log in as administrator and then check again whether the button is visible.
We add the following code snippet to the same postButtonSpecs.js
file as the one we used before:
describe('The Admin', function() { afterEach(function (done) { Meteor.logout(done); }) it('should be able to login and see the create post link', function (done) { var div = document.createElement('DIV'); Blaze.render(Template.home, div); Meteor.loginWithPassword('johndoe@example.com', '1234', function (err) { Tracker.afterFlush(function(){ expect($(div).find('a.createNewPost')[0]).toBeDefined(); expect(err).toBeUndefined(); done(); }); }); }); });
Here we add the home
template to a div
again, but this time we log in as an admin user, using our admin credentials. After we have logged in, we call Tracker.afterFlush()
to give Meteor time to re-render the template and then check whether the button is now present.
Because this test runs asynchronously, we need to call the done()
function, which we passed as an argument to the it()
function, telling Jasmine that the test is over.
Our credentials inside the test file are secure, as Meteor doesn't bundle files in the tests
directory.
If we now go back to our browser, we should see the two integration tests as passed:
After creating a test, we should always make sure we try to fail the test to see whether it actually works. To do so, we can simply comment out the a.createNewPost
link in my-meteor-blog/client/templates/home.html
.
You can run Velocity tests using PhantomJS as follows:
$ meteor run --test
You first need to install PhantomJS globally with $ npm install -g phantomjs
. Be aware that this feature is experimental at the time of writing this book and might not run all your tests.
Adding a test for the admin
In the second test, we will first log in as administrator and then check again whether the button is visible.
We add the following code snippet to the same postButtonSpecs.js
file as the one we used before:
describe('The Admin', function() { afterEach(function (done) { Meteor.logout(done); }) it('should be able to login and see the create post link', function (done) { var div = document.createElement('DIV'); Blaze.render(Template.home, div); Meteor.loginWithPassword('johndoe@example.com', '1234', function (err) { Tracker.afterFlush(function(){ expect($(div).find('a.createNewPost')[0]).toBeDefined(); expect(err).toBeUndefined(); done(); }); }); }); });
Here we add the home
template to a div
again, but this time we log in as an admin user, using our admin credentials. After we have logged in, we call Tracker.afterFlush()
to give Meteor time to re-render the template and then check whether the button is now present.
Because this test runs asynchronously, we need to call the done()
function, which we passed as an argument to the it()
function, telling Jasmine that the test is over.
Our credentials inside the test file are secure, as Meteor doesn't bundle files in the tests
directory.
If we now go back to our browser, we should see the two integration tests as passed:
After creating a test, we should always make sure we try to fail the test to see whether it actually works. To do so, we can simply comment out the a.createNewPost
link in my-meteor-blog/client/templates/home.html
.
You can run Velocity tests using PhantomJS as follows:
$ meteor run --test
You first need to install PhantomJS globally with $ npm install -g phantomjs
. Be aware that this feature is experimental at the time of writing this book and might not run all your tests.
Acceptance tests
Though we can test client and server code separately with these tests, we can't test the interaction between the two. For this, we need acceptance tests, which, if explained in detail, would go beyond the scope of this chapter.
At the time of this writing, there is no acceptance testing framework that is implemented using Velocity, though there are two you can use.
Nightwatch
The
clinical:nightwatch
package allows you to run an acceptance test in a simple way as follows:
"Hello World" : function (client) { client .url("http://127.0.0.1:3000") .waitForElementVisible("body", 1000) .assert.title("Hello World") .end(); }
Though the installation process is not as straightforward as installing a Meteor package, you need to install and run MongoDB and PhantomJS yourself before you can run the tests.
If you want to give it a try, check out the package on atmosphere-javascript website at https://atmospherejs.com/clinical/nightwatch.
Laika
If you want to test the communication between the server and the client, you can use Laika. Its installation process is similar to Nightwatch, as it requires separate MongoDB and PhantomJS installations.
Laika spins up a server instance and connects multiple clients. You then can set up subscriptions or insert and modify documents. You can also test their appearance in the clients.
To install Laika, go to http://arunoda.github.io/laika/.
Note
At the time of this writing, Laika is not compatible with Velocity, which tries to run all the files in the test folder in Laika's environment, causing errors.
Nightwatch
The
clinical:nightwatch
package allows you to run an acceptance test in a simple way as follows:
"Hello World" : function (client) { client .url("http://127.0.0.1:3000") .waitForElementVisible("body", 1000) .assert.title("Hello World") .end(); }
Though the installation process is not as straightforward as installing a Meteor package, you need to install and run MongoDB and PhantomJS yourself before you can run the tests.
If you want to give it a try, check out the package on atmosphere-javascript website at https://atmospherejs.com/clinical/nightwatch.
Laika
If you want to test the communication between the server and the client, you can use Laika. Its installation process is similar to Nightwatch, as it requires separate MongoDB and PhantomJS installations.
Laika spins up a server instance and connects multiple clients. You then can set up subscriptions or insert and modify documents. You can also test their appearance in the clients.
To install Laika, go to http://arunoda.github.io/laika/.
Note
At the time of this writing, Laika is not compatible with Velocity, which tries to run all the files in the test folder in Laika's environment, causing errors.
Laika
If you want to test the communication between the server and the client, you can use Laika. Its installation process is similar to Nightwatch, as it requires separate MongoDB and PhantomJS installations.
Laika spins up a server instance and connects multiple clients. You then can set up subscriptions or insert and modify documents. You can also test their appearance in the clients.
To install Laika, go to http://arunoda.github.io/laika/.
Note
At the time of this writing, Laika is not compatible with Velocity, which tries to run all the files in the test folder in Laika's environment, causing errors.
Summary
In this final chapter, we learned how to write simple unit tests using the sanjo:jasmine
package for Meteor's official testing framework, Velocity. We also took a brief look at possible acceptance test frameworks.
If you want to dig deeper into testing, you can take a look at the following resources:
- http://velocity.meteor.com
- http://jasmine.github.io
- http://www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing
- http://doctorllama.wordpress.com/2014/09/22/bullet-proof-internationalised-meteor-applications-with-velocity-unit-testing-integration-testing-and-jasmine/
- http://arunoda.github.io/laika/
- https://github.com/xolvio/velocity
You can find this chapter's code files at https://www.packtpub.com/books/content/support/17713 or on GitHub at https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter12.
Now that you have read the whole book, I assume you know a lot more about Meteor than before and are as excited about this framework as I am!
If you have any questions concerning Meteor, you can always ask them at http://stackoverflow.com, which has a great Meteor community.
I also recommend reading through all Meteor subprojects at https://www.meteor.com/projects, and study the documentation at https://docs.meteor.com.
I hope you had a great time reading this book and you're now ready to start making great apps using Meteor!