Skip to content

Commit

Permalink
refactor: Move tests from tape to mocha (#296)
Browse files Browse the repository at this point in the history
Implements all of #5 except for cross-browser testing.

This commit migrates all of our testing infrastructure from tape over to
mocha. We were hitting tape's limits in terms of usability for MDC-Web,
and this gives us a clean break.

Note that most of the changes here were done using an automated script
that can be found at
https://gist.github.com/traviskaufman/59476a12b65925d06b048b61b8ef085c

Note that there are some bugs in the code but I double-checked and it
looked like everything transpiled correctly.

 ## Improvements over the old infrastructure

- _Massive_ speedup in test runtime. Our full suite executes in ~6s
  (locally), which is about 3x faster than before.
- No pollution from TAP console output, only the results from the karma
  reporter are logged to the command line.
- A way better debugging experience. You can now exclude one or more
  tests, or include one or more tests, a big step up from test.only()
  that existed before. Furthermore, the Karma Debug UI now uses mocha's
  html reporter, which produces beautiful output and allows you to drill
  down into individual tests and only run them, view the test code in
  the browser, and more.
- All of the ease and usability of mocha (returning promises for async
  tests, a rich set of configuration options, top-level exception
  handling within tests, etc.)
- A more robust assertion library using chai, including better error
  reporting when assertions fail.

 ## What Changes

 ### Spec Descriptions

Instead of

```js
// math.test.js

import test from 'tape';

test('1 + 1 equals 2', (t) => {
  t.equal(1 + 1, 2);
  t.end();
});
```

You write

```js
// math.test.js

import {assert} from 'chai';

suite('MDCMath');

test('1 + 1 equals 2', () => {
  assert.equal(1 + 1, 2);
});
```

There is one `suite()` function per file, which is a top-level
description of the tests this file contains. Suites should always start
with `MDC`, which is enforced by eslint.

 ### Testdouble verifications

Instead of writing

```js
t.doesNotThrow(() => td.verify(mockAdapter.addClass('foo')));
```

You write

```js
td.verify(mockAdapter.addClass('foo');
```

 ### tape assertion methods vs. chai's assertion methods

- `t.true` -> `assert.isOk`
- `t.false` -> `assert.isNotOk`
- everything other assertion method pretty much stays the same. Plus, we
  have even more robust methods to work with thanks to chai.

 ## Next steps

- Re-enabling SauceLabs cross-browser testing, which is the final part
  of #5.
  • Loading branch information
traviskaufman authored and cristobalchao@google.com committed Feb 24, 2017
1 parent 70bbbaa commit 8f57ff3
Show file tree
Hide file tree
Showing 37 changed files with 1,664 additions and 2,359 deletions.
35 changes: 12 additions & 23 deletions .eslintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ globals:
env:
browser: true
plugins:
- tape
- mocha
parserOptions:
ecmaVersion: 2015
sourceType: module
Expand All @@ -23,25 +23,14 @@ rules:
valid-jsdoc: off
prefer-const: error

# tape rules
# see: https://github.com/atabel/eslint-plugin-tape/blob/master/index.js
tape/assertion-message:
- off
- always
tape/max-asserts:
- off
- 5
tape/no-identical-title: error
tape/no-ignored-test-files: error
tape/no-only-test: error
tape/no-skip-assert: error
tape/no-skip-test: error
tape/no-statement-after-end: error
tape/no-unknown-modifiers: error
tape/test-ended: error
tape/test-title:
- error
- if-multiple
tape/use-t-well: error
tape/use-t: error
tape/use-test: error
# Rules for our mocha unit tests. Note that even though we use mocha, we model our unit tests
# after frameworks such as tape and ava, which encourage modern paradigms, seek to minimize
# shared state across tests, and try and make tests look as simple as possible.
mocha/handle-done-callback: error
mocha/no-exclusive-tests: error
mocha/no-hooks: error
mocha/no-identical-title: error
mocha/no-nested-tests: error
mocha/no-pending-tests: error
mocha/no-skipped-tests: error
mocha/valid-suite-description: [error, ^MDC.+]
4 changes: 4 additions & 0 deletions docs/authoring-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,10 @@ Concretely:

### Testing

The following guidelines should be used to help write tests for MDC-Web code. Our tests are written
using [mocha](https://mochajs.org/) with the [qunit UI](https://mochajs.org/#qunit), and are driven by [karma](https://karma-runner.github.io/1.0/index.html). We use the [chai assert API](http://chaijs.com/api/assert/)
for assertions, and [testdouble](https://github.com/testdouble/testdouble.js/) for mocking and stubbing.

#### Verify foundation's adapters
When testing foundations, ensure that at least one of your test cases uses the
`verifyDefaultAdapter` method defined with our [foundation helpers](../test/unit/helpers/foundation.js). This is done to ensure that adapter interfaces do not
Expand Down
17 changes: 9 additions & 8 deletions karma.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ const SL_LAUNCHERS = {
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['tap'],
frameworks: ['mocha'],
files: [
'test/unit/index.js',
],
Expand All @@ -126,11 +126,15 @@ module.exports = function(config) {
],
},

client: {
mocha: {
reporter: 'html',
ui: 'qunit',
},
},

webpack: Object.assign({}, webpackConfig, {
devtool: 'inline-source-map',
node: {
fs: 'empty',
},
module: Object.assign({}, webpackConfig.module, {
// Cover source files when not debugging tests. Otherwise, omit coverage instrumenting to get
// uncluttered source maps.
Expand Down Expand Up @@ -162,11 +166,8 @@ module.exports = function(config) {
}
};

// Block-scoped declarations are not supported in Node 4.
/* eslint-disable no-var */

function determineBrowsers() {
var browsers = USING_SL ? Object.keys(SL_LAUNCHERS) : ['Chrome'];
let browsers = USING_SL ? Object.keys(SL_LAUNCHERS) : ['Chrome'];
if (USING_TRAVISCI && !IS_SECURE) {
console.warn(
'NOTICE: Falling back to firefox browser, as travis-ci JWT addon is currently not working ' +
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"babel-plugin-transform-object-assign": "^6.8.0",
"babel-preset-es2015": "^6.9.0",
"bel": "^4.4.3",
"chai": "^3.5.0",
"cli-table": "^0.3.1",
"cp-file": "^4.1.0",
"cross-env": "^2.0.0",
Expand All @@ -39,7 +40,7 @@
"dom-events": "^0.1.1",
"eslint": "^3.6.1",
"eslint-config-google": "^0.7.1",
"eslint-plugin-tape": "^1.1.0",
"eslint-plugin-mocha": "^4.8.0",
"extract-text-webpack-plugin": "2.0.0-rc.3",
"glob": "^7.1.1",
"husky": "^0.13.1",
Expand All @@ -50,13 +51,14 @@
"karma-chrome-launcher": "^1.0.1",
"karma-coverage": "^1.1.0",
"karma-firefox-launcher": "^1.0.0",
"karma-mocha": "^1.3.0",
"karma-sauce-launcher": "^1.0.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-tap": "^2.1.4",
"karma-webpack": "^1.7.0",
"lerna": "2.0.0-beta.36",
"lolex": "^1.5.0",
"mkdirp": "^0.5.1",
"mocha": "^3.2.0",
"node-sass": "^3.7.0",
"npm-run-all": "^2.3.0",
"postcss-loader": "^1.2.2",
Expand All @@ -71,7 +73,6 @@
"stylelint-order": "^0.2.2",
"stylelint-scss": "^1.4.1",
"stylelint-selector-bem-pattern": "^1.0.0",
"tape": "^4.6.0",
"testdouble": "^1.6.0",
"to-slug-case": "^1.0.0",
"validate-commit-msg": "^2.6.1",
Expand Down
9 changes: 5 additions & 4 deletions test/unit/helpers/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,22 @@
* limitations under the License.
*/

import {assert} from 'chai';
import td from 'testdouble';

// Sanity tests to ensure the following:
// - Default adapters contain functions
// - All expected adapter functions are accounted for
// - Invoking any of the default methods does not throw an error.
// Every foundation test suite include this verification.
export function verifyDefaultAdapter(FoundationClass, expectedMethods, t) {
export function verifyDefaultAdapter(FoundationClass, expectedMethods) {
const {defaultAdapter} = FoundationClass;
const methods = Object.keys(defaultAdapter).filter((k) => typeof defaultAdapter[k] === 'function');

t.equal(methods.length, Object.keys(defaultAdapter).length, 'Every adapter key must be a function');
t.deepEqual(methods, expectedMethods);
assert.equal(methods.length, Object.keys(defaultAdapter).length, 'Every adapter key must be a function');
assert.deepEqual(methods, expectedMethods);
// Test default methods
methods.forEach((m) => t.doesNotThrow(defaultAdapter[m]));
methods.forEach((m) => assert.doesNotThrow(defaultAdapter[m]));
}

// Returns an object that intercepts calls to an adapter method used to register event handlers, and adds
Expand Down
44 changes: 18 additions & 26 deletions test/unit/mdc-animation/mdc-animation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import test from 'tape';
import {assert} from 'chai';
import td from 'testdouble';

import {getCorrectEventName} from '../../../packages/mdc-animation';
Expand All @@ -31,7 +31,9 @@ const legacyWindowObj = td.object({
},
});

test('#getCorrectEventName does not prefix events when not necessary', (t) => {
suite('MDCAnimation');

test('#getCorrectEventName does not prefix events when not necessary', () => {
const windowObj = td.object({
document: {
createElement: (str) => ({
Expand All @@ -42,85 +44,75 @@ test('#getCorrectEventName does not prefix events when not necessary', (t) => {
},
});

t.equal(
assert.equal(
getCorrectEventName(windowObj, 'animationstart'),
'animationstart',
'no prefix'
);

t.end();
});

test('#getCorrectEventName does not prefix events if window does not contain a DOM node', (t) => {
test('#getCorrectEventName does not prefix events if window does not contain a DOM node', () => {
const windowObj = td.object({});

t.equal(
assert.equal(
getCorrectEventName(windowObj, 'animationstart'),
'animationstart',
'no prefix'
);

t.end();
});

test('#getCorrectPropertyName does not prefix events if window does not contain a DOM node', (t) => {
test('#getCorrectPropertyName does not prefix events if window does not contain a DOM node', () => {
const windowObj = td.object({});

t.equal(
assert.equal(
getCorrectPropertyName(windowObj, 'transition'),
'transition',
'no prefix'
);

t.end();
});

test('#getCorrectPropertyName prefixes css properties when required', (t) => {
t.equal(
test('#getCorrectPropertyName prefixes css properties when required', () => {
assert.equal(
getCorrectPropertyName(legacyWindowObj, 'animation'),
'-webkit-animation',
'added prefix'
);

t.equal(
assert.equal(
getCorrectPropertyName(legacyWindowObj, 'transform'),
'-webkit-transform',
'added prefix'
);

t.equal(
assert.equal(
getCorrectPropertyName(legacyWindowObj, 'transition'),
'-webkit-transition',
'added prefix'
);

t.end();
});

test('#getCorrectEventName prefixes javascript events when required', (t) => {
t.equal(
test('#getCorrectEventName prefixes javascript events when required', () => {
assert.equal(
getCorrectEventName(legacyWindowObj, 'animationstart'),
'webkitAnimationStart',
'added prefix'
);

t.equal(
assert.equal(
getCorrectEventName(legacyWindowObj, 'animationend'),
'webkitAnimationEnd',
'added prefix'
);

t.equal(
assert.equal(
getCorrectEventName(legacyWindowObj, 'animationiteration'),
'webkitAnimationIteration',
'added prefix'
);

t.equal(
assert.equal(
getCorrectEventName(legacyWindowObj, 'transitionend'),
'webkitTransitionEnd',
'added prefix'
);

t.end();
});
39 changes: 17 additions & 22 deletions test/unit/mdc-auto-init/mdc-auto-init.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import bel from 'bel';
import td from 'testdouble';
import test from 'tape';
import {assert} from 'chai';
import mdcAutoInit from '../../../packages/mdc-auto-init';

class FakeComponent {
Expand All @@ -41,62 +41,57 @@ const setupTest = () => {
return createFixture();
};

test('calls attachTo() on components registered for identifier on nodes w/ data-mdc-auto-init attr', (t) => {
suite('MDCAutoInit');

test('calls attachTo() on components registered for identifier on nodes w/ data-mdc-auto-init attr', () => {
const root = setupTest();
mdcAutoInit(root);

t.true(root.querySelector('.mdc-fake').FakeComponent instanceof FakeComponent);
t.end();
assert.isOk(root.querySelector('.mdc-fake').FakeComponent instanceof FakeComponent);
});

test('passes the node where "data-mdc-auto-init" was found to attachTo()', (t) => {
test('passes the node where "data-mdc-auto-init" was found to attachTo()', () => {
const root = setupTest();
mdcAutoInit(root);

const fake = root.querySelector('.mdc-fake');
t.equal(fake.FakeComponent.node, fake);
t.end();
assert.equal(fake.FakeComponent.node, fake);
});

test('throws when no constructor name is specified within "data-mdc-auto-init"', (t) => {
test('throws when no constructor name is specified within "data-mdc-auto-init"', () => {
const root = setupTest();
root.querySelector('.mdc-fake').dataset.mdcAutoInit = '';

t.throws(() => mdcAutoInit(root));
t.end();
assert.throws(() => mdcAutoInit(root));
});

test('throws when constructor is not registered', (t) => {
test('throws when constructor is not registered', () => {
const root = setupTest();
root.querySelector('.mdc-fake').dataset.mdcAutoInit = 'MDCUnregisteredComponent';

t.throws(() => mdcAutoInit(root));
t.end();
assert.throws(() => mdcAutoInit(root));
});

test('warns when autoInit called multiple times on a node', (t) => {
test('warns when autoInit called multiple times on a node', () => {
const root = setupTest();
const warn = td.func('warn');
const {contains} = td.matchers;

mdcAutoInit(root, warn);
mdcAutoInit(root, warn);

t.doesNotThrow(() => td.verify(warn(contains('(mdc-auto-init) Component already initialized'))));
t.end();
td.verify(warn(contains('(mdc-auto-init) Component already initialized')));
});

test('#register throws when Ctor is not a function', (t) => {
t.throws(() => mdcAutoInit.register('not-a-function', 'Not a function'));
t.end();
test('#register throws when Ctor is not a function', () => {
assert.throws(() => mdcAutoInit.register('not-a-function', 'Not a function'));
});

test('#register warns when registered key is being overridden', (t) => {
test('#register warns when registered key is being overridden', () => {
const warn = td.func('warn');
const {contains} = td.matchers;

mdcAutoInit.register('FakeComponent', () => ({overridden: true}), warn);

t.doesNotThrow(() => td.verify(warn(contains('(mdc-auto-init) Overriding registration'))));
t.end();
td.verify(warn(contains('(mdc-auto-init) Overriding registration')));
});
Loading

0 comments on commit 8f57ff3

Please sign in to comment.