Ember应用测试 #

一、应用测试概述 #

应用测试(Acceptance Test)用于测试完整的用户流程,模拟真实用户行为。

1.1 生成测试 #

bash
ember generate acceptance-test login
ember generate acceptance-test user-flow

1.2 测试结构 #

javascript
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, currentURL } from '@ember/test-helpers';

module('Acceptance | login', function (hooks) {
  setupApplicationTest(hooks);

  test('visiting /login', async function (assert) {
    await visit('/login');

    assert.strictEqual(currentURL(), '/login');
  });
});

二、导航测试 #

2.1 页面访问 #

javascript
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, currentURL } from '@ember/test-helpers';

module('Acceptance | navigation', function (hooks) {
  setupApplicationTest(hooks);

  test('visiting home page', async function (assert) {
    await visit('/');

    assert.strictEqual(currentURL(), '/');
    assert.dom('h1').hasText('Welcome');
  });

  test('navigating to about page', async function (assert) {
    await visit('/');
    await click('a[href="/about"]');

    assert.strictEqual(currentURL(), '/about');
  });
});

2.2 路由保护 #

javascript
module('Acceptance | protected routes', function (hooks) {
  setupApplicationTest(hooks);

  test('redirects to login when not authenticated', async function (assert) {
    await visit('/admin');

    assert.strictEqual(currentURL(), '/login');
  });

  test('can access admin when authenticated', async function (assert) {
    // 模拟登录
    const session = this.owner.lookup('service:session');
    session.login({ id: 1, role: 'admin' });

    await visit('/admin');

    assert.strictEqual(currentURL(), '/admin');
  });
});

三、用户流程测试 #

3.1 登录流程 #

javascript
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, fillIn, click, currentURL } from '@ember/test-helpers';

module('Acceptance | login', function (hooks) {
  setupApplicationTest(hooks);

  test('user can login', async function (assert) {
    await visit('/login');

    await fillIn('[data-test-email]', 'user@example.com');
    await fillIn('[data-test-password]', 'password');
    await click('[data-test-submit]');

    assert.strictEqual(currentURL(), '/dashboard');
    assert.dom('[data-test-user-name]').hasText('User');
  });

  test('shows error on invalid credentials', async function (assert) {
    await visit('/login');

    await fillIn('[data-test-email]', 'wrong@example.com');
    await fillIn('[data-test-password]', 'wrong');
    await click('[data-test-submit]');

    assert.dom('[data-test-error]').hasText('Invalid credentials');
  });
});

3.2 CRUD流程 #

javascript
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, fillIn, click, currentURL } from '@ember/test-helpers';

module('Acceptance | posts', function (hooks) {
  setupApplicationTest(hooks);

  test('user can create a post', async function (assert) {
    await visit('/posts/new');

    await fillIn('[data-test-title]', 'New Post');
    await fillIn('[data-test-body]', 'Post content');
    await click('[data-test-submit]');

    assert.strictEqual(currentURL(), '/posts/1');
    assert.dom('h1').hasText('New Post');
  });

  test('user can edit a post', async function (assert) {
    // 创建测试数据
    const store = this.owner.lookup('service:store');
    store.createRecord('post', { id: 1, title: 'Old Title', body: 'Body' });

    await visit('/posts/1/edit');

    await fillIn('[data-test-title]', 'Updated Title');
    await click('[data-test-submit]');

    assert.dom('h1').hasText('Updated Title');
  });

  test('user can delete a post', async function (assert) {
    const store = this.owner.lookup('service:store');
    store.createRecord('post', { id: 1, title: 'Post' });

    await visit('/posts/1');

    await click('[data-test-delete]');

    assert.strictEqual(currentURL(), '/posts');
    assert.dom('[data-test-post]').doesNotExist();
  });
});

四、模拟API #

4.1 使用Pretender #

bash
ember install ember-pretenderify
javascript
// tests/acceptance/posts-test.js
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit } from '@ember/test-helpers';
import Pretender from 'pretender';

module('Acceptance | posts', function (hooks) {
  setupApplicationTest(hooks);

  hooks.beforeEach(function () {
    this.server = new Pretender();

    this.server.get('/api/posts', () => {
      return [
        200,
        { 'Content-Type': 'application/json' },
        JSON.stringify({
          posts: [
            { id: 1, title: 'Post 1' },
            { id: 2, title: 'Post 2' },
          ],
        }),
      ];
    });
  });

  hooks.afterEach(function () {
    this.server.shutdown();
  });

  test('list posts', async function (assert) {
    await visit('/posts');

    assert.dom('[data-test-post]').exists({ count: 2 });
  });
});

4.2 使用Mirage #

bash
ember install ember-cli-mirage
javascript
// tests/acceptance/posts-test.js
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit } from '@ember/test-helpers';
import { setupMirage } from 'ember-cli-mirage/test-support';

module('Acceptance | posts', function (hooks) {
  setupApplicationTest(hooks);
  setupMirage(hooks);

  test('list posts', async function (assert) {
    this.server.createList('post', 3);

    await visit('/posts');

    assert.dom('[data-test-post]').exists({ count: 3 });
  });
});

五、测试助手 #

5.1 常用测试助手 #

javascript
import {
  visit, // 访问URL
  currentURL, // 获取当前URL
  click, // 点击
  fillIn, // 填写输入
  triggerKeyEvent, // 触发键盘事件
  waitFor, // 等待元素
  settled, // 等待异步完成
  pauseTest, // 暂停测试
  resumeTest, // 恢复测试
} from '@ember/test-helpers';

5.2 自定义测试助手 #

javascript
// tests/helpers/authentication.js
import { fillIn, click } from '@ember/test-helpers';

export async function authenticate(email, password) {
  await fillIn('[data-test-email]', email);
  await fillIn('[data-test-password]', password);
  await click('[data-test-submit]');
}

export async function logout() {
  await click('[data-test-logout]');
}
javascript
// tests/acceptance/dashboard-test.js
import { authenticate } from '../helpers/authentication';

test('authenticated user can see dashboard', async function (assert) {
  await visit('/login');
  await authenticate('user@example.com', 'password');

  assert.strictEqual(currentURL(), '/dashboard');
});

六、调试测试 #

6.1 暂停测试 #

javascript
test('debug test', async function (assert) {
  await visit('/');

  await pauseTest(); // 暂停测试

  // 在浏览器控制台输入 resumeTest() 继续
});

6.2 查看DOM #

javascript
test('inspect DOM', async function (assert) {
  await visit('/');

  console.log(this.element.innerHTML);
});

七、最佳实践 #

7.1 测试用户视角 #

javascript
// 好的做法 - 从用户视角测试
test('user can search', async function (assert) {
  await visit('/search');
  await fillIn('[data-test-search-input]', 'ember');
  await click('[data-test-search-button]');

  assert.dom('[data-test-result]').exists();
});

// 避免 - 测试内部状态
test('search state', async function (assert) {
  const controller = this.owner.lookup('controller:search');
  assert.strictEqual(controller.searchTerm, 'ember');
});

7.2 合理的测试覆盖 #

javascript
// 测试主要用户流程
test('complete checkout flow', async function (assert) {
  // 添加商品到购物车
  await visit('/products/1');
  await click('[data-test-add-to-cart]');

  // 查看购物车
  await visit('/cart');
  assert.dom('[data-test-cart-item]').exists();

  // 结账
  await click('[data-test-checkout]');
  await fillIn('[data-test-card-number]', '4242424242424242');
  await click('[data-test-place-order]');

  assert.dom('[data-test-order-confirmation]').exists();
});

八、总结 #

应用测试要点:

方法 用途
visit 访问页面
click 点击元素
fillIn 填写表单
currentURL 获取当前URL
pauseTest 调试暂停

应用测试验证完整的用户流程,是质量保障的重要环节。

最后更新:2026-03-28