十二、测试 Meteor

在最后一章,我们将讨论如何测试一个 Meteor 应用。

测试是一个综合性的话题,超出了本章的范围。 为了简单起见,我们将简要介绍两种可用的工具,因为它们当然是不同的,并分别给出一个简单的示例。

在本章中,我们将涵盖以下主题:

测试类型

测试是用于测试应用其他代码或功能的代码片段。

我们可以将测试分为四大类:

  • 单元测试:在这个测试中,我们只测试了一小部分代码。 例如,这可以是一个函数或一段代码。 单元测试不应调用其他函数,不应写入硬盘或数据库,也不应访问网络。 如果需要这样的功能,应该编写存根,这是返回预期值而不调用实际函数的函数。
  • 集成测试:在这个测试中,我们将多个测试组合在一起,并在不同的环境到中运行,以确保它们仍然能够工作。 这个测试与单元测试的不同之处在于,我们实际运行的是连接的功能,比如调用数据库。
  • 功能测试:是一个单元测试或测试界面,但只会测试的功能特性/功能没有检查副作用,如变量是否清理干净。
  • 验收测试:该在整个系统上运行测试,例如,可以是一个 web 浏览器。 这个想法是尽可能地模仿实际用户。 这些测试非常类似于定义功能的用户描述。 缺点是,当测试发生在更高的级别时,很难追踪 bug。

在下面的示例中,我们将主要编写功能测试,以简化操作。

测试包

在前面的章节中,我们用ReactiveTimer对象构建了一个包。 一个好的包应该总是包含单元测试,这样人们就可以运行它们,并确保对包的更改不会破坏它的功能。

Meteor 提供了一个简单的包单元测试工具,称为TinyTest,我们将使用它来测试我们的包:

  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

    ```

    注释

    此外,我们需要确保我们删除了my-meteor-blog/client/ReactiveTimer.js文件,如果我们使用了第 10 章、部署我们的应用中的代码示例,就应该有这个文件作为基础。

  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'); }); ```

    当运行测试时,这将包括我们的meteor-book:reactive-timer包和tinytest。 然后它将运行tests.js文件,其中包含我们的单元测试。

  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.

    目前,Meteor 没有记录tinytest包,但它很小,这意味着它是非常简单的。

    基本上有两个函数,Tinytest.add(test)Tinytest.addAsync(test, expect)。 它们都运行一个简单的测试函数,我们可以使用test.equal(x, y)test.isTrue(x)test.isUndefined(x)通过或失败该函数。

    对于包测试,我们只需要在启动计时器后测试ReactiveTimer._intervalId是否为空,就可以知道计时器是否运行。

添加包测试

测试是通过首先描述将要测试的内容而建立的。

为了测试_intervalId,我们将以下代码添加到tests.js文件中:

Tinytest.add('The timer set the _intervalId property', function (test) {
    var timer = new ReactiveTimer();
    timer.start(1);

    test.isTrue(timer._intervalId !== null);

    timer.stop();
});

然后我们启动一个计时器并测试它的_intervalId属性是否不再为空。 最后,我们再次停止计时器来清理测试。

我们将添加到tests.js文件的下一个测试将是异步的,因为我们需要等待计时器至少运行一次:

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);
});

让我们看看在这个异步测试中发生了什么:

  • 首先,我们以 1 秒的间隔再次启动计时器,并创建一个名为run的变量。 然后,只有当活性Tracker.autorun()函数运行时,我们才将这个变量切换到true。 注意,我们使用了if(!c.firstRun)来防止在函数第一次执行时设置run变量,因为我们只希望在 1 秒后使用“tick”来计数。
  • 然后用Meteor.setTimeout()函数检查run是否变为trueexpect()告诉Tinytest.addAsync()测试结束并输出结果。 注意,我们还停止了计时器,因为我们总是需要在每次测试后进行清理。

运行包测试

为了最后运行测试,我们可以从应用的根文件夹运行以下命令:

$ meteor test-packages meteor-book:reactive-timer

这将启动 Meteor 应用,并运行我们的包测试。 为了看到它们,我们导航到http://localhost:3000:

Running the package tests

提示

我们还可以同时运行多个包的测试,方法是用空格分隔多个包:

$ meteor test-packages meteor-book:reactive-timer iron:router

为了看看测试是否有效,我们将故意让它失败,注释掉my-meteor-book/packages/reactive-timer/ReactiveTimer.js文件中的Meteor.setInterval(),如下截图所示:

Running the package tests

我们应该始终尝试让测试失败,因为测试也可以以一种从未成功或失败的方式编写(例如,当expect()从未被调用时)。 这将停止其他测试的执行,因为当前的测试永远无法完成。

一个好的经验法则是测试功能,就像我们在看一个黑盒一样。 如果我们过于依赖函数的编写方式来定制我们的测试,我们将很难在改进函数的同时修复测试。

测试 Meteor app

为了测试应用本身,我们可以使用 Velocity Meteor 的官方测试框架。

Velocity 本身并不包含测试工具,而是为 Jasmine 或 Mocha 等测试包提供了一个统一的方法来测试 Meteor 应用,并使用velocity:html-reporter包在控制台或应用界面中报告它们的输出。

让我们引用他们自己的话:

Velocity 监视您的测试/目录,并将测试文件发送到正确的测试插件。 测试插件执行测试,并在每次测试完成后将结果发送回 Velocity。 然后,Velocity 结合来自所有测试插件的结果,并通过一个或多个报告插件输出它们。 当应用或测试发生变化时,Velocity 将重新运行测试并响应性地更新结果。

本文摘自http://velocity.meteor.com。 此外,Velocity 还添加了 Meteor 存根和自动存根等功能。 它可以创建用于独立测试的镜像应用并运行安装代码(fixture)。

现在,我们将看看使用 Jasmine 的单元和集成测试,以及使用 Nightwatch 的验收测试。

茉莉花测试

要使用 Velocity 的 Jasmine,我们需要在velocity:html-reporter包的同时安装sanjo:jasmine包。

为了做到这一点,我们将在我们的 apps 文件夹中运行以下命令:

$ meteor add velocity:html-reporter

然后我们使用以下命令为 Meteor 安装 Jasmine:

$ meteor add sanjo:jasmine

为了让 Velocity 能够找到测试,我们需要创建以下文件夹结构:

- my-meteor-blog
  - tests
    - jasmine
    - client
      - unit
      - integration
    - server
      - unit

现在,当我们使用$ meteor启动 Meteor 服务器时,我们将看到 Jasmine 包已经在/my-meteor-blog/tests/jasmine/server/unit文件夹中创建了两个文件,其中包含了我们包的存根。

向服务器添加单元测试

现在我们可以向客户端和服务器添加单元测试。 在本书中,我们将只向服务器添加一个单元测试,然后向客户端添加集成测试,以保持在本章的范围内。 这样做的步骤如下:

  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 () {

    这将创建一个测试框架,描述内部测试的内容。

  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(); }); ```

    注释

    存根是模仿其原始函数或对象,但不运行实际代码的函数或对象。 相反,存根可以用来返回我们测试的函数所依赖的特定值。

    存根确保单元测试只测试特定的代码单元,而不测试它的依赖项。 否则,依赖函数或对象中的中断将导致其他测试链失败,从而难以找到实际问题。

  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;
        }};
    });
    

    ```

    首先,我们为在insertPost方法中使用的所有函数创建存根,以确保它们返回我们想要的内容。

    特别是,看看spyOn(Posts, "findOne")呼叫。 正如我们所看到的,我们调用一个假函数并返回一个只有标题的假文档。 实际上,我们可以返回任何东西作为insertPost方法,只检查是否找到了具有相同蛞蝓的文档。

  4. 接下来,我们实际调用该方法并给它一些 post 数据:

    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'
        });
    });
    

    }); ```

    首先,我们检查错误对象是否为空。 然后我们检查方法的结果段塞是否包含'my-title'字符串。 因为我们在前面的Posts.findOne()函数中返回了一个假文档,所以我们希望我们的方法向代码中添加一些随机数,比如'my-title-fotvadydf4rt3xr'。 因此,我们检查长度是否大于原来的'my-title'字符串的 8 个字符。

    最后,我们检查是否用期望值调用了Post.insert()函数。

    注释

    要完全理解如何测试 Jasmine,请查看https://jasmine.github.io/2.0/introduction.html文档。

    你也可以在http://www.cheatography.com/citguy/cheat-sheets/jasmine-js-testing上找到一个很好的 Jasmine 函数小记。

  6. 最后,我们在开头关闭describe(...函数:

    });

如果我们现在使用$ meteor再次启动 Meteor 应用,过一会儿我们会看到一个绿色的点出现在右上角。

点击这个点让我们访问 Velocity 的html-reporter,它应该告诉我们我们的测试已经通过了:

Adding unit tests to the server

为了让我们的测试失败,让我们转到my-meteor-blog/methods.js文件并注释掉以下几行:

if(Posts.findOne({slug: postDocument.slug}))
    postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);

这将防止蛞蝓被改变,即使一个文档与相同的蛞蝓已经存在,并失败了我们的测试。 如果我们返回并在浏览器中检查,我们应该看到测试失败:

Adding unit tests to the server

只要增加一个新的it('should be xyz', function() {...});函数,就可以增加更多的测试。

向客户端添加集成测试

添加集成测试就像添加单元测试一样简单。 不同之处在于,所有的测试规格文件都进入了my-meteor-blog/tests/jasmine/client/integration文件夹。

与单元测试不同,集成测试在实际的应用环境中运行。

为访客添加一个测试

在我们的第一个示例测试中,我们将测试以确保访问者无法看到Create Post按钮。 在第二个测试中,我们将以管理员身份登录并检查是否能够看到它。

  1. 让我们在my-meteor-blog/tests/jasmine/client/integration文件夹中创建一个名为postButtonSpecs.js的文件。
  2. 现在我们将以下代码片段添加到文件中并保存它:

这里我们手动创建一个divHTML 元素,并渲染home模板。 之后,我们检查a.createNewPost链接是否存在。

如果我们回到我们的应用,我们应该看到添加并通过了集成测试:

Adding a test for the visitors

提示

如果测试没有显示,就退出并在终端中重新启动 Meteor 应用。

添加管理员测试

在第二个测试中,我们将首先以管理员身份登录,然后再次检查按钮是否可见。

我们将以下代码片段添加到我们之前使用的同一个postButtonSpecs.js文件中:

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();
            });

        });
    });
});

在这里,我们再次将home模板添加到div,但这次我们以管理员用户身份登录,使用我们的管理员凭证。 在我们登录后,我们调用Tracker.afterFlush()给 Meteor 时间重新渲染模板,然后检查按钮现在是否存在。

因为这个测试是异步运行的,我们需要调用done()函数,我们将它作为参数传递给it()函数,告诉 Jasmine 测试已经结束。

注释

我们的凭证内的测试文件是安全的,Meteor 不捆绑文件在tests目录。

如果我们现在回到浏览器,我们应该看到两个通过的集成测试:

Adding a test for the admin

在创建了一个测试之后,我们应该始终确保测试失败,看看它是否实际工作。 要做到这一点,我们可以简单地注释掉my-meteor-blog/client/templates/home.html中的a.createNewPost链接。

注释

你可以使用 PhantomJS 运行以下速度测试:

$ meteor run --test

你首先需要用$ npm install -g phantomjs全局安装 PhantomJS。 请注意,在编写本书时,该特性是实验性的,可能不会运行所有测试。

验收测试

虽然我们可以用这些测试分别测试客户端和服务器代码,但我们不能测试两者之间的交互。 为此,我们需要验收测试,如果详细解释,将超出本章的范围。

在撰写本文时,还没有使用 Velocity 实现的验收测试框架,尽管您可以使用两个。

守夜人

clinical:nightwatch包允许您以如下简单方式运行验收测试:

"Hello World" : function (client) {
     client
        .url("http://127.0.0.1:3000")
        .waitForElementVisible("body", 1000)
        .assert.title("Hello World")
        .end();
}

虽然安装过程不像安装 Meteor 包那么简单,你需要自己安装并运行 MongoDB 和 PhantomJS,然后才能运行测试。

如果你想尝试一下,可以在 atmosphere-javascript 网站https://atmospherejs.com/clinical/nightwatch上查看这个包。

Laika

如果您想测试服务器和客户端之间的通信,可以使用 Laika。 它的安装过程类似于 Nightwatch,因为它需要单独的 MongoDB 和 PhantomJS 安装。

Laika 旋转一个服务器实例并连接多个客户端。 然后可以设置订阅或插入和修改文档。 您还可以在客户端测试它们的外观。

安装 Laika,请登录http://arunoda.github.io/laika/

注释

在撰写本文时,Laika 与 Velocity 不兼容,Velocity 试图运行 Laika 环境中测试文件夹中的所有文件,从而导致错误。

小结

在最后一章中,我们学习了如何使用 Meteor 的官方测试框架 Velocity 的sanjo:jasmine包编写简单的单元测试。 我们还简要介绍了可能的验收测试框架。

如果你想更深入地研究测试,你可以看看以下资源:

你可以在https://www.packtpub.com/books/content/support/17713或在 GitHubhttps://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter12上找到本章的代码文件。

现在你已经读了整本书,我假设你知道更多的 Meteor 比以前,并对这个框架感到兴奋,因为我是!

如果你有任何关于 Meteor 的问题,你可以在http://stackoverflow.com上问他们,那里有一个很棒的 Meteor 社区。

我还建议阅读所有 Meteor 子项目https://www.meteor.com/projects,并研究文档https://docs.meteor.com

我希望你有一个伟大的时间阅读这本书,你现在准备开始制作伟大的应用使用 Meteor!