Skip to content

Commit

Permalink
Replace avoriaz with Vue Test Utils
Browse files Browse the repository at this point in the history
Closes #173. Notes:

- avoriaz has some information on migrating to Vue Test Utils (VTU):
  https://github.com/eddyerburgh/avoriaz/blob/master/docs/guides/migrating-to-vue-test-utils.md
- VTU wrappers no longer have a data() method. Using the `vm` property
  to access data properties instead.
- Replace wrapper methods:
  - avoriaz() getProp() becomes VTU props()
  - hasClass() becomes classes()
  - getAttribute() and hasAttribute() become attributes()
  - first() becomes get() or getComponent()
  - find() becomes findAll() or findAllComponents()
    - Unless doing an existence check, in which case we call either
      find() or findComponent(), then call exists().
- For a wrapper of a Vue component, access the element using the
  `element` property rather than using vm.$el.
- The VTU wrapper method text() trims the text. The avoriaz wrapper
  method did not, so we often called trim() after text(). We now access
  element.textContent directly in the rare case that we do not want the
  text to be trimmed.
- The VTU wrapper method html() prettifies the HTML, which the avoriaz
  wrapper method did not. That may be useful for us in the future, but
  for our few existing tests that used the method (in
  SubmissionFeedEntry), we now use element.outerHTML instead.
- Replace our `trigger` object with the VTU wrapper methods trigger(),
  setValue(), and setChecked().
  - But keeping a version of trigger.dragAndDrop(), in the new
    test/util/file.js.
  - The methods of the `trigger` object returned a promise that resolved
    to the avoriaz wrapper. The VTU wrapper methods return a promise
    that doesn't resolve to a value, so I had to restructure some
    promise chains. In many cases, I rewrote the promise chain using
    async/await.
  - It is now possible to specify options when using VTU to trigger an
    event, so we don't need to use jQuery to mock events. I removed
    jQuery from FormNew and FormAttachmentUploadFiles, which only used
    jQuery so that we could mock events in testing.
  - The tests of FormNew have good examples of some of these changes.
- Use the VTU wrapper method emitted() instead of using Sinon to fake
  $emit(). (The tests of DateRangePicker have good examples of this
  change.)
- Replace attachToDocument with attachTo.
- avoriaz allowed a wrapper to be passed as a slot, but VTU does not.
  However, VTU allows a template string to be passed, which avoriaz did
  not. The only place this comes up is a test of LinkIfCan, where the
  wrapper could be replaced with a template string.
- Use the parentComponent option of mount() to account for i18n custom
  blocks. Previously, we overwrote the setProps() wrapper method.
- We inject the router in fewer tests. In some cases, we are able to
  switch from load() to mount(), making the test more isolated and often
  making it synchronous. (The tests of Download and FormDraftChecklist
  have good examples of that.)
  - If a component uses <router-link>, VTU can stub that.
  - If a component uses $route, VTU can mock that.
  - In order for VTU to mock $route, we use createLocalVue() to create a
    copy of Vue with the router and a copy without, which is what VTU
    recommends.
    - That means that src/setup.js can no longer import src/plugins.js.
      Instead, src/plugins.js is imported in src/main.js.
  - For some components, these changes result in a natural sequence of
    tests: tests first use mount() to test the behavior of the component
    before a request, then use mockHttp() to test behavior before a
    successful response, then use load() to test a route change and
    other behavior after a successful response. For some components (for
    example, AccountLogin), I reordered tests to match this sequence.
  - load() will now inject the router only if it is mounting the root
    component (App). In other cases, it stubs <router-link> and mocks
    $route.
  - The documentation for mockHttp() and load() describes some of this
    in more detail, including guidelines around which utility function
    to use in different cases.
  - One case where mocking $route doesn't work is if an async component
    uses $route: see
    vuejs/vue-test-utils#1486. This was an
    issue for ProjectSubmissionOptions, so it is no longer loaded async.
    I had actually already wanted not to load it async: it's not a large
    component, and it may be needed soon after the page renders. I think
    I set it up to load async because multiple components import it, but
    I don't think I realized at the time that webpack will automatically
    split out a large shared component into its own .js file.
  - test/index.js used to contain some complexity related to the router,
    but with these changes in place, it felt like the right time to
    remove the previous workaround. I was able to simplify things by
    having the router use abstract mode in testing. This change also
    sped up tests significantly.
  - Previously, one of the tests of AccountClaim would fail
    intermittently depending on how quickly the Password async component
    loaded. After these changes, the test failed more often (maybe
    because some of the tests before it are now faster?), so I updated
    the test to wait for the component to load. This involved a small
    change to FormGroup.
- There are benefits to mocking/stubbing for Vue Router, but there seem
  to be fewer benefits to doing so for our other plugins, Vuex and Vue
  I18n. The router contains a fair amount of logic and implements some
  of that behavior (some of which can be async) as soon as it is
  injected into a component. However, the same is not true of Vuex or
  Vue I18n.
- I'm not sure whether avoriaz created a parent component when mounting,
  but Vue Test Utils seems to. We destroy the parent component after
  each test. We used to destroy the component before removing it from
  the DOM, but we now do those in the reverse order. That matches what
  avoriaz and VTU do, and I don't see any obvious issues with that
  approach. (We introduced the previous logic in
  304e5db, maybe because Modal at the
  time used a ref in its beforeDestroy hook. However, that is no longer
  the case.)
- VTU automatically sets Vue.config.productionTip and
  Vue.config.devtools to `false`. This removes a message about Vue
  devtools that was previously shown in testing. Since VTU now sets
  productionTip, I have moved that configuration from src/setup.js to
  src/main.js.
- VTU wrappers have an isVisible() method, but I think we should
  continue using our visible() and hidden() assertions. One reason for
  that is that both assertions can be used to test style-based
  visibility. Also, the hidden() assertion is more specific than
  not.visible().
- Make other improvements to testing, including:
  - Remove deprecated functions and properties:
    - Replace mockRoute() with load() or mockHttp().
    - Replace mountAndMark() with mount().
    - Replace mockHttp().standardButton() with
      mockHttp().testStandardButton().
    - Replace testData.administrators with testData.standardUsers.
    - Remove the disabled() assertion.
  - Remove String.prototype.iTrim().
  - Use mockHttp().testModalToggles() in more tests, and call it with a
    single argument consistently.
  - Mount the root component (App) in fewer tests.
    - I initially made this change in too many cases. I've added a check
      to load() to help prevent that going forward.
  - Add an id or class attribute to certain elements to make it easier
    to select elements in testing. In other cases, shorten an existing
    class name.
  - Move tests:
    - From UserList to router.spec.js
    - From ProjectRow to ProjectIntroduction
  - Remove unneeded tests.
  • Loading branch information
matthew-white committed Jun 23, 2021
1 parent 8282e47 commit 1c9c9b3
Show file tree
Hide file tree
Showing 142 changed files with 5,165 additions and 5,498 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
}],
'max-classes-per-file': 'off',
'max-len': 'off',
'newline-per-chained-call': 'off',
'no-console': 'error',
'no-debugger': 'error',
'no-empty': ['error', { allowEmptyCatch: true }],
Expand Down
35 changes: 11 additions & 24 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,17 @@ We use Vue.js along with Vue Router, Vuex, and Vue CLI.

ODK Central Frontend uses jQuery in limited ways.

Wherever possible, we try to use Vue instead of jQuery. Vue will not always know about or respect the changes that jQuery makes, and using jQuery can add complexity to a component. It can also add complexity to testing: for example, we generally use avoriaz for testing, but if you use jQuery in a component, then in testing, you may need to use jQuery's `trigger()` method rather than avoriaz's `trigger()` method.
Wherever possible, we try to use Vue instead of jQuery. Vue will not always know about or respect changes that jQuery makes to the DOM, and using jQuery can add complexity to a component.

That said, there are a couple of occasions in which we reach for jQuery:

- We use some of Bootstrap's jQuery plugins.
- avoriaz does not allow you to mock events. However, jQuery does, so we sometimes use jQuery in order to facilitate testing.

One thing to keep in mind when using jQuery is that you will have to manually remove any jQuery listeners and data, perhaps when the component is destroyed.

If possible despite our use of Bootstrap, we may wish to remove jQuery from Frontend in the future. For that reason, if you have a choice between using jQuery and vanilla JavaScript, you should consider using the latter. Remember though that as with jQuery, Vue will not always know about or respect the changes that you make using vanilla JavaScript.
That said, we make use of some of Bootstrap's jQuery plugins. We may replace those in the future, after which we may be able to remove jQuery. With that in mind, if you have a choice between using jQuery and vanilla JavaScript, consider using the latter. Remember though that as with jQuery, Vue will not always know about or respect changes to the DOM that you make using vanilla JavaScript.

### Bootstrap

ODK Central Frontend uses Bootstrap 3. (However, we are considering [moving to Bootstrap 4](https://github.com/getodk/central-frontend/issues/142). Let us know if that is something you can help with!)
ODK Central Frontend uses Bootstrap 3.

Frontend's [global styles](/src/assets/scss/app.scss) override some of Bootstrap's, as do the styles of Frontend components that correspond to a Bootstrap component (for example, `Modal`). However, we tend to stick pretty closely to Bootstrap, and you should be able to use most of Bootstrap's examples with only small changes. If you are creating a new component that is similar to an existing one, you may find it useful to base the new component off the existing one.
Frontend's [global styles](/src/assets/scss/app.scss) override some of Bootstrap's, as do the styles of Frontend components that correspond to a Bootstrap component (for example, `Modal`). However, we tend to stick pretty closely to Bootstrap, and you should be able to use many of Bootstrap's examples with only small changes. If you are creating a new component that is similar to an existing Frontend component, you may find it useful to base the new component off the existing one.

We use some, but not all, of Bootstrap's jQuery plugins ([`/src/bootstrap.js`](/src/bootstrap.js)). We try to limit our use of Bootstrap's plugins, because they use jQuery, and jQuery tends to add complexity to components and testing in the ways described above. For example, if you use a Bootstrap plugin, then in testing, you may need to use jQuery's `trigger()` method rather than avoriaz's.
We use a limited number of Bootstrap's jQuery plugins: see the [section above](https://github.com/getodk/central-frontend/blob/master/CONTRIBUTING.md#jquery) on jQuery.

### Global Utilities

Expand Down Expand Up @@ -105,6 +98,8 @@ To learn how a given component works, one of the best places to start is how the
- Does it have slots?
- Does it emit events?

A component can also communicate with other components using the Vuex store. For example, a component may use [response data](https://github.com/getodk/central-frontend/blob/master/CONTRIBUTING.md#response-data) that another component requested.

### Component Names

We specify a name for every component, which facilitates the use of the Vue devtools. In general, we try not to use component names to drive behavior: in most ways, renaming a component should have no effect.
Expand Down Expand Up @@ -189,7 +184,7 @@ node bin/transifex/restructure.js
// Multiple comments are combined.
"hello": "Hello, world!",
// This comment will be added for each of the messages within "fruit".
fruit: {
"fruit": {
"apple": "Apple",
"banana": "Banana"
}
Expand Down Expand Up @@ -322,15 +317,15 @@ Our tests use a number of external packages:
- Should.js, for assertions
- Sinon.JS, for spies and stubs
- faker.js, to generate test data
- avoriaz, to test Vue components
- Vue Test Utils, to test Vue components

`npm run test` runs [`/test/index.js`](/test/index.js), which mocks global utilities and sets up Mocha hooks.

We extend Should.js assertions in [`/test/assertions.js`](/test/assertions.js).

[avoriaz](https://eddyerburgh.gitbooks.io/avoriaz/content/) renders Vue components for testing, allowing you to test that a component renders and behaves as expected. We have built some functionality on top of avoriaz, in particular [`mount()`](/test/util/lifecycle.js) and [`trigger`](/test/util/event.js). We define components used only for testing in [`/test/util/components/`](/test/util/components/).
[Vue Test Utils](https://vue-test-utils.vuejs.org/) renders Vue components for testing, allowing you to test that a component renders and behaves as expected. We have built some functionality on top of Vue Test Utils, in particular [`mount()`](/test/util/lifecycle.js). We define components used only for testing in [`/test/util/components/`](/test/util/components/).

Many tests involve sending a request. You can mock a series of request-response cycles by using `load()` or `mockHttp()`, defined in [`test/util/http.js`](/test/util/http.js). You can use these to implement common tests, for example, testing some standard button things: see [`/test/util/http/common.js`](/test/util/http/common.js).
Many tests involve sending a request. You can mock a series of request-response cycles by using `load()` or `mockHttp()`, defined in [`/test/util/http.js`](/test/util/http.js). You can use these to implement common tests, for example, testing some standard button things: see [`/test/util/http/common.js`](/test/util/http/common.js).

As provided by default by Mocha, add `.only` after any `describe()` or `it()` call in the tests to run only the marked tests. For example:

Expand All @@ -349,11 +344,3 @@ We generate and store test data specific to ODK Central using the [`testData`](/
Most Backend resources have a `createdAt` property. To generate an object whose `createdAt` property is in the past, use the `createPast()` method of the store or view. To generate an object whose `createdAt` property is set to the current time, use `createNew()`. Most of the time, you will use `createPast()`. For a test that mounts a component, use `createPast()` to set up data that exists before the component is mounted. Use `createNew()` for data created after the component is mounted, for example, after the component sends a POST request. You can pass options to `createPast()` and `createNew()`; each store accepts a different set of options.

To learn more about stores and views, see [`/test/data/data-store.js`](/test/data/data-store.js).

#### Improvements to Testing

We want to improve our testing in two major ways. (Let us know if this is something you can help with!)

- Moving away from Karma
- Vue CLI does not offer core support for Karma. Perhaps the easiest move would be to the unit-mocha plugin for Vue CLI, which uses JSDOM.
- Moving from avoriaz to Vue Test Utils
9 changes: 5 additions & 4 deletions karma.conf.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
/*
This config is based on:
- https://github.com/Nikku/karma-browserify
- https://vue-test-utils.vuejs.org/installation/#using-other-test-runners
- https://github.com/eddyerburgh/vue-test-utils-karma-example
- https://github.com/eddyerburgh/avoriaz-karma-mocha-example
- https://github.com/Nikku/karma-browserify
*/

const webpackConfig = require('./node_modules/@vue/cli-service/webpack.config.js');

const { entry, ...configForTests } = webpackConfig;
configForTests.devtool = 'inline-source-map';
const { entry, ...webpackConfigForKarma } = webpackConfig;
webpackConfigForKarma.devtool = 'inline-source-map';

module.exports = function(config) {
config.set({
Expand All @@ -29,7 +30,7 @@ module.exports = function(config) {
preprocessors: {
'test/index.js': ['webpack', 'sourcemap']
},
webpack: configForTests,
webpack: webpackConfigForKarma,
browsers: ['ChromeHeadless'],
reporters: ['spec'],
singleRun: true
Expand Down
Loading

0 comments on commit 1c9c9b3

Please sign in to comment.