Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
Meteor: Full-Stack Web Application Development

You're reading from   Meteor: Full-Stack Web Application Development Rapidly build web apps with Meteor

Arrow left icon
Product type Course
Published in Nov 2016
Publisher Packt
ISBN-13 9781787287754
Length 685 pages
Edition 1st Edition
Languages
Tools
Arrow right icon
Authors (2):
Arrow left icon
Fabian Vogelsteller Fabian Vogelsteller
Author Profile Icon Fabian Vogelsteller
Fabian Vogelsteller
Marcelo Reyna Marcelo Reyna
Author Profile Icon Marcelo Reyna
Marcelo Reyna
Arrow right icon
View More author details
Toc

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:

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:

  1. To add tests, we need to copy the meteor-book:reactive-timer package, which we built in the previous chapter, to the my-meteor-blog/packages folder of our app. This way, we can make changes to the package, as Meteor will prefer the package in the packages 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.

  2. Then we open the package.js file from our packages 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 and tinytest when running tests. It will then run the tests.js file, which will contain our unit tests.

  3. Now, we can create the tests by adding a folder called tests to our package's folder and create a file called tests.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) and Tinytest.addAsync(test, expect). They both run a simple test function, which we can pass or fail using test.equal(x, y), test.isTrue(x), or test.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 to true only when our reactive Tracker.autorun() function ran. Note that we used if(!c.firstRun) to prevent the run 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 whether run was changed to true. The expect() tells Tinytest.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:

Running the package tests

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:

Running the package tests

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 to true only when our reactive Tracker.autorun() function ran. Note that we used if(!c.firstRun) to prevent the run 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 whether run was changed to true. The expect() tells Tinytest.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:

Running the package tests

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:

Running the package tests

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:

Running the package tests

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:

Running the package tests

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:

  1. 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.

  2. Inside the test frame, we call the beforeEach() and afterEach() functions, which will run before and after each test, respectively. Inside, we will create stubs for all Meteor functions using MeteorStubs.install() and clean them afterwards using MeteorStubs.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.

  3. 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 the insertPost method only checks whether a document with the same slug was found or not.

  4. 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){
  5. 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 the Posts.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.

  6. 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:

Adding unit tests to the server

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:

Adding unit tests to the server

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.

  1. Let's create a file named postButtonSpecs.js in our my-meteor-blog/tests/jasmine/client/integration folder.
  2. 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:

Adding a test for the visitors

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:

Adding a test for the admin

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:

  1. 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.

  2. Inside the test frame, we call the beforeEach() and afterEach() functions, which will run before and after each test, respectively. Inside, we will create stubs for all Meteor functions using MeteorStubs.install() and clean them afterwards using MeteorStubs.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.

  3. 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 the insertPost method only checks whether a document with the same slug was found or not.

  4. 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){
  5. 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 the Posts.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.

  6. 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:

Adding unit tests to the server

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:

Adding unit tests to the server

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.

  1. Let's create a file named postButtonSpecs.js in our my-meteor-blog/tests/jasmine/client/integration folder.
  2. 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:

Adding a test for the visitors

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:

Adding a test for the admin

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:

  1. 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.

  2. Inside the test frame, we call the beforeEach() and afterEach() functions, which will run before and after each test, respectively. Inside, we will create stubs for all Meteor functions using MeteorStubs.install() and clean them afterwards using MeteorStubs.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.

  3. 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 the insertPost method only checks whether a document with the same slug was found or not.

  4. 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){
  5. 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 the Posts.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.

  6. 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:

Adding unit tests to the server

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:

Adding unit tests to the server

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.

  1. Let's create a file named postButtonSpecs.js in our my-meteor-blog/tests/jasmine/client/integration folder.
  2. 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:

Adding a test for the visitors

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:

Adding a test for the admin

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.

  1. Let's create a file named postButtonSpecs.js in our my-meteor-blog/tests/jasmine/client/integration folder.
  2. 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:

Adding a test for the visitors

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:

Adding a test for the admin

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.

  1. Let's create a file named postButtonSpecs.js in our my-meteor-blog/tests/jasmine/client/integration folder.
  2. 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:

Adding a test for the visitors
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:

Adding a test for the admin

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 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:

Adding a test for the admin

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.

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:

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!

You have been reading a chapter from
Meteor: Full-Stack Web Application Development
Published in: Nov 2016
Publisher: Packt
ISBN-13: 9781787287754
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Banner background image