Testing Dojo applications (detailed)

Keywords: Front-end Attribute JSON Firefox REST

Test Services

Intern supports BrowserStack,SauceLabs and TestingBot Run the test remotely on the service. You can choose one of these services, register an account number and provide credentials to cli-test-intern. By default, all test services run tests on browsers like IE11, Firefox, and Chrome.

BrowserStack

Use BrowserStack The service requires access key and user name. Access key and user name can be specified or set as environment variables on the command line, as detailed in Intern Documents.

dojo test -a -c browserstack -k <accesskey> --userName <username>

Or use environmental variables

BROWSERSTACK_USERNAME=<username> BROWSERSTACK_ACCESS_KEY=<key> dojo test -a -c browserstack

SauceLabs

Use SauceLabs The service requires access key and user name. Access key and user name can be specified or set as environment variables on the command line, as detailed in Intern Documents.

dojo test -a -c saucelabs -k <accesskey> --userName <username>

Or use environmental variables

SAUCE_USERNAME=<username> SAUCE_ACCESS_KEY=<key> dojo test -a -c saucelabs

TestingBot

Use TestingBot The service needs to provide key and secret. Key and secret can be specified or set as environment variables on the command line, as detailed in Intern Documents.

dojo test -a -c testingbot -k <key> -s <secret>

Or use environmental variables

TESTINGBOT_SECRET=<secret> TESTINGBOT_KEY=<key> dojo test -a -c saucelabs

harness

When using @dojo/framework/testing, harness() is the most important API for setting up each test and providing a context for executing virtual DOM assertions and interactions. The goal is to mirror the core behavior of the component when updating properties or children, and when the component fails, without any special or custom logic.

API

interface HarnessOptions {
	customComparators?: CustomComparator[];
	middleware?: [MiddlewareResultFactory<any, any, any>, MiddlewareResultFactory<any, any, any>][];
}

harness(renderFunction: () => WNode, customComparators?: CustomComparator[]): Harness;
harness(renderFunction: () => WNode, options?: HarnessOptions): Harness;
  • renderFunction: A function that returns the WNode of the component under test
  • customComparators A set of custom comparator descriptors. Each descriptor provides a comparator function to compare properties located through selector and property
  • Options: Extension options for harness, including customComparators and a set of middleware/mocks tuples.

The harness function returns a Harness object that provides several API s that interact with the component under test:

Harness

  • expect Execute an assertion on the complete rendering result of the component under test
  • expectPartial Execute an assertion on the rendering result of the part under test
  • trigger Trigger function on the node of the component under test
  • getRender Returns the corresponding renderer from harness according to the index provided

Using the w() function in @dojo/framework/core to generate a component for testing is very simple:

const { describe, it } = intern.getInterface('bdd');
import WidgetBase from '@dojo/framework/widget-core/WidgetBase';
import harness from '@dojo/framework/testing/harness';
import { w, v } from '@dojo/framework/widget-core/d';

class MyWidget extends WidgetBase<{ foo: string }> {
	protected render() {
		const { foo } = this.properties;
		return v('div', { foo }, this.children);
	}
}

const h = harness(() => w(MyWidget, { foo: 'bar' }, ['child']));

As shown below, the harness function also supports tsx. The rest of the examples in the README document use the programmable w() API, in which unit testing You can see more examples of tsx.

const h = harness(() => <MyWidget foo="bar">child</MyWidget>);

renderFunction is delayed, so additional logic can be included between assertions to manipulate the properties and children of components.

describe('MyWidget', () => {
  it('renders with foo correctly', () => {
		let foo = 'bar';

		const h = harness(() => {
			return w(MyWidget, { foo }, [ 'child' ]));
		};

		h.expect(/** assertion that includes bar **/);
		// update the property that is passed to the widget
		foo = 'foo';
		h.expect(/** assertion that includes foo **/)
  });
});

Mocking Middleware

When harness is initialized, mock middleware can be specified as part of the HarnessOptions value. Mock middleware is defined as a tuple consisting of the original middleware and the implementation of mock middleware. Mock middleware is created in the same way as other middleware.

import myMiddleware from './myMiddleware';
import myMockMiddleware from './myMockMiddleware';
import harness from '@dojo/framework/testing/harness';

import MyWidget from './MyWidget';

describe('MyWidget', () => {
	it('renders', () => {
		const h = harness(() => <MyWidget />, { middleware: [[myMiddleware, myMockMiddleware]] });
		h.expect(/** assertion that executes the mock middleware instead of the normal middleware **/);
	});
});

Harness automatically mock s many core middleware and injects it into any middleware that needs them:

  • invalidator
  • setProperty
  • destroy

Dojo Mock Middleware

When testing components using Dojo middleware, many mock middleware can be used. Mock exports a factory that creates a restricted scope mock middleware that will be used in each test.

Mock node Middleware

createNodeMock in @dojo/framework/testing/mocks/middleware/node can mock a node middleware. To set the expected value returned from the node mock, you need to call the created mock node middleware and pass in the key and the expected DOM node.

import createNodeMock from '@dojo/framework/testing/mocks/middleware/node';

// create the mock node middleware
const mockNode = createNodeMock();

// create a mock DOM node
const domNode = {};

// call the mock middleware with a key and the DOM
// to return.
mockNode('key', domNode);
Mock intersection Middleware

Create Intersection Mock in @dojo/framework/testing/mocks/middleware/intersection can mock an intersection middleware. To set the expected value returned from the intersection mock, you need to call the created mock intersection middleware and pass in the key and the expected intersection details.

Consider the following components:

import { create, tsx } from '@dojo/framework/core/vdom';
import intersection from '@dojo/framework/core/middleware/intersection';

const factory = create({ intersection });

const App = factory(({ middleware: { intersection } }) => {
	const details = intersection.get('root');
	return <div key="root">{JSON.stringify(details)}</div>;
});

Using mock intersection middleware:

import { tsx } from '@dojo/framework/core/vdom';
import createIntersectionMock from '@dojo/framework/testing/mocks/middleware/intersection';
import intersection from '@dojo/framework/core/middleware/intersection';
import harness from '@dojo/framework/testing/harness';

import MyWidget from './MyWidget';

describe('MyWidget', () => {
	it('test', () => {
		// create the intersection mock
		const intersectionMock = createIntersectionMock();
		// pass the intersection mock to the harness so it knows to
		// replace the original middleware
		const h = harness(() => <App key="app" />, { middleware: [[intersection, intersectionMock]] });

		// call harness.expect as usual, asserting the default response
		h.expect(() => <div key="root">{`{"intersectionRatio":0,"isIntersecting":false}`}</div>);

		// use the intersection mock to set the expected return
		// of the intersection middleware by key
		intersectionMock('root', { isIntersecting: true });

		// assert again with the updated expectation
		h.expect(() => <div key="root">{`{"isIntersecting": true }`}</div>);
	});
});
Mock resize Middleware

createResizeMock in @dojo/framework/testing/mocks/middleware/resize can mock a resize middleware. To set the expected value returned from resize mock, you need to call the created mock resize middleware and pass in the key and the desired rectangular area to hold the content.

const mockResize = createResizeMock();
mockResize('key', { width: 100 });

Consider the following components:

import { create, tsx } from '@dojo/framework/core/vdom'
import resize from '@dojo/framework/core/middleware/resize'

const factory = create({ resize });

export const MyWidget = factory(function MyWidget({ middleware }) => {
	const  { resize } = middleware;
	const contentRects = resize.get('root');
	return <div key="root">{JSON.stringify(contentRects)}</div>;
});

Using mock resize middleware:

import { tsx } from '@dojo/framework/core/vdom';
import createResizeMock from '@dojo/framework/testing/mocks/middleware/resize';
import resize from '@dojo/framework/core/middleware/resize';
import harness from '@dojo/framework/testing/harness';

import MyWidget from './MyWidget';

describe('MyWidget', () => {
	it('test', () => {
		// create the resize mock
		const resizeMock = createResizeMock();
		// pass the resize mock to the harness so it knows to replace the original
		// middleware
		const h = harness(() => <App key="app" />, { middleware: [[resize, resizeMock]] });

		// call harness.expect as usual
		h.expect(() => <div key="root">null</div>);

		// use the resize mock to set the expected return of the resize middleware
		// by key
		resizeMock('root', { width: 100 });

		// assert again with the updated expectation
		h.expect(() => <div key="root">{`{"width":100}`}</div>);
	});
});
Mock Store Middleware

createMockStoreMiddleware in @dojo/framework/testing/mocks/middleware/store can mock a strong type of store middleware and also supports mock process. In order to mock a store's process, a tuple composed of the original store process and stub process can be passed in. The middleware will call stub instead of the original process. If no stub is passed in, the middleware stops calling all processes.

To modify the value in the mock store, you need to call the mockStore and pass in a function that returns a set of store operations. This injects the path function of the store to create a pointer to the state that needs to be modified.

mockStore((path) => [replace(path('details', { id: 'id' })]);

Consider the following components:

src/MyWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom'
import { myProcess } from './processes';
import MyState from './interfaces';
// application store middleware typed with the state interface
// Example: `const store = createStoreMiddleware<MyState>();`
import store from './store';

const factory = create({ store }).properties<{ id: string }>();

export default factory(function MyWidget({ properties, middleware: store }) {
	const { id } = properties();
    const { path, get, executor } = store;
    const details = get(path('details');
    let isLoading = get(path('isLoading'));

    if ((!details || details.id !== id) && !isLoading) {
        executor(myProcess)({ id });
        isLoading = true;
    }

    if (isLoading) {
        return <Loading />;
    }

    return <ShowDetails {...details} />;
});

Using mock store middleware:

tests/unit/MyWidget.tsx

import { tsx } from '@dojo/framework/core/vdom'
import createMockStoreMiddleware from '@dojo/framework/testing/mocks/middleware/store';
import harness from '@dojo/framework/testing/harness';

import { myProcess } from './processes';
import MyWidget from './MyWidget';
import MyState from './interfaces';
import store from './store';

// import a stub/mock lib, doesn't have to be sinon
import { stub } from 'sinon';

describe('MyWidget', () => {
     it('test', () => {
          const properties = {
               id: 'id'
          };
         const myProcessStub = stub();
         // type safe mock store middleware
         // pass through an array of tuples `[originalProcess, stub]` for mocked processes
         // calls to processes not stubbed/mocked get ignored
         const mockStore = createMockStoreMiddleware<MyState>([[myProcess, myProcessStub]]);
         const h = harness(() => <MyWidget {...properties} />, {
             middleware: [store, mockStore]
         });
         h.expect(/* assertion template for `Loading`*/);

         // assert again the stubbed process
         expect(myProcessStub.calledWith({ id: 'id' })).toBeTruthy();

         mockStore((path) => [replace(path('isLoading', true)]);
         h.expect(/* assertion template for `Loading`*/);
         expect(myProcessStub.calledOnce()).toBeTruthy();

         // use the mock store to apply operations to the store
         mockStore((path) => [replace(path('details', { id: 'id' })]);
         mockStore((path) => [replace(path('isLoading', true)]);

         h.expect(/* assertion template for `ShowDetails`*/);

         properties.id = 'other';
         h.expect(/* assertion template for `Loading`*/);
         expect(myProcessStub.calledTwice()).toBeTruthy();
         expect(myProcessStub.secondCall.calledWith({ id: 'other' })).toBeTruthy();
         mockStore((path) => [replace(path('details', { id: 'other' })]);
         h.expect(/* assertion template for `ShowDetails`*/);
     });
});

Custom Comparators

In some cases, we can't know the exact value of the attribute during the test, so we need to use custom comparison descriptor.

There is one in the descriptor to locate the virtual node to be checked. selector A property name that applies a custom comparison and a comparator function that receives the actual value and returns the result of a boolean type assertion.

const compareId = {
	selector: '*', // all nodes
	property: 'id',
	comparator: (value: any) => typeof value === 'string' // checks the property value is a string
};

const h = harness(() => w(MyWidget, {}), [compareId]);

For all assertions, the returned harness API will test only the id attribute using comparator, not the standard equivalent test.

selectors

The harness API supports the CSS style Selector concept to locate nodes in the virtual DOM to assert and operate on. See A complete list of supported selectors For more information.

In addition to the standard API, it also provides:

  • Support for abbreviating key attribute of location node as @ symbol
  • When using standard. to locate style classes, use class attributes instead of class attributes

harness.expect

The most common requirement in testing is the output structure of the render function of the assertion component. Expct receives a function that returns the desired rendering result of the component under test as a parameter.

API

expect(expectedRenderFunction: () => DNode | DNode[], actualRenderFunction?: () => DNode | DNode[]);
  • expectedRenderFunction: Function that returns the desired DNode structure of the query node
  • actualRenderFunction: An optional function that returns the actual DNode structure asserted
h.expect(() =>
	v('div', { key: 'foo' }, [w(Widget, { key: 'child-widget' }), 'text node', v('span', { classes: ['class'] })])
);

Expct also receives the second optional parameter, returning the function of the rendering result to be asserted.

h.expect(() => v('div', { key: 'foo' }), () => v('div', { key: 'foo' }));

If the actual rendering output is different from the expected rendering output, an exception is thrown and all the differences are pointed out with (A) (actual value) and (E) (expected value) using a structured visual method.

Example assertion failure output:

v('div', {
	'classes': [
		'root',
(A)		'other'
(E)		'another'
	],
	'onclick': 'function'
}, [
	v('span', {
		'classes': 'span',
		'id': 'random-id',
		'key': 'label',
		'onclick': 'function',
		'style': 'width: 100px'
	}, [
		'hello 0'
	])
	w(ChildWidget, {
		'id': 'random-id',
		'key': 'widget'
	})
	w('registry-item', {
		'id': true,
		'key': 'registry'
	})
])

harness.trigger

harness.trigger() calls the function specified by name on the node located by the selector.

interface FunctionalSelector {
	(node: VNode | WNode): undefined | Function;
}

trigger(selector: string, functionSelector: string | FunctionalSelector, ...args: any[]): any;
  • Selector: A selector for finding the target node
  • functionSelector: A function selector that either finds the name of the called function from the properties of the node or returns a function from the properties of the node
  • args: The parameters passed in for the localized function

If a result is returned, the result of the triggered function is returned.

Examples of usage:

// calls the `onclick` function on the first node with a key of `foo`
h.trigger('@foo', 'onclick');
// calls the `customFunction` function on the first node with a key of `bar` with an argument of `100`
// and receives the result of the triggered function
const result = h.trigger('@bar', 'customFunction', 100);

Functional Selector returns functions in component properties. Functions can also be triggered in the same way as normal string function selectors.

Examples of usage:

Assume the following VDOM tree structure:

v(Toolbar, {
	key: 'toolbar',
	buttons: [
		{
			icon: 'save',
			onClick: () => this._onSave()
		},
		{
			icon: 'cancel',
			onClick: () => this._onCancel()
		}
	]
});

And you want to trigger the onClick function of the save button.

h.trigger('@buttons', (renderResult: DNode<Toolbar>) => {
	return renderResult.properties.buttons[0].onClick;
});

Note: If the specified selector is not found, trigger throws an error.

harness.getRender

harness.getRender() returns the renderer specified by the index, and the last renderer if no index is provided.

getRender(index?: number);
  • index: index of the renderer to be returned

Examples of usage:

// Returns the result of the last render
const render = h.getRender();
// Returns the result of the render for the index provided
h.getRender(1);

Assertion Templates

assertion template allows you to build the desired rendering function for passing in h.expect(). The idea behind assertion templates comes from often asserting the entire rendering output and requiring modifications to some parts of the assertion.

To use assertion templates, you need to import modules:

import assertionTemplate from '@dojo/framework/testing/assertionTemplate';

Then, in your test, you can write a basic assertion that it is the default rendering state of the component:

Assume the following components:

src/widgets/Profile.ts

import WidgetBase from '@dojo/framework/widget-core/WidgetBase';
import { v } from '@dojo/framework/widget-core/d';

import * as css from './styles/Profile.m.css';

export interface ProfileProperties {
	username?: string;
}

export default class Profile extends WidgetBase<ProfileProperties> {
	protected render() {
		const { username } = this.properties;
		return v('h1', { classes: [css.root] }, [`Welcome ${username || 'Stranger'}!`]);
	}
}

The basic assertions are as follows:

tests/unit/widgets/Profile.ts

const { describe, it } = intern.getInterface('bdd');
import harness from '@dojo/framework/testing/harness';
import assertionTemplate from '@dojo/framework/testing/assertionTemplate';
import { w, v } from '@dojo/framework/widget-core/d';

import Profile from '../../../src/widgets/Profile';
import * as css from '../../../src/widgets/styles/Profile.m.css';

const profileAssertion = assertionTemplate(() =>
	v('h1', { classes: [css.root], '~key': 'welcome' }, ['Welcome Stranger!'])
);

Write this in the test:

tests/unit/widgets/Profile.ts

const profileAssertion = assertionTemplate(() =>
	v('h1', { classes: [css.root], '~key': 'welcome' }, ['Welcome Stranger!'])
);

describe('Profile', () => {
	it('default renders correctly', () => {
		const h = harness(() => w(Profile, {}));
		h.expect(profileAssertion);
	});
});
it('default renders correctly', () => {
	const h = harness(() => w(Profile, {}));
	h.expect(profileAssertion);
});

Now let's see how to test the output after passing in the username attribute for the Profile widget:

tests/unit/widgets/Profile.ts

describe('Profile', () => {
	...

  it('renders given username correctly', () => {
    // update the expected result with a given username
    const namedAssertion = profileAssertion.setChildren('~welcome', [
      'Welcome Kel Varnsen!'
    ]);
    const h = harness(() => w(Profile, { username: 'Kel Varnsen' }));
    h.expect(namedAssertion);
  });
});

Here, we use the setChildren() api of the base Assertion, and then we use a special ~selector to locate nodes whose key value is ~message. ~ The key attribute (assertion-key in the template using tsx) is a special attribute of the assertion template, which is deleted when assertion is made, so it will not be displayed when matching the rendering structure. This feature allows you to modify the assertion template so that you can simply select nodes without extending the actual widget rendering function. Once we get the message node, we can set its child node to the expected number 5, and then use the generated template in h.expect. It should be noted that the assertion template always returns a new assertion template when setting values, which ensures that you do not accidentally modify the existing template (which may lead to other test failures), and allows you to build new templates incrementally step by step based on the new template.

The assertion template has the following API s:

insertBefore(selector: string, children: () => DNode[]): AssertionTemplateResult;
insertAfter(selector: string, children: () => DNode[]): AssertionTemplateResult;
insertSiblings(selector: string, children: () => DNode[], type?: 'before' | 'after'): AssertionTemplateResult;
append(selector: string, children: () => DNode[]): AssertionTemplateResult;
prepend(selector: string, children: () => DNode[]): AssertionTemplateResult;
replaceChildren(selector: string, children: () => DNode[]): AssertionTemplateResult;
setChildren(selector: string, children: () => DNode[], type?: 'prepend' | 'replace' | 'append'): AssertionTemplateResult;
setProperty(selector: string, property: string, value: any): AssertionTemplateResult;
setProperties(selector: string, value: any | PropertiesComparatorFunction): AssertionTemplateResult;
getChildren(selector: string): DNode[];
getProperty(selector: string, property: string): any;
getProperties(selector: string): any;
replace(selector: string, node: DNode): AssertionTemplateResult;
remove(selector: string): AssertionTemplateResult;

Mocking

As you may have noticed, when testing components, we mainly test whether the user interface renders correctly after various modifications to attributes. They don't contain real business logic, but you might want to test whether attribute methods are invoked after, for example, clicking a button. This test does not care what the method actually does, but only whether the interface is invoked correctly. In this case, you can use something similar Sinon Library.

src/widgets/Action.ts

import WidgetBase from '@dojo/framework/widget-core/WidgetBase';
import { v, w } from '@dojo/framework/widget-core/d';
import Button from '@dojo/widgets/button';

import * as css from './styles/Action.m.css';

export default class Action extends WidgetBase<{ fetchItems: () => void }> {
	protected render() {
		return v('div', { classes: [css.root] }, [w(Button, { onClick: this.handleClick, key: 'button' }, ['Fetch'])]);
	}
	private handleClick() {
		this.properties.fetchItems();
	}
}

You might want to test whether the. properties. fetchItems method is invoked when the button is clicked.

tests/unit/widgets/Action.ts

const { describe, it } = intern.getInterface('bdd');
import harness from '@dojo/framework/testing/harness';
import { w, v } from '@dojo/framework/widget-core/d';

import { stub } from 'sinon';

describe('Action', () => {
	const fetchItems = stub();
	it('can fetch data on button click', () => {
		const h = harness(() => w(Home, { fetchItems }));
		h.expect(() => v('div', { classes: [css.root] }, [w(Button, { onClick: () => {}, key: 'button' }, ['Fetch'])]));
		h.trigger('@button', 'onClick');
		assert.isTrue(fetchItems.calledOnce);
	});
});

In this case, you can mock the fetchItems method of an Action component, which will try to get data items. Then you can use @button to locate the button, trigger the onClick event of the button, and verify whether the fetchItems method has been called once.

To learn more about mocking, read Sinon File.

functional testing

Unlike unit testing, which loads and executes code, functional testing loads a page in a browser and tests the interaction of the application.

If you want to verify the page content corresponding to a route, you can simplify the test by updating the link.

src/widgets/Menu.ts

import WidgetBase from '@dojo/framework/widget-core/WidgetBase';
import { w } from '@dojo/framework/widget-core/d';
import Link from '@dojo/framework/routing/ActiveLink';
import Toolbar from '@dojo/widgets/toolbar';

import * as css from './styles/Menu.m.css';

export default class Menu extends WidgetBase {
	protected render() {
		return w(Toolbar, { heading: 'My Dojo App!', collapseWidth: 600 }, [
			w(
				Link,
				{
					id: 'home', // add id attribute
					to: 'home',
					classes: [css.link],
					activeClasses: [css.selected]
				},
				['Home']
			),
			w(
				Link,
				{
					id: 'about', // add id attribute
					to: 'about',
					classes: [css.link],
					activeClasses: [css.selected]
				},
				['About']
			),
			w(
				Link,
				{
					id: 'profile', // add id attribute
					to: 'profile',
					classes: [css.link],
					activeClasses: [css.selected]
				},
				['Profile']
			)
		]);
	}
}

When using an application, you need to click the profile link and navigate to the Welcome User page. You can write a functional test to verify this behavior.

tests/functional/main.ts

const { describe, it } = intern.getInterface('bdd');
const { assert } = intern.getPlugin('chai');

describe('routing', () => {
	it('profile page correctly loads', ({ remote }) => {
		return (
			remote
				// loads the HTML file in local node server
				.get('../../output/dev/index.html')
				// find the id of the anchor tag
				.findById('profile')
				// click on the link
				.click()
				// end this action
				.end()
				// find the h1 tag
				.findByTagName('h1')
				// get the text in the h1 tag
				.getVisibleText()
				.then((text) => {
					// verify the content of the h1 tag on the profile page
					assert.equal(text, 'Welcome Dojo User!');
				})
		);
	});
});

When running functional tests, Dojo provides a remote object that interacts with the page. Because loading pages and interacting with pages are asynchronous operations, the remote object must be returned in the test.

Functional testing can be performed on the command line.

command line

npm run test:functional

This loads the HTML page into the remote instance of Chrome on your computer to test the interaction.

Functional testing is very useful to ensure that your program code works as expected in the browser.

You can read more about it. Intern Functional Testing Content.

Posted by kwdelre on Wed, 24 Jul 2019 21:06:28 -0700