企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# Frontend testing standards and style guidelines > 原文:[https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html](https://docs.gitlab.com/ee/development/testing_guide/frontend_testing.html) * [Vue.js testing](#vuejs-testing) * [Jest](#jest) * [Karma test suite](#karma-test-suite) * [When should I use Jest over Karma?](#when-should-i-use-jest-over-karma) * [Differences to Karma](#differences-to-karma) * [Limitations of jsdom](#limitations-of-jsdom) * [Debugging Jest tests](#debugging-jest-tests) * [Timeout error](#timeout-error) * [What and how to test](#what-and-how-to-test) * [Don’t test the library](#dont-test-the-library) * [Don’t test your mock](#dont-test-your-mock) * [Follow the user](#follow-the-user) * [Common practices](#common-practices) * [How to query DOM elements](#how-to-query-dom-elements) * [Naming unit tests](#naming-unit-tests) * [Testing promises](#testing-promises) * [Manipulating Time](#manipulating-time) * [`setTimeout()` / `setInterval()` in application](#settimeout--setinterval-in-application) * [Waiting in tests](#waiting-in-tests) * [Promises and Ajax calls](#promises-and-ajax-calls) * [Vue rendering](#vue-rendering) * [Events](#events) * [Ensuring that tests are isolated](#ensuring-that-tests-are-isolated) * [Jest best practices](#jest-best-practices) * [Prefer `toBe` over `toEqual` when comparing primitive values](#prefer-tobe-over-toequal-when-comparing-primitive-values) * [Prefer more befitting matchers](#prefer-more-befitting-matchers) * [Avoid using `toBeTruthy` or `toBeFalsy`](#avoid-using-tobetruthy-or-tobefalsy) * [Tricky `toBeDefined` matcher](#tricky-tobedefined-matcher) * [Avoid using `setImmediate`](#avoid-using-setimmediate) * [Factories](#factories) * [Mocking Strategies with Jest](#mocking-strategies-with-jest) * [Stubbing and Mocking](#stubbing-and-mocking) * [Manual module mocks](#manual-module-mocks) * [Where should I put manual mocks?](#where-should-i-put-manual-mocks) * [Manual mock examples](#manual-mock-examples) * [Keep mocks light](#keep-mocks-light) * [Additional mocking techniques](#additional-mocking-techniques) * [Running Frontend Tests](#running-frontend-tests) * [Live testing and focused testing – Jest](#live-testing-and-focused-testing----jest) * [Live testing and focused testing – Karma](#live-testing-and-focused-testing----karma) * [Frontend test fixtures](#frontend-test-fixtures) * [Data-driven tests](#data-driven-tests) * [Gotchas](#gotchas) * [RSpec errors due to JavaScript](#rspec-errors-due-to-javascript) * [Overview of Frontend Testing Levels](#overview-of-frontend-testing-levels) * [Test helpers](#test-helpers) * [Vuex Helper: `testAction`](#vuex-helper-testaction) * [Wait until Axios requests finish](#wait-until-axios-requests-finish) * [Testing with older browsers](#testing-with-older-browsers) * [BrowserStack](#browserstack) * [Firefox](#firefox) * [macOS](#macos) # Frontend testing standards and style guidelines[](#frontend-testing-standards-and-style-guidelines "Permalink") 在 GitLab 上开发前端代码时,会遇到两种测试套件. 我们将 Karma 与 Jasmine 和 Jest 一起用于 JavaScript 单元和集成测试,将 RSpec 功能测试与 Capybara 一起用于 e2e(端对端)集成测试. 需要为所有新功能编写单元和功能测试. 大多数时候,您应该使用[RSpec](https://github.com/rspec/rspec-rails#feature-specs)进行功能测试. 应该编写回归测试来修复错误,以防止将来再次发生. See the [Testing Standards and Style Guidelines](index.html) page for more information on general testing practices at GitLab. ## Vue.js testing[](#vuejs-testing "Permalink") 如果您正在寻找有关 Vue 组件测试的指南,则可以立即跳至本[部分](../fe_guide/vue.html#testing-vue-components) . ## Jest[](#jest "Permalink") 我们已经开始将前端测试迁移到[Jest](https://jestjs.io)测试框架(另请参见相应的[epic](https://gitlab.com/groups/gitlab-org/-/epics/895) ). 开玩笑的测试可以在 EE 的`/spec/frontend`和`/ee/spec/frontend`中找到. > **Note:** > > 大多数示例都有 Jest 和 Karma 示例. 如果您在发现过程中偶然发现了一些用例,请仅参阅 Karma 示例以解释代码中发生的事情. 开玩笑的例子是您应该遵循的例子. ## Karma test suite[](#karma-test-suite "Permalink") 当 GitLab 切换到[Jest 时,](https://jestjs.io)您仍然会在我们的应用程序中找到 Karma 测试. [Karma](http://karma-runner.github.io/)是使用[Jasmine](https://jasmine.github.io/)作为测试框架的测试运行程序. Jest 还使用 Jasmine 作为基础,这就是为什么它看起来非常相似的原因. 业力测试存在于`/ee/spec/javascripts`中的`spec/javascripts/`和`/ee/spec/javascripts`中. `app/assets/javascripts/behaviors/autosize.js`可能具有相应的`spec/javascripts/behaviors/autosize_spec.js`文件. 请记住,在 CI 环境中,这些测试是在无头浏览器中运行的,您将无权访问某些必须进行存根的 API,例如[`Notification`](https://s0developer0mozilla0org.icopy.site/en-US/docs/Web/API/notification) . ### When should I use Jest over Karma?[](#when-should-i-use-jest-over-karma "Permalink") 如果您需要更新现有的 Karma 测试文件(可在`spec/javascripts`找到),则无需将整个规范迁移到 Jest. 只需更新 Karma 规范以测试您的更改就可以了. 在单独的合并请求中迁移到 Jest 可能更合适. 如果创建新的测试文件,则需要在 Jest 中创建它. 这将有助于支持我们的迁移,我们认为您会喜欢使用 Jest. 与往常一样,请谨慎使用. Jest 解决了我们在 Karma 中遇到的许多问题,并提供了更好的开发人员体验,但是可能会出现潜在的意外问题(尤其是针对浏览器特定功能进行测试). ### Differences to Karma[](#differences-to-karma "Permalink") * Jest 在 Node.js 环境中运行,而不是在浏览器中. 运行在浏览器中测试玩笑支持[计划](https://gitlab.com/gitlab-org/gitlab/-/issues/26982) . * 由于 Jest 在 Node.js 环境中运行,因此默认情况下它使用[jsdom](https://github.com/jsdom/jsdom) . 另请参见下面的[限制](#limitations-of-jsdom) . * Jest 无权访问 Webpack 加载程序或别名. Jest 使用的别名是在其[自己的配置](https://gitlab.com/gitlab-org/gitlab/blob/master/jest.config.js)中定义的. * 对`setTimeout`和`setInterval`所有调用都被模拟掉了. 另请参阅[Jest Timer Mocks](https://jestjs.io/docs/en/timer-mocks) . * `rewire`不需要因为玩笑支撑嘲笑模块. 另请参见" [手动模拟"](https://jestjs.io/docs/en/manual-mocks) . * 没有[上下文对象](https://jasmine.github.io/tutorials/your_first_suite#section-The_<code>this</code>_keyword)传递给 Jest 中的测试. 这意味着`this.something`在`beforeEach()`和`it()`之间共享`this.something`不起作用. 相反,您应该在需要共享变量的上下文中声明它们(通过`const` / `let` ). * 以下将导致测试在 Jest 中失败: * 未模拟的请求. * 未处理的承诺拒绝. * 调用`console.warn` ,包括来自 Vue 之类的库的警告. ### Limitations of jsdom[](#limitations-of-jsdom "Permalink") [如上所述](#differences-to-karma) ,Jest 使用 jsdom 而不是浏览器来运行测试. 这有很多限制,即: * [No scrolling support](https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/browser/Window.js#L623-L625) * [No element sizes or positions](https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/living/nodes/Element-impl.js#L334-L371) * 一般[没有版面设计引擎](https://github.com/jsdom/jsdom/issues/1322) 另请参阅有关[在浏览器中运行 Jest 测试的支持](https://gitlab.com/gitlab-org/gitlab/-/issues/26982)问题. ### Debugging Jest tests[](#debugging-jest-tests "Permalink") 运行`yarn jest-debug`将在调试模式下运行 Jest,从而允许您按照[Jest docs](https://jestjs.io/docs/en/troubleshooting#tests-are-failing-and-you-don-t-know-why)中的说明进行调试/检查. ### Timeout error[](#timeout-error "Permalink") Jest 的默认超时是在[`/spec/frontend/test_setup.js`](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/frontend/test_setup.js)设置的. 如果您的测试超过该时间,它将失败. 如果无法提高测试性能,则可以使用[`setTestTimeout`](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/frontend/helpers/timeout.js)来增加特定测试的超时时间. ``` import { setTestTimeout } from 'helpers/timeout'; describe('Component', () => { it('does something amazing', () => { setTestTimeout(500); // ... }); }); ``` 请记住,每个测试的性能取决于环境. ## What and how to test[](#what-and-how-to-test "Permalink") 在深入了解有关 Jest 特定工作流程(如模拟和间谍)的更具体细节之前,我们应该简要介绍一下使用 Jest 测试的内容. ### Don’t test the library[](#dont-test-the-library "Permalink") 库是任何 JavaScript 开发人员生活中不可或缺的一部分. 一般建议是不要测试库的内部,但是希望库知道它应该做什么并且自己进行测试覆盖. 一个一般的例子可能是这样的 ``` import { convertToFahrenheit } from 'temperatureLibrary' function getFahrenheit(celsius) { return convertToFahrenheit(celsius) } ``` 测试我们的`getFahrenheit`函数没有任何意义,因为在其下面除了调用库函数外,什么都没有做,而且我们可以预期它正在按预期工作. (简化,我知道) 让我们看一下 Vue 领域. Vue 是 GitLab JavaScript 代码库的关键部分. 在编写 Vue 组件规范时,通常的陷阱是最终测试 Vue 提供的功能,因为这似乎是最容易测试的事情. 这是取自我们代码库的示例. ``` // Component { computed: { hasMetricTypes() { return this.metricTypes.length; }, } ``` 这是相应的规格 ``` describe('computed', () => { describe('hasMetricTypes', () => { it('returns true if metricTypes exist', () => { factory({ metricTypes }); expect(wrapper.vm.hasMetricTypes).toBe(2); }); it('returns true if no metricTypes exist', () => { factory(); expect(wrapper.vm.hasMetricTypes).toBe(0); }); }); }); ``` 测试`hasMetricTypes`计算的道具似乎是给定的,但是要测试所计算的属性是否返回`metricTypes`的长度, `metricTypes`测试 Vue 库本身. 除了将其添加到测试套件之外,这没有任何价值. 最好以用户与之交互的方式对其进行测试. 大概通过模板. 请注意这些测试,因为它们只会使更新逻辑变得比所需的更加脆弱和乏味. 其他库也是如此. 在[前端单元测试部分中](testing_levels.html#frontend-unit-tests)可以找到更多示例. ### Don’t test your mock[](#dont-test-your-mock "Permalink") 另一个常见的陷阱是规范最终验证了该模拟程序是否正常运行. 如果使用模拟,则模拟应支持测试,但不应成为测试的目标. **Bad:** ``` const spy = jest.spyOn(idGenerator, 'create') spy.mockImplementation = () = '1234' expect(idGenerator.create()).toBe('1234') ``` **Good:** ``` const spy = jest.spyOn(idGenerator, 'create') spy.mockImplementation = () = '1234' // Actually focusing on the logic of your component and just leverage the controllable mocks output expect(wrapper.find('div').html()).toBe('<div id="1234">...</div>') ``` ### Follow the user[](#follow-the-user "Permalink") 在繁重的组件环境中,单元测试和集成测试之间的界限可能非常模糊. 最重要的准则如下: * 编写干净的单元测试,以孤立的方式测试复杂的逻辑部分是否具有实际价值,以防止将来被破坏 * 否则,请尝试将规格写得尽可能接近用户的需求 例如,与手动调用方法并验证数据结构或计算的属性相比,使用生成的标记来触发按钮单击并验证相应更改的标记更好. 在测试通过并提供错误的安全感的同时,总是有偶然破坏用户流的机会. ## Common practices[](#common-practices "Permalink") 接下来,您会发现一些通用的常规做法,它们将作为我们测试套件的一部分. 如果您发现不遵循本指南的内容,最好立即进行修复. ### How to query DOM elements[](#how-to-query-dom-elements "Permalink") 当涉及到测试中的 DOM 元素查询时,最好以唯一且语义上为目标的元素. 有时这无法切实可行. 在这些情况下,添加测试属性以简化选择器可能是最佳选择. 优选地,在使用`@vue/test-utils`进行组件测试时,您应该使用组件本身来查询子组件. 这有助于强制执行该组件的各个单元测试可以涵盖的特定行为. 否则,请尝试使用: * 语义属性(例如`name` (还验证`name`是否正确设置) * 一个`data-testid`属性( [由`@vue/test-utils`维护者推荐](https://github.com/vuejs/vue-test-utils/issues/1498#issuecomment-610133465) ) * Vue `ref` (如果使用`@vue/test-utils` ) Examples: ``` it('exists', () => { // Good wrapper.find(FooComponent); wrapper.find('input[name=foo]'); wrapper.find('[data-testid="foo"]'); wrapper.find({ ref: 'foo'}); // Bad wrapper.find('.js-foo'); wrapper.find('.btn-primary'); wrapper.find('.qa-foo-component'); wrapper.find('[data-qa-selector="foo"]'); }); ``` 不建议您仅添加`.js-*`类用于测试目的. 仅当没有其他可行的选择时才这样做. 除 QA 端到端测试外,请勿将`.qa-*`类或`data-qa-selector`属性用于任何测试. ### Naming unit tests[](#naming-unit-tests "Permalink") 在编写描述测试块以测试特定功能/方法时,请使用方法名称作为描述块名称. ``` // Good describe('methodName', () => { it('passes', () => { expect(true).toEqual(true); }); }); // Bad describe('#methodName', () => { it('passes', () => { expect(true).toEqual(true); }); }); // Bad describe('.methodName', () => { it('passes', () => { expect(true).toEqual(true); }); }); ``` ### Testing promises[](#testing-promises "Permalink") 在测试 Promises 时,应始终确保测试是异步的并且拒绝被处理. 现在可以在测试套件中使用`async/await`语法: ``` it('tests a promise', async () => { const users = await fetchUsers() expect(users.length).toBe(42) }); it('tests a promise rejection', async () => { expect.assertions(1); try { await user.getUserName(1); } catch (e) { expect(e).toEqual({ error: 'User with 1 not found.', }); } }); ``` 您也可以使用 Promise 链. 在这种情况下,您可以利用`done`回调和`done.fail`发生错误. 以下是一些示例: ``` // Good it('tests a promise', done => { promise .then(data => { expect(data).toBe(asExpected); }) .then(done) .catch(done.fail); }); // Good it('tests a promise rejection', done => { promise .then(done.fail) .catch(error => { expect(error).toBe(expectedError); }) .then(done) .catch(done.fail); }); // Bad (missing done callback) it('tests a promise', () => { promise.then(data => { expect(data).toBe(asExpected); }); }); // Bad (missing catch) it('tests a promise', done => { promise .then(data => { expect(data).toBe(asExpected); }) .then(done); }); // Bad (use done.fail in asynchronous tests) it('tests a promise', done => { promise .then(data => { expect(data).toBe(asExpected); }) .then(done) .catch(fail); }); // Bad (missing catch) it('tests a promise rejection', done => { promise .catch(error => { expect(error).toBe(expectedError); }) .then(done); }); ``` ### Manipulating Time[](#manipulating-time "Permalink") 有时我们必须测试时间敏感的代码. 例如,每隔 X 秒或类似的时间运行一次重复发生的事件. 在这里,您将找到一些应对策略: #### `setTimeout()` / `setInterval()` in application[](#settimeout--setinterval-in-application "Permalink") 如果应用程序本身正在等待一段时间,请模拟等待. 在 Jest 中[,默认情况下](https://gitlab.com/gitlab-org/gitlab/blob/a2128edfee799e49a8732bfa235e2c5e14949c68/jest.config.js#L47)已[完成](https://gitlab.com/gitlab-org/gitlab/blob/a2128edfee799e49a8732bfa235e2c5e14949c68/jest.config.js#L47)此[操作](https://gitlab.com/gitlab-org/gitlab/blob/a2128edfee799e49a8732bfa235e2c5e14949c68/jest.config.js#L47) (另请参见[Jest Timer Mocks](https://jestjs.io/docs/en/timer-mocks) ). 在 Karma 中,您可以使用[Jasmine 模拟时钟](https://jasmine.github.io/api/2.9/Clock.html) . ``` const doSomethingLater = () => { setTimeout(() => { // do something }, 4000); }; ``` **在:** ``` it('does something', () => { doSomethingLater(); jest.runAllTimers(); expect(something).toBe('done'); }); ``` **在业力中:** ``` it('does something', () => { jasmine.clock().install(); doSomethingLater(); jasmine.clock().tick(4000); expect(something).toBe('done'); jasmine.clock().uninstall(); }); ``` ### Waiting in tests[](#waiting-in-tests "Permalink") 有时,测试需要等待应用程序中的某些事情发生后才能继续. 避免使用[`setTimeout`](https://s0developer0mozilla0org.icopy.site/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)因为它会使等待的原因不清楚,如果在 Karma 中使用的时间大于零,则会减慢我们的测试套件的速度. 而是使用以下方法之一. #### Promises and Ajax calls[](#promises-and-ajax-calls "Permalink") 注册处理程序函数以等待`Promise`被解决. ``` const askTheServer = () => { return axios .get('/endpoint') .then(response => { // do something }) .catch(error => { // do something else }); }; ``` **在:** ``` it('waits for an Ajax call', async () => { await askTheServer() expect(something).toBe('done'); }); ``` **在业力中:** ``` it('waits for an Ajax call', done => { askTheServer() .then(() => { expect(something).toBe('done'); }) .then(done) .catch(done.fail); }); ``` 如果您无法将处理程序注册到`Promise` ,例如因为它是在同步 Vue 生命周期挂钩中执行的,请查看[waitFor](#wait-until-axios-requests-finish)帮助器,或者您可以刷新所有待处理的`Promise` : **在:** ``` it('waits for an Ajax call', () => { synchronousFunction(); jest.runAllTicks(); expect(something).toBe('done'); }); ``` #### Vue rendering[](#vue-rendering "Permalink") 要等到重新渲染 Vue 组件后,请使用等效的[`Vue.nextTick()`](https://vuejs.org/v2/api/#Vue-nextTick)或`vm.$nextTick()` . **在:** ``` it('renders something', () => { wrapper.setProps({ value: 'new value' }); return wrapper.vm.$nextTick().then(() => { expect(wrapper.text()).toBe('new value'); }); }); ``` **in Karma:** ``` it('renders something', done => { wrapper.setProps({ value: 'new value' }); wrapper.vm .$nextTick() .then(() => { expect(wrapper.text()).toBe('new value'); }) .then(done) .catch(done.fail); }); ``` #### Events[](#events "Permalink") 如果应用程序触发了您需要在测试中等待的事件,请注册一个包含断言的事件处理程序: ``` it('waits for an event', done => { eventHub.$once('someEvent', eventHandler); someFunction(); function eventHandler() { expect(something).toBe('done'); done(); } }); ``` 在 Jest 中,您还可以使用`Promise` : ``` it('waits for an event', () => { const eventTriggered = new Promise(resolve => eventHub.$once('someEvent', resolve)); someFunction(); return eventTriggered.then(() => { expect(something).toBe('done'); }); }); ``` ### Ensuring that tests are isolated[](#ensuring-that-tests-are-isolated "Permalink") 测试通常以一种模式进行架构,该模式要求重复设置并破坏被测组件. 这是通过使用`beforeEach`和`afterEach`挂钩完成的. Example ``` let wrapper; beforeEach(() => { wrapper = mount(Component); }); afterEach(() => { wrapper.destroy(); }); ``` 最初查看此内容时,您会怀疑该组件是在每次测试之前设置的,然后在以后进行了细分,从而在测试之间提供了隔离. 但是,这并不是完全正确的,因为`destroy`方法不会删除`wrapper`对象上所有已突变的东西. 对于功能组件,destroy 仅从文档中删除呈现的 DOM 元素. In order to ensure that a clean wrapper object and DOM are being used in each test, the breakdown of the component should rather be performed as follows: ``` afterEach(() => { wrapper.destroy(); wrapper = null; }); ``` 另请参见[有关`destroy`](https://vue-test-utils.vuejs.org/api/wrapper/#destroy)的[Vue Test Utils 文档](https://vue-test-utils.vuejs.org/api/wrapper/#destroy) . ### Jest best practices[](#jest-best-practices "Permalink") 在 GitLab 13.2 中[引入](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/34209) . #### Prefer `toBe` over `toEqual` when comparing primitive values[](#prefer-tobe-over-toequal-when-comparing-primitive-values "Permalink") [`toBe`](https://jestjs.io/docs/en/expect#tobevalue) [`toEqual`](https://jestjs.io/docs/en/expect#toequalvalue)匹配者. 由于[`toBe`](https://jestjs.io/docs/en/expect#tobevalue)使用[`Object.is`](https://s0developer0mozilla0org.icopy.site/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is)比较值,因此(默认情况下)比使用`toEqual` . 尽管后者最终将回退以利用[`Object.is`](https://github.com/facebook/jest/blob/master/packages/expect/src/jasmineUtils.ts#L91)获得原始值,但仅应在需要比较复杂对象的情况下使用它. Examples: ``` const foo = 1; // good expect(foo).toBe(1); // bad expect(foo).toEqual(1); ``` #### Prefer more befitting matchers[](#prefer-more-befitting-matchers "Permalink") Jest 提供了有用的匹配器,例如`toHaveLength`或`toBeUndefined`以使您的测试更具可读性并产生更易理解的错误消息. 查看他们的文档以获取[匹配器](https://jestjs.io/docs/en/expect#methods)的[完整列表](https://jestjs.io/docs/en/expect#methods) . Examples: ``` const arr = [1, 2]; // prints: // Expected length: 1 // Received length: 2 expect(arr).toHaveLength(1); // prints: // Expected: 1 // Received: 2 expect(arr.length).toBe(1); // prints: // expect(received).toBe(expected) // Object.is equality // Expected: undefined // Received: "bar" const foo = 'bar'; expect(foo).toBe(undefined); // prints: // expect(received).toBeUndefined() // Received: "bar" const foo = 'bar'; expect(foo).toBeUndefined(); ``` #### Avoid using `toBeTruthy` or `toBeFalsy`[](#avoid-using-tobetruthy-or-tobefalsy "Permalink") Jest 还提供以下匹配器: `toBeTruthy`和`toBeFalsy` . 我们不应该使用它们,因为它们会使测试变弱并产生假阳性结果. 例如,当`someBoolean === null`以及`someBoolean === false`时,会通过`expect(someBoolean).toBeFalsy()` . #### Tricky `toBeDefined` matcher[](#tricky-tobedefined-matcher "Permalink") Jest 有一个棘手的`toBeDefined`匹配器,它可能会产生假阳性测试. 因为它仅[验证](https://github.com/facebook/jest/blob/master/packages/expect/src/matchers.ts#L204) `undefined`的给定值. ``` // good expect(wrapper.find('foo').exists()).toBe(true); // bad // if finder returns null, the test will pass expect(wrapper.find('foo')).toBeDefined(); ``` #### Avoid using `setImmediate`[](#avoid-using-setimmediate "Permalink") 尝试避免使用`setImmediate` . `setImmediate`是一个临时解决方案,可在 I / O 完成后运行您的回调. 而且它不是 Web API 的一部分,因此,我们在单元测试中以 NodeJS 环境为目标. 代替`setImmediate` ,使用`jest.runAllTimers`或`jest.runOnlyPendingTimers`来运行暂挂计时器. 当代码中有`setInterval`时,后者很有用. **请记住:**我们的 Jest 配置使用假计时器. ## Factories[](#factories "Permalink") TBU ## Mocking Strategies with Jest[](#mocking-strategies-with-jest "Permalink") ### Stubbing and Mocking[](#stubbing-and-mocking "Permalink") Jasmine 提供存根和模拟功能. 在 Karma 和 Jest 中,如何使用它有一些细微的差异. 存根或间谍通常是同义词. 在 Jest 中,使用`.spyOn`方法非常容易. [官方文档](https://jestjs.io/docs/en/jest-object#jestspyonobject-methodname)更具挑战性的部分是模拟,可用于功能甚至依赖项. ### Manual module mocks[](#manual-module-mocks "Permalink") 手动模拟用于模拟整个 Jest 环境中的模块. 这是一个功能非常强大的测试工具,它通过模拟出在我们的测试环境中不容易使用的模块来帮助简化单元测试. > **警告:**如果不应在每个规范中始终采用模拟,则不要使用手动模拟(即,仅少数规范需要使用). 而是考虑在相关规范文件中使用[`jest.mock(..)`](https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options) (或类似的[`jest.mock(..)`](https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options)功能). #### Where should I put manual mocks?[](#where-should-i-put-manual-mocks "Permalink") Jest 通过将模拟放置在源模块旁边的`__mocks__/`目录(例如`app/assets/javascripts/ide/__mocks__` )中来支持[手动模块](https://jestjs.io/docs/en/manual-mocks)模拟. **不要这样** 我们希望将所有与测试相关的代码保存在一个地方( `spec/`文件夹). 如果`node_modules`软件包需要手动模拟,请使用`spec/frontend/__mocks__`文件夹. 这是[`monaco-editor`软件包](https://gitlab.com/gitlab-org/gitlab/blob/b7f914cddec9fc5971238cdf12766e79fa1629d7/spec/frontend/__mocks__/monaco-editor/index.js#L1)的[Jest 模拟](https://gitlab.com/gitlab-org/gitlab/blob/b7f914cddec9fc5971238cdf12766e79fa1629d7/spec/frontend/__mocks__/monaco-editor/index.js#L1)示例. 如果 CE 模块需要手动模拟,请将其放在`spec/frontend/mocks/ce` . * `spec/frontend/mocks/ce`文件将从`app/assets/javascripts`模拟相应的 CE 模块,从而镜像源模块的路径. * 示例: `spec/frontend/mocks/ce/lib/utils/axios_utils`将模拟模块`~/lib/utils/axios_utils` . * 我们尚不支持模拟 EE 模块. * 如果找到了不存在源模块的模拟,则测试套件将失败. 目前尚不支持"虚拟"模拟或与源模块没有 1 对 1 关联的模拟. #### Manual mock examples[](#manual-mock-examples "Permalink") * 模拟[`mocks/axios_utils`](https://gitlab.com/gitlab-org/gitlab/blob/bd20aeb64c4eed117831556c54b40ff4aee9bfd1/spec/frontend/mocks/ce/lib/utils/axios_utils.js#L1)此模拟很有用,因为我们不希望任何未经模拟的请求通过任何测试. 另外,我们能够注入一些测试助手,例如`axios.waitForAll` . * [`__mocks__/mousetrap/index.js`](https://gitlab.com/gitlab-org/gitlab/blob/cd4c086d894226445be9d18294a060ba46572435/spec/frontend/__mocks__/mousetrap/index.js#L1)此模拟很有用,因为该模块本身使用 webpack 可以理解的 AMD 格式,但与玩笑的环境不兼容. 该模拟不会删除任何行为,仅提供与 es6 兼容的包装器. * [`__mocks__/monaco-editor/index.js`](https://gitlab.com/gitlab-org/gitlab/blob/b7f914cddec9fc5971238cdf12766e79fa1629d7/spec/frontend/__mocks__/monaco-editor/index.js) - This mock is helpful because the Monaco package is completely incompatible in a Jest environment. In fact, webpack requires a special loader to make it work. This mock simply makes this package consumable by Jest. ### Keep mocks light[](#keep-mocks-light "Permalink") 全局模拟会引入魔术,并且从技术上讲可以减少测试的覆盖范围. 当嘲笑被视为有利可图时: * 保持模拟简短而集中. * 请在模拟中留下为什么有必要的顶级评论. ### Additional mocking techniques[](#additional-mocking-techniques "Permalink") 请查阅[官方的 Jest 文档](https://jestjs.io/docs/en/jest-object#mock-modules) ,以获取有关可用模拟功能的完整概述. ## Running Frontend Tests[](#running-frontend-tests "Permalink") 要运行前端测试,您需要以下命令: * `rake frontend:fixtures` (re-)generates [fixtures](#frontend-test-fixtures). * `yarn test`执行测试. * `yarn jest`执行 Jest 测试. 只要固定装置不变, `yarn test`就足够了(并节省了一些时间). ### Live testing and focused testing – Jest[](#live-testing-and-focused-testing----jest "Permalink") 在测试套件上工作时,您可能希望以监视模式运行这些规范,因此它们会在每次保存时自动重新运行. ``` # Watch and rerun all specs matching the name icon yarn jest --watch icon # Watch and rerun one specifc file yarn jest --watch path/to/spec/file.spec.js ``` 您也可以在不使用`--watch`标志的情况下运行一些重点测试 ``` # Run specific jest file yarn jest ./path/to/local_spec.js # Run specific jest folder yarn jest ./path/to/folder/ # Run all jest files which path contain term yarn jest term ``` ### Live testing and focused testing – Karma[](#live-testing-and-focused-testing----karma "Permalink") 业力允许类似的东西,但是成本更高. 用`yarn run karma-start`运行 Karma 将编译 JavaScript 资产并在`http://localhost:9876/`上运行服务器,在该服务器上它将自动在连接到它的任何浏览器上运行测试. 您可以一次在多个浏览器上输入该 URL,以使其在每个浏览器上并行运行测试. 当 Karma 运行时,您所做的任何更改都会立即触发**整个测试套件**的重新编译和重新**测试** ,因此您可以立即查看是否使用所做的更改破坏了测试. 您可以使用[针对 Jasmine 的](https://jasmine.github.io/2.5/focused_specs.html)测试或排除测试(使用`fdescribe`或`xdescribe` )来使 Karma 仅在您使用特定功能时运行所需的测试,但是请确保在提交代码时删除这些指令. 通过使用参数`--filter-spec`或 short `-f`过滤运行测试,也可以仅在特定的文件夹或文件上运行 Karma: ``` # Run all files yarn karma-start # Run specific spec files yarn karma-start --filter-spec profile/account/components/update_username_spec.js # Run specific spec folder yarn karma-start --filter-spec profile/account/components/ # Run all specs which path contain vue_shared or vie yarn karma-start -f vue_shared -f vue_mr_widget ``` 您还可以使用 glob 语法来匹配文件. 请记住在引号周围加上引号,否则您的 shell 可能会将其拆分为多个参数: ``` # Run all specs named `file_spec` within the IDE subdirectory yarn karma -f 'spec/javascripts/ide/**/file_spec.js' ``` ## Frontend test fixtures[](#frontend-test-fixtures "Permalink") 添加到 HAML 模板(在`app/views/` )或向后端发出 Ajax 请求的代码具有要求后端提供 HTML 或 JSON 的测试. 这些测试的夹具位于: * `spec/frontend/fixtures/` ,用于在 CE 中运行测试. * `ee/spec/frontend/fixtures/` ,用于在 EE 中运行测试. 灯具文件位于: * Karma 测试套件由[jasmine-jquery 提供](https://github.com/velesin/jasmine-jquery) . * 开玩笑地使用`spec/frontend/helpers/fixtures.js` . 以下是适用于 Karma 和 Jest 的测试示例: ``` it('makes a request', () => { const responseBody = getJSONFixture('some/fixture.json'); // loads spec/frontend/fixtures/some/fixture.json axiosMock.onGet(endpoint).reply(200, responseBody); myButton.click(); // ... }); it('uses some HTML element', () => { loadFixtures('some/page.html'); // loads spec/frontend/fixtures/some/page.html and adds it to the DOM const element = document.getElementById('#my-id'); // ... }); ``` HTML 和 JSON 固定装置是使用 RSpec 从后端视图和控制器生成的(请参阅`spec/frontend/fixtures/*.rb` ). 对于每个灯具, `response`变量的内容存储在输出文件中. 如果测试标记为`type: :request`或`type: :controller`则此变量将自动设置. 可以使用`bin/rake frontend:fixtures`命令来重新生成`bin/rake frontend:fixtures`但是您也可以单独生成它们,例如`bin/rspec spec/frontend/fixtures/merge_requests.rb` . 创建新的固定装置时,通常有必要在`(ee/)spec/controllers/`或`(ee/)spec/requests/`查看端点的相应测试. ## Data-driven tests[](#data-driven-tests "Permalink") 与[RSpec 的参数化测试](best_practices.html#table-based--parameterized-tests)类似,Jest 支持以下数据驱动的测试: * 使用[`test.each`](https://jestjs.io/docs/en/api#testeachtable-name-fn-timeout)单独测试(别名为`it.each` ). * 使用[`describe.each`](https://jestjs.io/docs/en/api#describeeachtable-name-fn-timeout)的测试组. 这些对于减少测试中的重复很有用. 每个选项都可以采用数据值数组或带标签的模板文字. 例如: ``` // function to test const icon = status => status ? 'pipeline-passed' : 'pipeline-failed' const message = status => status ? 'pipeline-passed' : 'pipeline-failed' // test with array block it.each([ [false, 'pipeline-failed'], [true, 'pipeline-passed'] ])('icon with %s will return %s', (status, icon) => { expect(renderPipeline(status)).toEqual(icon) } ); ``` ``` // test suite with tagged template literal block describe.each` status | icon | message ${false} | ${'pipeline-failed'} | ${'Pipeline failed - boo-urns'} ${true} | ${'pipeline-passed'} | ${'Pipeline succeeded - win!'} `('pipeline component', ({ status, icon, message }) => { it(`returns icon ${icon} with status ${status}`, () => { expect(icon(status)).toEqual(message) }) it(`returns message ${message} with status ${status}`, () => { expect(message(status)).toEqual(message) }) }); ``` ## Gotchas[](#gotchas "Permalink") ### RSpec errors due to JavaScript[](#rspec-errors-due-to-javascript "Permalink") 默认情况下,RSpec 单元测试不会在无头浏览器中运行 JavaScript,而仅依赖于检查 rails 生成的 HTML. 如果集成测试依赖 JavaScript 才能正确运行,则需要确保将规范配置为在运行测试时启用 JavaScript. 如果不这样做,您会看到规范运行器发出的模糊错误消息. To enable a JavaScript driver in an `rspec` test, add `:js` to the individual spec or the context block containing multiple specs that need JavaScript enabled: ``` # For one spec it 'presents information about abuse report', :js do # assertions... end describe "Admin::AbuseReports", :js do it 'presents information about abuse report' do # assertions... end it 'shows buttons for adding to abuse report' do # assertions... end end ``` ## Overview of Frontend Testing Levels[](#overview-of-frontend-testing-levels "Permalink") 有关前端测试级别的主要信息,请参见" [测试级别"页面](testing_levels.html) . 与前端开发相关的测试可以在以下位置找到: * `spec/javascripts/` ,用于业力测试 * `spec/frontend/` ,用于 Is 测试 * `spec/features/` ,用于 RSpec 测试 RSpec 运行完整的[功能测试](testing_levels.html#frontend-feature-tests) ,而 Jest 和 Karma 目录包含[前端单元测试](testing_levels.html#frontend-unit-tests) , [前端组件测试](testing_levels.html#frontend-component-tests)和[前端集成测试](testing_levels.html#frontend-integration-tests) . `spec/javascripts/`所有测试最终都将迁移到`spec/frontend/` (另请参见[#52483](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/52483) ). 在 2018 年 5 月之前, `features/`还包含 Spinach 运行的功能测试. 这些测试已于 2018 年 5 月从代码库中删除( [#23036](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/23036) ). 另请参阅[有关测试 Vue 组件的说明](../fe_guide/vue.html#testing-vue-components) . ## Test helpers[](#test-helpers "Permalink") ### Vuex Helper: `testAction`[](#vuex-helper-testaction "Permalink") 根据[官方文档](https://vuex.vuejs.org/guide/testing.html) ,我们提供了一个帮助程序,可以简化测试操作: ``` testAction( actions.actionName, // action { }, // params to be passed to action state, // state [ { type: types.MUTATION}, { type: types.MUTATION_1, payload: {}}, ], // mutations committed [ { type: 'actionName', payload: {}}, { type: 'actionName1', payload: {}}, ] // actions dispatched done, ); ``` 在[`spec/javascripts/ide/stores/actions_spec.jsspec/javascripts/ide/stores/actions_spec.js`](https://gitlab.com/gitlab-org/gitlab/blob/master/spec/javascripts/ide/stores/actions_spec.js) . ### Wait until Axios requests finish[](#wait-until-axios-requests-finish "Permalink") The Axios Utils mock module located in `spec/frontend/mocks/ce/lib/utils/axios_utils.js` contains two helper methods for Jest tests that spawn HTTP requests. These are very useful if you don’t have a handle to the request’s Promise, for example when a Vue component does a request as part of its life cycle. * `waitFor(url, callback)` :对`url`的请求完成后(成功或失败)运行`callback` . * `waitForAll(callback)` :所有未完成的请求完成后,运行`callback` . 如果没有待处理的请求,则在下一个刻度上运行`callback` . 这两个函数运行`callback` (使用请求结束后的下一个滴答`setImmediate()`以允许任何`.then()`或`.catch()`处理程序来运行. ## Testing with older browsers[](#testing-with-older-browsers "Permalink") 某些回归仅影响特定的浏览器版本. 我们可以按照以下步骤使用 Firefox 或 BrowserStack 在特定的浏览器中安装和测试: ### BrowserStack[](#browserstack "Permalink") [BrowserStack](https://www.browserstack.com/)可以测试 1200 多种移动设备和浏览器. 您可以直接通过[实时应用程序](https://www.browserstack.com/live)使用它,也可以安装[chrome 扩展程序](https://chrome.google.com/webstore/detail/browserstack/nkihdmlheodkdfojglpcjjmioefjahjb)以便于访问. 使用保存在 GitLab [共享 1Password 帐户](https://about.gitlab.com/handbook/security/#1password-guide)的**Engineering**库中的凭据登录到 BrowserStack. ### Firefox[](#firefox "Permalink") #### macOS[](#macos "Permalink") 您可以从发布的 FTP 服务器[https://ftp.mozilla.org/pub/firefox/releases/](https://ftp.mozilla.org/pub/firefox/releases/)下载任何较旧版本的 Firefox: 1. 从网站上选择一个版本,在本例中为`50.0.1` . 2. 转到 mac 文件夹. 3. 选择您喜欢的语言,您将在其中找到 DMG 软件包并下载. 4. 将应用程序拖放到"应用`Applications`文件夹以外的任何其他文件夹中. 5. 将应用程序重命名为`Firefox_Old` . 6. 将应用程序移至"应用`Applications`文件夹. 7. 打开终端,然后运行`/Applications/Firefox_Old.app/Contents/MacOS/firefox-bin -profilemanager`以创建特定于该 Firefox 版本的新配置文件. 8. 创建配置文件后,请退出应用程序,然后像平常一样再次运行它. 现在,您可以使用较旧的 Firefox 版本. * * * [Return to Testing documentation](index.html)