集成测试
集成测试是一种跨模块的测试。在最简单的情形下,这里的简单意味着它将非常类似一个单元测试,当你针对多个模块进行了隔离,创建一个非奇异的“被测系统”。
Although conceptually different to unit tests, such tests typically do not need to be run any differently to unit tests and can use the same meteor test
mode and isolation techniques as we use for unit tests.
However, an integration test that crosses the client-server boundary of a Meteor application (where the modules under test cross that boundary) requires a different testing infrastructure, namely Meteor's "full app" testing mode.
Let's take a look at example of both kinds of tests.
简单的集成测试
Our reusable components were a natural fit for a unit test; similarly our smart components tend to require an integration test to really be exercised properly, as the job of a smart component is to bring data together and supply it to a reusable component.
In the Todos example app, we have an integration test for the Lists_show_page
smart component. This test simply ensures that when the correct data is present in the database, the template renders correctly -- that it is gathering the correct data as we expect. It isolates the rendering tree from the more complex data subscription part of the Meteor stack. If we wanted to test that the subscription side of things was working in concert with the smart component, we'd need to write a full app integration test.
imports/ui/components/client/todos-item.tests.js
:
/* eslint-env mocha */
/* eslint-disable func-names, prefer-arrow-callback */
import { Meteor } from 'meteor/meteor';
import { Factory } from 'meteor/factory';
import { Random } from 'meteor/random';
import { chai } from 'meteor/practicalmeteor:chai';
import StubCollections from 'meteor/hwillson:stub-collections';
import { Template } from 'meteor/templating';
import { _ } from 'meteor/underscore';
import { $ } from 'meteor/jquery';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { sinon } from 'meteor/practicalmeteor:sinon';
import { withRenderedTemplate } from '../../test-helpers.js';
import '../lists-show-page.js';
import { Todos } from '../../../api/todos/todos.js';
import { Lists } from '../../../api/lists/lists.js';
describe('Lists_show_page', function () {
const listId = Random.id();
beforeEach(function () {
StubCollections.stub([Todos, Lists]);
Template.registerHelper('_', key => key);
sinon.stub(FlowRouter, 'getParam', () => listId);
sinon.stub(Meteor, 'subscribe', () => ({
subscriptionId: 0,
ready: () => true,
}));
});
afterEach(function () {
StubCollections.restore();
Template.deregisterHelper('_');
FlowRouter.getParam.restore();
Meteor.subscribe.restore();
});
it('renders correctly with simple data', function () {
Factory.create('list', { _id: listId });
const timestamp = new Date();
const todos = _.times(3, i => Factory.create('todo', {
listId,
createdAt: new Date(timestamp - (3 - i)),
}));
withRenderedTemplate('Lists_show_page', {}, el => {
const todosText = todos.map(t => t.text).reverse();
const renderedText = $(el).find('.list-items input[type=text]')
.map((i, e) => $(e).val())
.toArray();
chai.assert.deepEqual(renderedText, todosText);
});
});
});
Of particular interest in this test is the following:
导入
As we'll run this test in the same way that we did our unit test, we need to import
the relevant modules under test in the same way that we did in the unit test.
Stub
As the system under test in our integration test has a larger surface area, we need to stub out a few more points of integration with the rest of the stack. Of particular interest here is our use of the hwillson:stub-collections
package and of Sinon to stub out Flow Router and our Subscription.
创建数据
In this test, we used Factory package's .create()
API, which inserts data into the real collection. However, as we've proxied all of the Todos
and Lists
collection methods onto a local collection (this is what hwillson:stub-collections
is doing), we won't run into any problems with trying to perform inserts from the client.
This integration test can be run the exact same way as we ran unit tests above.
全应用集成测试
In the Todos example application, we have a integration test which ensures that we see the full contents of a list when we route to it, which demonstrates a few techniques of integration tests.
imports/startup/client/routes.app-test.js
:
/* eslint-env mocha */
/* eslint-disable func-names, prefer-arrow-callback */
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';
import { DDP } from 'meteor/ddp-client';
import { FlowRouter } from 'meteor/kadira:flow-router';
import { assert } from 'meteor/practicalmeteor:chai';
import { Promise } from 'meteor/promise';
import { $ } from 'meteor/jquery';
import { generateData } from './../../api/generate-data.app-tests.js';
import { Lists } from '../../api/lists/lists.js';
import { Todos } from '../../api/todos/todos.js';
// Utility -- returns a promise which resolves when all subscriptions are done
const waitForSubscriptions = () => new Promise(resolve => {
const poll = Meteor.setInterval(() => {
if (DDP._allSubscriptionsReady()) {
clearInterval(poll);
resolve();
}
}, 200);
});
// Tracker.afterFlush runs code when all consequent of a tracker based change
// (such as a route change) have occured. This makes it a promise.
const afterFlushPromise = Promise.denodeify(Tracker.afterFlush);
if (Meteor.isClient) {
describe('data available when routed', () => {
// First, ensure the data that we expect is loaded on the server
// Then, route the app to the homepage
beforeEach(() => generateData().then(() => FlowRouter.go('/')));
describe('when logged out', () => {
it('has all public lists at homepage', () => {
assert.equal(Lists.find().count(), 3);
});
it('renders the correct list when routed to', () => {
const list = Lists.findOne();
FlowRouter.go('Lists.show', { _id: list._id });
return afterFlushPromise()
.then(() => {
assert.equal($('.title-wrapper').html(), list.name);
})
.then(() => waitForSubscriptions())
.then(() => {
assert.equal(Todos.find({ listId: list._id }).count(), 3);
});
});
});
});
}
Of note here:
Before running, each test sets up the data it needs using the
generateData
helper (see the section on creating integration test data for more detail) then goes to the homepage.Although Flow Router doesn't take a done callback, we can use
Tracker.afterFlush
to wait for all its reactive consequences to occur.Here we wrote a little utility (which could be abstracted into a general package) to wait for all the subscriptions which are created by the route change (the
todos.inList
subscription in this case) to become ready before checking their data.
运行全应用测试
To run the full-app tests in our application, we run:
meteor test --full-app --driver-package practicalmeteor:mocha
When we connect to the test instance in a browser, we want to render a testing UI rather than our app UI, so the mocha-web-reporter
package will hide any UI of our application and overlay it with its own. However the app continues to behave as normal, so we are able to route around and check the correct data is loaded.
创建数据
To create test data in full-app test mode, it usually makes sense to create some special test methods which we can call from the client side. Usually when testing a full app, we want to make sure the publications are sending through the correct data (as we do in this test), and so it's not sufficient to stub out the collections and place synthetic data in them. Instead we'll want to actually create data on the server and let it be published.
Similar to the way we cleared the database using a method in the beforeEach
in the test data section above, we can call a method to do that before running our tests. In the case of our routing tests, we've used a file called imports/api/generate-data.app-tests.js
which defines this method (and will only be loaded in full app test mode, so is not available in general!):
// This file will be auto-imported in the app-test context,
// ensuring the method is always available
import { Meteor } from 'meteor/meteor';
import { Factory } from 'meteor/factory';
import { resetDatabase } from 'meteor/xolvio:cleaner';
import { Random } from 'meteor/random';
import { _ } from 'meteor/underscore';
const createList = (userId) => {
const list = Factory.create('list', { userId });
_.times(3, () => Factory.create('todo', { listId: list._id }));
return list;
};
// Remember to double check this is a test-only file before
// adding a method like this!
Meteor.methods({
generateFixtures: function generateFixturesMethod() {
resetDatabase();
// create 3 public lists
_.times(3, () => createList());
// create 3 private lists
_.times(3, () => createList(Random.id()));
},
});
let generateData;
if (Meteor.isClient) {
// Create a second connection to the server to use to call
// test data methods. We do this so there's no contention
// with the currently tested user's connection.
const testConnection = Meteor.connect(Meteor.absoluteUrl());
generateData = Promise.denodeify((cb) => {
testConnection.call('generateFixtures', cb);
});
}
export { generateData };
Note that we've exported a client-side symbol generateData
which is a promisified version of the method call, which makes it simpler to use this sequentially in tests.
Also of note is the way we use a second DDP connection to the server in order to send these test "control" method calls.