Ember单元测试 #
一、单元测试概述 #
单元测试用于测试独立的逻辑单元,如模型、服务、工具函数等。
1.1 生成测试 #
bash
# 生成模型测试
ember generate model-test post
# 生成服务测试
ember generate service-test session
# 生成工具函数测试
ember generate test-helper format-date
二、模型测试 #
2.1 基本模型测试 #
javascript
// tests/unit/models/post-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | post', function (hooks) {
setupTest(hooks);
test('it exists', function (assert) {
const store = this.owner.lookup('service:store');
const model = store.createRecord('post', {
title: 'Test Post',
body: 'Test content',
});
assert.ok(model, 'Model exists');
assert.strictEqual(model.title, 'Test Post');
assert.strictEqual(model.body, 'Test content');
});
});
2.2 计算属性测试 #
javascript
// tests/unit/models/user-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | user', function (hooks) {
setupTest(hooks);
test('fullName computed property', function (assert) {
const store = this.owner.lookup('service:store');
const user = store.createRecord('user', {
firstName: 'John',
lastName: 'Doe',
});
assert.strictEqual(user.fullName, 'John Doe');
});
test('initials computed property', function (assert) {
const store = this.owner.lookup('service:store');
const user = store.createRecord('user', {
firstName: 'John',
lastName: 'Doe',
});
assert.strictEqual(user.initials, 'JD');
});
});
2.3 关联关系测试 #
javascript
// tests/unit/models/post-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Model | post', function (hooks) {
setupTest(hooks);
test('it has author relationship', function (assert) {
const store = this.owner.lookup('service:store');
const post = store.createRecord('post');
assert.ok(post.belongsTo('author'), 'Has author relationship');
});
test('it has comments relationship', function (assert) {
const store = this.owner.lookup('service:store');
const post = store.createRecord('post');
assert.ok(post.hasMany('comments'), 'Has comments relationship');
});
});
三、服务测试 #
3.1 基本服务测试 #
javascript
// tests/unit/services/session-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | session', function (hooks) {
setupTest(hooks);
test('it starts unauthenticated', function (assert) {
const service = this.owner.lookup('service:session');
assert.false(service.isAuthenticated);
assert.strictEqual(service.currentUser, null);
});
test('login sets authenticated state', function (assert) {
const service = this.owner.lookup('service:session');
const user = { id: 1, name: 'Test User' };
service.login(user);
assert.true(service.isAuthenticated);
assert.deepEqual(service.currentUser, user);
});
test('logout clears authenticated state', function (assert) {
const service = this.owner.lookup('service:session');
service.login({ id: 1, name: 'Test User' });
service.logout();
assert.false(service.isAuthenticated);
assert.strictEqual(service.currentUser, null);
});
});
3.2 服务依赖测试 #
javascript
// tests/unit/services/api-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | api', function (hooks) {
setupTest(hooks);
hooks.beforeEach(function () {
// 模拟session服务
this.owner.register(
'service:session',
class MockSessionService {
isAuthenticated = true;
token = 'test-token';
}
);
});
test('it includes auth header when authenticated', function (assert) {
const service = this.owner.lookup('service:api');
const headers = service.headers;
assert.strictEqual(headers.Authorization, 'Bearer test-token');
});
});
四、工具函数测试 #
4.1 Helper测试 #
javascript
// tests/unit/helpers/format-date-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import formatDate from 'my-app/helpers/format-date';
module('Unit | Helper | format-date', function (hooks) {
setupTest(hooks);
test('it formats date correctly', function (assert) {
const date = new Date('2024-01-15T10:30:00');
const result = formatDate([date]);
assert.ok(result.includes('2024'));
});
test('it handles null', function (assert) {
const result = formatDate([null]);
assert.strictEqual(result, '');
});
test('it supports short format', function (assert) {
const date = new Date('2024-01-15');
const result = formatDate([date], { format: 'short' });
assert.ok(result);
});
});
4.2 工具类测试 #
javascript
// tests/unit/utils/calculator-test.js
import { module, test } from 'qunit';
import Calculator from 'my-app/utils/calculator';
module('Unit | Utility | calculator', function () {
test('add method', function (assert) {
const calc = new Calculator();
assert.strictEqual(calc.add(2, 3), 5);
assert.strictEqual(calc.add(-1, 1), 0);
});
test('subtract method', function (assert) {
const calc = new Calculator();
assert.strictEqual(calc.subtract(5, 3), 2);
assert.strictEqual(calc.subtract(3, 5), -2);
});
});
五、模拟和存根 #
5.1 模拟方法 #
javascript
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | auth', function (hooks) {
setupTest(hooks);
test('login calls API', async function (assert) {
const service = this.owner.lookup('service:auth');
// 模拟fetch
const originalFetch = window.fetch;
window.fetch = async (url, options) => {
assert.strictEqual(url, '/api/login');
assert.strictEqual(options.method, 'POST');
return {
ok: true,
json: async () => ({ token: 'test-token', user: { id: 1 } }),
};
};
try {
await service.login('test@example.com', 'password');
} finally {
window.fetch = originalFetch;
}
});
});
5.2 使用Sinon #
bash
ember install ember-sinon
javascript
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { setupSinon } from 'ember-sinon';
module('Unit | Service | auth', function (hooks) {
setupTest(hooks);
setupSinon(hooks);
test('login calls correct endpoint', async function (assert) {
const service = this.owner.lookup('service:auth');
const fetchStub = this.stub(window, 'fetch');
fetchStub.resolves({
ok: true,
json: async () => ({ token: 'test' }),
});
await service.login('test@example.com', 'password');
assert.ok(fetchStub.calledOnce);
assert.ok(fetchStub.calledWith('/api/login'));
});
});
六、最佳实践 #
6.1 测试独立性 #
javascript
// 好的做法 - 每个测试独立
test('test A', function (assert) {
const store = this.owner.lookup('service:store');
const model = store.createRecord('post');
// 测试A
});
test('test B', function (assert) {
const store = this.owner.lookup('service:store');
const model = store.createRecord('post');
// 测试B
});
// 避免 - 测试之间有依赖
6.2 清晰的断言 #
javascript
// 好的做法
assert.strictEqual(user.fullName, 'John Doe', 'fullName should be "John Doe"');
// 避免
assert.ok(user.fullName);
七、总结 #
单元测试要点:
| 测试对象 | 方法 |
|---|---|
| 模型 | setupTest + store |
| 服务 | setupTest + lookup |
| Helper | 直接导入调用 |
| 工具函数 | 直接导入调用 |
单元测试是测试金字塔的基础,应该大量编写。
最后更新:2026-03-28