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