Reading/Web/QUnit test guidelines


Writing modules

edit

Modules should be prefixed with "MobileFrontend". QUnit supports filtering.

e.g.

QUnit.module( 'MobileFrontend Browser.js', {

Writing tests

edit

Test names should start with the method name.

var Browser = require( './Browser' );

// Testing a method
QUnit.test( 'isIos()', function ( assert ) {
    const b = new Browser();
    const isIos = b.isIoS();
    ...
} );

If no instantiation is needed use the pound sign.

var util = require( './util' );

// Testing a method
QUnit.test( '#getOptions', function ( assert ) {
    const options = util.getOptions();
    ...
} );

Testing Views

edit

There are two recommended patterns for testing Views inside MobileFrontend and getting 100% code coverage. Ideally, we would generate screenshots and look for differences, but this is currently not supported.

Pattern 1 - test constructor

edit

This test pattern ensures the View constructor is called with the right parameters. It assumes the templating code and View code is well tested.

beforeEach: function () {
		spy = sandbox.spy( icons, 'Icon' );
....

QUnit.test( '#spinner()', function ( assert ) {
	icons.spinner( {
		foo: 'will be passed down',
		additionalClassNames: 'will-be-ignored'
	} );
	assert.deepEqual( spy.getCall( 0 ).args[ 0 ], {
		foo: 'will be passed down',
		additionalClassNames: 'spinner loading',
		name: 'spinner',
		label: 'mobile-frontend-loading-message'
	}, 'Options are passed down' );
} );

This doesn't test the output of the HTML so be careful when using it.

Pattern 2 - test output

edit

You can test the HTML of a MobileFrontend View by diving into the $el property.

When doing this, you get full confidence that your View is rendering as expected. Be careful, as CSS is loaded separate to HTML in headless mode so testing whether an element is visible might not be wise.

This method is usually recommended when there is no test coverage in other forms e.g. Selenium integration tests or UI regression tests.

We recommend querying the resulting HTML rather than testing the HTML string, as particularly in larger components this could make refactoring the HTML makeup of View's difficult in future. For instance, it would be frustrating if adding an aria attribute for accessibility to a Button caused 10+ unit tests to fail!

QUnit.test( 'creates a link if passed href option', function ( assert ) {
	var
		url = 'https://www.foo.com',
		button = new Button( {
			href: url
		} );

	assert.strictEqual( button.$el[0].tagName, 'A' );
	assert.strictEqual( button.$el[0].getAttribute( 'href' ), 'https://www.foo.com' );
} );

Testing View Events

edit

Note, it might make sense in some unit tests to trigger events to ensure a View is behaving correctly. However, a Selenium browser test may work just as well. When writing any test, make sure you explain your motivation in the commit message!

Making assertions

edit

Always test the comparison itself;

Avoid assert.ok / notOk(), sinon.assert.calledOnce / calledTwice / calledThrice() (Sinon.JS); QUnit cannot print useful failure messages in these cases.
Prefer assert.strictEqual / deepEqual(), assert.strictEqual(spy.callCount, 2) (Sinon.JS); QUnit can print a useful failure diff.
Always use assert.strictEqual(); never use assert.equal(). The Wikimedia lint rules require strict === and forbid loose (==) equality comparisons. This should apply to QUnit as well.
Avoid assert.expect() when tests are synchronous; consider assert.expect() when asynchronous assertions are necessary. In synchronous tests, tallying the assertion count can add noise without providing much value. For asynchronous tests, setting expectations can help catch programmer errors.
Always add an assertion failure message.
When a test fails we know why it fails. The message "1 !== 2" is less meaningful then "1 !== 2; The API should be called one time."
Use assert.async rather than $.Deferred.resolve() when possible.
Asynchronous tests should return a Promise. Use helper functions where possible to keep code simple.
Use fake objects where possible. Do not stub the prototype. Especially with regards to mw.Api which can cause HTTP requests
Avoid doing this.sandbox.stub( Api.prototype, 'ajax' ).returns - instead pass a FakeApi as a parameter. If code doesn't support this, it should!

Under discussion

edit
  • We could use eslint to enforce some of these practices, but it may be overkill.