Test Renderer

Dojo provides a simple and type safe test renderer for shallowly asserting the expected output and behavior from a widget. The test renderer’s API has been designed to encourage unit testing best practices from the outset to ensure high confidence in your Dojo application.

Working with assertions and the test renderer is done using wrapped test nodes that are defined in the assertion structure, ensuring type safety throughout the testing life-cycle.

The expected structure of a widget is defined using an assertion and passed to the test renderer’s .expect() function which executes the assertion.

src/MyWidget.spec.tsx

  1. import { tsx } from '@dojo/framework/core/vdom';
  2. import renderer, { assertion } from '@dojo/framework/testing/renderer';
  3. import MyWidget from './MyWidget';
  4. const baseAssertion = assertion(() => (
  5. <div>
  6. <h1>Heading</h1>
  7. <h2>Sub Heading</h2>
  8. <div>Content</div>
  9. </div>
  10. ));
  11. const r = renderer(() => <MyWidget />);
  12. r.expect(baseAssertion);

Wrapped Test Nodes

In order for the test renderer and assertions to be able to identify nodes within the expected and actual node structure a special wrapping node must be used. The wrapped nodes can get used in place of the real node in the expected assertion structure, maintaining all the correct property and children typings.

To create a wrapped test node use the wrap function from @dojo/framework/testing/renderer:

src/MyWidget.spec.tsx

  1. import { wrap } from '@dojo/framework/testing/renderer';
  2. import MyWidget from './MyWidget';
  3. // Create a wrapped node for a widget
  4. const WrappedMyWidget = wrap(MyWidget);
  5. // Create a wrapped node for a vnode
  6. const WrappedDiv = wrap('div');

The test renderer uses the location of a wrapped test node in the expected tree to attempt to perform any requested actions (either r.property() or r.child()) on the actual output of the widget under test. If the wrapped test node does not match the corresponding node in the actual output tree then no action will be performed and the assertion will report a failure.

Note: Wrapped test nodes should only be used once within an assertion, if the same test node is detected more than once during an assertion an error will be thrown and the test fail.

Assertion

Assertions get used to build the expected widget output structure to use with renderer.expect(). The assertion expose a wide range of APIs that enable the expected output to vary between tests.

Given a widget that renders output differently based on property values:

src/Profile.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import * as css from './Profile.m.css';
  3. export interface ProfileProperties {
  4. username?: string;
  5. }
  6. const factory = create().properties<ProfileProperties>();
  7. const Profile = factory(function Profile({ properties }) {
  8. const { username = 'Stranger' } = properties();
  9. return <h1 classes={[css.root]}>{`Welcome ${username}!`}</h1>;
  10. });
  11. export default Profile;

Create an assertion using @dojo/framework/testing/renderer#assertion:

src/Profile.spec.tsx

  1. const { describe, it } = intern.getInterface('bdd');
  2. import { tsx } from '@dojo/framework/core/vdom';
  3. import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
  4. import Profile from '../../../src/widgets/Profile';
  5. import * as css from '../../../src/widgets/Profile.m.css';
  6. // Create a wrapped node
  7. const WrappedHeader = wrap('h1');
  8. // Create an assertion using the `WrappedHeader` in place of the `h1`
  9. const baseAssertion = assertion(() => <WrappedHeader classes={[css.root]}>Welcome Stranger!</WrappedHeader>);
  10. describe('Profile', () => {
  11. it('Should render using the default username', () => {
  12. const r = renderer(() => <Profile />);
  13. // Test against the base assertion
  14. r.expect(baseAssertion);
  15. });
  16. });

To test when a username property gets passed to the Profile widget, we could create a new assertion with the updated expected username. However, as a widget increases its functionality, recreating the entire assertion for each scenario becomes verbose and unmaintainable, as any changes to the common widget structure would require updating every assertion.

To help avoid the maintenance overhead and reduce duplication, assertions offer a comprehensive API for creating variations from a base assertion. The assertion API uses wrapped test nodes to identify the node in the expected structure to update.

src/Profile.spec.tsx

  1. const { describe, it } = intern.getInterface('bdd');
  2. import { tsx } from '@dojo/framework/core/vdom';
  3. import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
  4. import Profile from '../../../src/widgets/Profile';
  5. import * as css from '../../../src/widgets/Profile.m.css';
  6. // Create a wrapped node
  7. const WrappedHeader = wrap('h1');
  8. // Create an assertion using the `WrappedHeader` in place of the `h1`
  9. const baseAssertion = assertion(() => <WrappedHeader classes={[css.root]}>Welcome Stranger!</WrappedHeader>);
  10. describe('Profile', () => {
  11. it('Should render using the default username', () => {
  12. const r = renderer(() => <Profile />);
  13. // Test against the base assertion
  14. r.expect(baseAssertion);
  15. });
  16. it('Should render using the passed username', () => {
  17. const r = renderer(() => <Profile username="Dojo" />);
  18. // Create a variation of the base assertion
  19. const usernameAssertion = baseAssertion.setChildren(WrappedHeader, () => ['Dojo']);
  20. // Test against the username assertion
  21. r.expect(usernameAssertion);
  22. });
  23. });

Creating assertions from a base assertion means that if there is a change to the default widget output, only a change to the baseAssertion is required to update all the widget’s tests.

Assertion API

assertion.setChildren()

Returns a new assertion with the new children either pre-pended, appended or replaced depending on the type passed.

  1. .setChildren(
  2. wrapped: Wrapped,
  3. children: () => RenderResult,
  4. type: 'prepend' | 'replace' | 'append' = 'replace'
  5. ): AssertionResult;

assertion.append()

Returns a new assertion with the new children appended to the node’s existing children.

  1. .append(wrapped: Wrapped, children: () => RenderResult): AssertionResult;

assertion.prepend()

Returns a new assertion with the new children pre-pended to the node’s existing children.

  1. .prepend(wrapped: Wrapped, children: () => RenderResult): AssertionResult;

assertion.replaceChildren()

Returns a new assertion with the new children replacing the node’s existing children.

  1. .replaceChildren(wrapped: Wrapped, children: () => RenderResult): AssertionResult;

assertion.insertSiblings()

Returns a new assertion with the passed children inserted either before or after depending on the type passed.

  1. .insertSiblings(
  2. wrapped: Wrapped,
  3. children: () => RenderResult,
  4. type: 'before' | 'after' = 'before'
  5. ): AssertionResult;

assertion.insertBefore()

Returns a new assertion with the passed children inserted before the existing node’s children.

  1. .insertBefore(wrapped: Wrapped, children: () => RenderResult): AssertionResult;

assertion.insertAfter()

Returns a new assertion with the passed children inserted after the existing node’s children.

  1. .insertAfter(wrapped: Wrapped, children: () => RenderResult): AssertionResult;

assertion.replace()

Returns a new assertion replacing the existing node with the node that is passed. Note that if you need to interact with the new node in either assertions or the test renderer, it should be a wrapped test node.

  1. .replace(wrapped: Wrapped, node: DNode): AssertionResult;

assertion.remove()

Returns a new assertion removing the target wrapped node completely.

  1. .remove(wrapped: Wrapped): AssertionResult;

assertion.setProperty()

Returns a new assertion with the updated property for the target wrapped node.

  1. .setProperty<T, K extends keyof T['properties']>(
  2. wrapped: Wrapped<T>,
  3. property: K,
  4. value: T['properties'][K]
  5. ): AssertionResult;

assertion.setProperties()

Returns a new assertion with the updated properties for the target wrapped node.

  1. .setProperties<T>(
  2. wrapped: Wrapped<T>,
  3. value: T['properties'] | PropertiesComparatorFunction<T['properties']>
  4. ): AssertionResult;

A function can be set in place of the properties object to return the expected properties based off the actual properties.

Triggering Properties

In addition to asserting the output from a widget, widget behavior can be tested by using the renderer.property() function. The property() function takes a wrapped test node and the key of a property to call before the next call to expect().

src/MyWidget.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import icache from '@dojo/framework/core/middleware/icache';
  3. import { RenderResult } from '@dojo/framework/core/interfaces';
  4. import MyWidgetWithChildren from './MyWidgetWithChildren';
  5. const factory = create({ icache }).properties<{ onClick: () => void }>();
  6. export const MyWidget = factory(function MyWidget({ properties, middleware: { icache } }) {
  7. const count = icache.getOrSet('count', 0);
  8. return (
  9. <div>
  10. <h1>Header</h1>
  11. <span>{`${count}`}</span>
  12. <button
  13. onclick={() => {
  14. icache.set('count', icache.getOrSet('count', 0) + 1);
  15. properties().onClick();
  16. }}
  17. >
  18. Increase Counter!
  19. </button>
  20. </div>
  21. );
  22. });

src/MyWidget.spec.tsx

  1. const { describe, it } = intern.getInterface('bdd');
  2. import { tsx } from '@dojo/framework/core/vdom';
  3. import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
  4. import * as sinon from 'sinon';
  5. import MyWidget from './MyWidget';
  6. // Create a wrapped node for the button
  7. const WrappedButton = wrap('button');
  8. const WrappedSpan = wrap('span');
  9. const baseAssertion = assertion(() => (
  10. <div>
  11. <h1>Header</h1>
  12. <WrappedSpan>0</WrappedSpan>
  13. <WrappedButton onclick={() => {
  14. icache.set('count', icache.getOrSet('count', 0) + 1);
  15. properties().onClick();
  16. }}>Increase Counter!</button>
  17. </WrappedButton>
  18. ));
  19. describe('MyWidget', () => {
  20. it('render', () => {
  21. const onClickStub = sinon.stub();
  22. const r = renderer(() => <MyWidget onClick={onClickStub} />);
  23. // assert against the base assertion
  24. r.expect(baseAssertion);
  25. // register a call to the button's onclick property
  26. r.property(WrappedButton, 'onclick');
  27. // create a new assertion with the updated count
  28. const counterAssertion = baseAssertion.setChildren(WrappedSpan, () => ['1']);
  29. // expect against the new assertion, the property will be called before the test render
  30. r.expect(counterAssertion);
  31. // once the assertion is complete, check that the stub property was called
  32. assert.isTrue(onClickStub.calledOnce);
  33. });
  34. });

Arguments for the function can be passed after the function name, for example r.property(WrappedButton, 'onclick', { target: { value: 'value' }}). When there are multiple parameters for the function they are passed one after the other r.property(WrappedButton, 'onclick', 'first-arg', 'second-arg', 'third-arg')

Asserting Functional Children

To assert the output from functional children the test renderer needs to understand how to resolve the child render functions. This includes passing in any expected injected values.

The test renderer renderer.child() function enables children to get resolved in order to include them in the assertion. Using the .child() function requires the widget with functional children to be wrapped when included in the assertion, and the wrapped node gets passed to the .child function.

src/MyWidget.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import { RenderResult } from '@dojo/framework/core/interfaces';
  3. import MyWidgetWithChildren from './MyWidgetWithChildren';
  4. const factory = create().children<(value: string) => RenderResult>();
  5. export const MyWidget = factory(function MyWidget() {
  6. return (
  7. <div>
  8. <h1>Header</h1>
  9. <MyWidgetWithChildren>{(value) => <div>{value}</div>}</MyWidgetWithChildren>
  10. </div>
  11. );
  12. });

src/MyWidget.spec.tsx

  1. const { describe, it } = intern.getInterface('bdd');
  2. import { tsx } from '@dojo/framework/core/vdom';
  3. import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
  4. import MyWidgetWithChildren from './MyWidgetWithChildren';
  5. import MyWidget from './MyWidget';
  6. // Create a wrapped node for the widget with functional children
  7. const WrappedMyWidgetWithChildren = wrap(MyWidgetWithChildren);
  8. const baseAssertion = assertion(() => (
  9. <div>
  10. <h1>Header</h1>
  11. <WrappedMyWidgetWithChildren>{() => <div>Hello!</div>}</MyWidgetWithChildren>
  12. </div>
  13. ));
  14. describe('MyWidget', () => {
  15. it('render', () => {
  16. const r = renderer(() => <MyWidget />);
  17. // instruct the test renderer to resolve the children
  18. // with the provided params
  19. r.child(WrappedMyWidgetWithChildren, ['Hello!']);
  20. r.expect(baseAssertion);
  21. });
  22. });

Custom Property Comparators

There are circumstances where the exact value of a property is unknown during testing, so will require the use of a custom comparator. Custom comparators get used for any wrapped widget along with the @dojo/framework/testing/renderer#compare function in place of the usual widget or node property.

  1. compare(comparator: (actual) => boolean)
  1. import { assertion, wrap, compare } from '@dojo/framework/testing/renderer';
  2. // create a wrapped node the `h1`
  3. const WrappedHeader = wrap('h1');
  4. const baseAssertion = assertion(() => (
  5. <div>
  6. <WrappedHeader id={compare((actual) => typeof actual === 'string')}>Header!</WrappedHeader>
  7. </div>
  8. ));

Ignoring Nodes during Assertion

When dealing with widgets that render multiple items, for example a list it can be desirable to be able to instruct the test renderer to ignore sections of the output. For example asserting that the first and last items are valid and then ignoring the detail of the items in-between, simply asserting that they are the expected type. To do this with the test renderer the ignore function can be used that instructs the test renderer to ignore the node, as long as it is the same type, i.e. matching tag name or matching widget factory/constructor.

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import renderer, { assertion, ignore } from '@dojo/framework/testing/renderer';
  3. const factory = create().properties<{ items: string[] }>();
  4. const ListWidget = create(function ListWidget({ properties }) {
  5. const { items } = properties();
  6. return (
  7. <div>
  8. <ul>{items.map((item) => <li>{item}</li>)}</ul>
  9. </div>
  10. );
  11. });
  12. const r = renderer(() => <ListWidget items={['a', 'b', 'c', 'd']} />);
  13. const IgnoredItem = ignore('li');
  14. const listAssertion = assertion(() => (
  15. <div>
  16. <ul>
  17. <li>a</li>
  18. <IgnoredItem />
  19. <IgnoredItem />
  20. <li>d</li>
  21. </ul>
  22. </div>
  23. ));
  24. r.expect(listAssertion);

Mocking Middleware

When initializing the test renderer, mock middleware can get specified as part of the RendererOptions. The mock middleware gets defined as a tuple of the original middleware and the mock middleware implementation. Mock middleware gets created in the same way as any other middleware.

  1. import myMiddleware from './myMiddleware';
  2. import myMockMiddleware from './myMockMiddleware';
  3. import renderer from '@dojo/framework/testing/renderer';
  4. import MyWidget from './MyWidget';
  5. describe('MyWidget', () => {
  6. it('renders', () => {
  7. const r = renderer(() => <MyWidget />, { middleware: [[myMiddleware, myMockMiddleware]] });
  8. h
  9. .expect
  10. /** assertion that executes the mock middleware instead of the normal middleware **/
  11. ();
  12. });
  13. });

The test renderer automatically mocks a number of core middlewares that will get injected into any middleware that requires them:

  • invalidator
  • setProperty
  • destroy

Additionally, there are a number of mock middleware available to support widgets that use the corresponding provided Dojo middleware. See the mocking section for more information on provided mock middleware.