单元测试
单元测试是指隔离一部分代码然后测试这部分代码在内部是否按预期工作的过程。当我们已经把我们的代码分离成基于ES2015的模块时,去一次性地测试这些模块就变得十分自然。
By isolating a module and simply testing its internal functionality, we can write tests that are fast and accurate---they can quickly tell you where a problem in your application lies. Note however that incomplete unit tests can often hide bugs because of the way they stub out dependencies. For that reason it's useful to combine unit tests with slower (and perhaps less commonly run) integration and acceptance tests.
一个简单的单元测试
In the Todos example application, thanks to the fact that we've split our User Interface into smart and reusable components, it's natural to want to unit test some of our reusable components (we'll see below how to integration test our smart components).
To do so, we'll use a very simple test helper that renders a Blaze component off-screen with a given data context (note that the React test utils can do a similar thing for React). As we place it in imports
, it won't load in our app by in normal mode (as it's not required anywhere).
import { _ } from 'meteor/underscore';
import { Template } from 'meteor/templating';
import { Blaze } from 'meteor/blaze';
import { Tracker } from 'meteor/tracker';
const withDiv = function withDiv(callback) {
const el = document.createElement('div');
document.body.appendChild(el);
try {
callback(el);
} finally {
document.body.removeChild(el);
}
};
export const function withRenderedTemplate(template, data, callback) {
withDiv((el) => {
const ourTemplate = _.isString(template) ? Template[template] : template;
Blaze.renderWithData(ourTemplate, data, el);
Tracker.flush();
callback(el);
});
};
A simple example of a reusable component to test is the Todos_item
template. Here's what a unit test looks like (you can see some others in the app repository).
imports/ui/components/client/todos-item.tests.js
:
/* eslint-env mocha */
/* eslint-disable func-names, prefer-arrow-callback */
import { Factory } from 'meteor/factory';
import { chai } from 'meteor/practicalmeteor:chai';
import { Template } from 'meteor/templating';
import { $ } from 'meteor/jquery';
import { withRenderedTemplate } from '../../test-helpers.js';
import '../todos-item.js';
describe('Todos_item', function () {
beforeEach(function () {
Template.registerHelper('_', key => key);
});
afterEach(function () {
Template.deregisterHelper('_');
});
it('renders correctly with simple data', function () {
const todo = Factory.build('todo', { checked: false });
const data = {
todo,
onEditingChange: () => 0,
};
withRenderedTemplate('Todos_item', data, el => {
chai.assert.equal($(el).find('input[type=text]').val(), todo.text);
chai.assert.equal($(el).find('.list-item.checked').length, 0);
chai.assert.equal($(el).find('.list-item.editing').length, 0);
});
});
});
Of particular interest in this test is the following:
导入
When we run our app in test mode, only our test files will be eagerly loaded. In particular, this means that in order to use our templates, we need to import them! In this test, we import todos-item.js
, which itself imports todos.html
(yes, you do need to import the HTML files of your Blaze templates!)
Stub
To be a unit test, we must stub out the dependencies of the module. In this case, thanks to the way we've isolated our code into a reusable component, there's not much to do; principally we need to stub out the {% raw %}{{_}}{% endraw %}
helper that's created by the tap:i18n
system. Note that we stub it out in a beforeEach
and restore it the afterEach
.
If you're testing code that makes use of globals, you'll need to import those globals. For instance if you have a global Todos
collection and are testing this file:
// logging.js
export const function logTodos() {
console.log(Todos.findOne());
}
then you'll need to import Todos
both in that file and in the test:
// logging.js
import { Todos } from './todos.js'
export const function logTodos() {
console.log(Todos.findOne());
}
// logging.test.js
import { Todos } from './todos.js'
Todos.findOne = () => {
return {text: "write a guide"}
}
import { logTodos } from './logging.js'
// then test logTodos
...
创建数据
We can use the Factory package's .build()
API to create a test document without inserting it into any collection. As we've been careful not to call out to any collections directly in the reusable component, we can pass the built todo
document directly into the template.
运行单元测试
To run the tests that our app defines, we run our app in test mode:
meteor test --driver-package practicalmeteor:mocha
As we've defined a test file (imports/todos/todos.tests.js
), what this means is that the file above will be eagerly loaded, adding the 'builds correctly from factory'
test to the Mocha registry.
To run the tests, visit http://localhost:3000 in your browser. This kicks off practicalmeteor:mocha
, which runs your tests both in the browser and on the server. It displays the test results in the browser in a Mocha test reporter:
Usually, while developing an application, it make sense to run meteor test
on a second port (say 3100
), while also running your main application in a separate process:
# 在一个终端窗口中
meteor
# 在另一个终端窗口中
meteor test --driver-package practicalmeteor:mocha --port 3100
Then you can open two browser windows to see the app in action while also ensuring that you don't break any tests as you make changes.
隔离技术
In the unit test above we saw a very limited example of how to isolate a module from the larger app. This is critical for proper unit testing. Some other utilities and techniques include:
The
velocity:meteor-stubs
package, which creates simple stubs for most Meteor core objects.Alternatively, you can also use tools like Sinon to stub things directly, as we'll see for example in our simple integration test.
The
hwillson:stub-collections
package we mentioned above.(Using another package from the example app) to isolate a publication, the
publication-collector
package:describe('lists.public', function () { it('sends all public lists', function (done) { // Allows us to look at the output of a publication without // needing a client connection const collector = new PublicationCollector(); collector.collect('lists.public', (collections) => { chai.assert.equal(collections.Lists.length, 3); done(); }); }); });
There's a lot of scope for better isolation and testing utilities (the two packages from the example app above could be improved greatly!). We encourage the community to take the lead on these.