Mocking

A common type of test is validating a widget’s user interface renders as expected without necessarily being concerned with the widget’s underlying business logic. These tests may want to assert scenarios such as button clicks calling widget property methods, without concern as to what the property method implementations are, only that the interface is called as expected. A mocking library such as Sinon can be used to help in these cases.

src/widgets/Action.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import Button from '@dojo/widgets/button';
  3. import * as css from './Action.m.css';
  4. const factory = create().properties<{ fetchItems: () => void }>();
  5. const Action = factory(function Action({ properties }) {
  6. return (
  7. <div classes={[css.root]}>
  8. <Button key="button" onClick={() => properties().fetchItems()}>
  9. Fetch
  10. </Button>
  11. </div>
  12. );
  13. });
  14. export default Action;

To test that the properties().fetchItems method is called when the button is clicked:

tests/unit/widgets/Action.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 Action from '../../../src/widgets/Action';
  5. import * as css from '../../../src/widgets/Action.m.css';
  6. import Button from '@dojo/widgets/button';
  7. import { stub } from 'sinon';
  8. import { assert } from 'chai';
  9. describe('Action', () => {
  10. const fetchItems = stub();
  11. it('can fetch data on button click', () => {
  12. const WrappedButton = wrap(Button);
  13. const baseAssertion = assertion(() => (
  14. <div classes={[css.root]}>
  15. <WrappedButton key="button" onClick={() => {}}>
  16. Fetch
  17. </WrappedButton>
  18. </div>
  19. ));
  20. const r = renderer(() => <Action fetchItems={fetchItems} />);
  21. r.expect(baseAssertion);
  22. r.property(WrappedButton, 'onClick');
  23. r.expect(baseAssertion);
  24. assert.isTrue(fetchItems.calledOnce);
  25. });
  26. });

In this case, a mock of the fetchItems method is provided to the Action widget that requires items to be fetched. The @button key is then targeted to trigger the button’s onClick, after which an assertion is validated that the fetchItems mock was called only once.

See the Sinon documentation for more details on mocking.

Provided middleware mocks

There are a number of mock middleware available to support testing widgets that use the corresponding Dojo middleware. The mocks export a factory used to create the scoped mock middleware to be used in each test.

Mock breakpoint middleware

Using createBreakpointMock from @dojo/framework/testing/mocks/middlware/breakpoint offers tests manual control over resizing events to trigger breakpoint tests.

Consider the following widget which displays an additional h2 when the LG breakpoint is activated:

src/Breakpoint.tsx

  1. import { tsx, create } from '@dojo/framework/core/vdom';
  2. import breakpoint from '@dojo/framework/core/middleware/breakpoint';
  3. const factory = create({ breakpoint });
  4. export default factory(function Breakpoint({ middleware: { breakpoint } }) {
  5. const bp = breakpoint.get('root');
  6. const isLarge = bp && bp.breakpoint === 'LG';
  7. return (
  8. <div key="root">
  9. <h1>Header</h1>
  10. {isLarge && <h2>Subtitle</h2>}
  11. <div>Longer description</div>
  12. </div>
  13. );
  14. });

By using the mockBreakpoint(key: string, contentRect: Partial<DOMRectReadOnly>) method on the breakpoint middleware mock, the test can explicitly trigger a given resize:

tests/unit/Breakpoint.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 breakpoint from '@dojo/framework/core/middleware/breakpoint';
  5. import createBreakpointMock from '@dojo/framework/testing/mocks/middleware/breakpoint';
  6. import Breakpoint from '../../src/Breakpoint';
  7. describe('Breakpoint', () => {
  8. it('resizes correctly', () => {
  9. const WrappedHeader = wrap('h1');
  10. const mockBreakpoint = createBreakpointMock();
  11. const baseAssertion = assertion(() => (
  12. <div key="root">
  13. <WrappedHeader>Header</WrappedHeader>
  14. <div>Longer description</div>
  15. </div>
  16. ));
  17. const r = renderer(() => <Breakpoint />, {
  18. middleware: [[breakpoint, mockBreakpoint]]
  19. });
  20. r.expect(baseAssertion);
  21. mockBreakpoint('root', { breakpoint: 'LG', contentRect: { width: 800 } });
  22. r.expect(baseAssertion.insertAfter(WrappedHeader, () => [<h2>Subtitle</h2>]);
  23. });
  24. });

Mock focus middleware

Using createFocusMock from @dojo/framework/testing/middleware/focus provides tests with manual control over when the focus middleware reports that a node with a specified key gets focused.

Consider the following widget:

src/FormWidget.tsx

  1. import { tsx, create } from '@dojo/framework/core/vdom';
  2. import focus, { FocusProperties } from '@dojo/framework/core/middleware/focus';
  3. import * as css from './FormWidget.m.css';
  4. export interface FormWidgetProperties extends FocusProperties {}
  5. const factory = create({ focus }).properties<FormWidgetProperties>();
  6. export const FormWidget = factory(function FormWidget({ middleware: { focus } }) {
  7. return (
  8. <div key="wrapper" classes={[css.root, focus.isFocused('text') ? css.focused : null]}>
  9. <input type="text" key="text" value="focus me" />
  10. </div>
  11. );
  12. });

By calling focusMock(key: string | number, value: boolean) the result of the focus middleware’s isFocused method can get controlled during a test.

tests/unit/FormWidget.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 focus from '@dojo/framework/core/middleware/focus';
  5. import createFocusMock from '@dojo/framework/testing/mocks/middleware/focus';
  6. import * as css from './FormWidget.m.css';
  7. describe('Focus', () => {
  8. it('adds a "focused" class to the wrapper when the input is focused', () => {
  9. const focusMock = createFocusMock();
  10. const WrappedRoot = wrap('div');
  11. const baseAssertion = assertion(() => (
  12. <WrappedRoot key="wrapper" classes={[css.root, null]}>
  13. <input type="text" key="text" value="focus me" />
  14. </WrappedRoot>
  15. ));
  16. const r = renderer(() => <FormWidget />, {
  17. middleware: [[focus, focusMock]]
  18. });
  19. r.expect(baseAssertion);
  20. focusMock('text', true);
  21. r.expect(baseAssertion.setProperty(WrappedRoot, 'classes', [css.root, css.focused]));
  22. });
  23. });

Mock icache middleware

Using createICacheMiddleware from @dojo/framework/testing/mocks/middleware/icache allows tests to access cache items directly while the mock provides a sufficient icache experience for the widget under test. This is particularly useful when icache is used to asynchronously retrieve data. Direct cache access enables the test to await the same promise as the widget.

Consider the following widget which retrieves data from an API:

src/MyWidget.tsx

  1. import { tsx, create } from '@dojo/framework/core/vdom';
  2. import { icache } from '@dojo/framework/core/middleware/icache';
  3. import fetch from '@dojo/framework/shim/fetch';
  4. const factory = create({ icache });
  5. export default factory(function MyWidget({ middleware: { icache } }) {
  6. const value = icache.getOrSet('users', async () => {
  7. const response = await fetch('url');
  8. return await response.json();
  9. });
  10. return value ? <div>{value}</div> : <div>Loading</div>;
  11. });

Testing the asynchronous result using the mock icache middleware is simple:

tests/unit/MyWidget.tsx

  1. const { describe, it, afterEach } = intern.getInterface('bdd');
  2. import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
  3. import { tsx } from '@dojo/framework/core/vdom';
  4. import * as sinon from 'sinon';
  5. import global from '@dojo/framework/shim/global';
  6. import icache from '@dojo/framework/core/middleware/icache';
  7. import createICacheMock from '@dojo/framework/testing/mocks/middleware/icache';
  8. import MyWidget from '../../src/MyWidget';
  9. describe('MyWidget', () => {
  10. afterEach(() => {
  11. sinon.restore();
  12. });
  13. it('test', async () => {
  14. // stub the fetch call to return a known value
  15. global.fetch = sinon.stub().returns(Promise.resolve({ json: () => Promise.resolve('api data') }));
  16. const WrappedRoot = wrap('div');
  17. const baseAssertion = assertion(() => <WrappedRoot>Loading</WrappedRoot>);
  18. const mockICache = createICacheMock();
  19. const r = renderer(() => <Home />, { middleware: [[icache, mockICache]] });
  20. r.expect(baseAssertion);
  21. // await the async method passed to the mock cache
  22. await mockICache('users');
  23. r.expect(baseAssertion.setChildren(WrappedRoot, () => ['api data']));
  24. });
  25. });

Mock intersection middleware

Using createIntersectionMock from @dojo/framework/testing/mocks/middleware/intersection creates a mock intersection middleware. To set the expected return from the intersection mock, call the created mock intersection middleware with a key and expected intersection details.

Consider the following widget:

  1. import { create, tsx } from '@dojo/framework/core/vdom';
  2. import intersection from '@dojo/framework/core/middleware/intersection';
  3. const factory = create({ intersection });
  4. const App = factory(({ middleware: { intersection } }) => {
  5. const details = intersection.get('root');
  6. return <div key="root">{JSON.stringify(details)}</div>;
  7. });

Using the mock intersection middleware:

  1. import { tsx } from '@dojo/framework/core/vdom';
  2. import createIntersectionMock from '@dojo/framework/testing/mocks/middleware/intersection';
  3. import intersection from '@dojo/framework/core/middleware/intersection';
  4. import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
  5. import MyWidget from './MyWidget';
  6. describe('MyWidget', () => {
  7. it('test', () => {
  8. // create the intersection mock
  9. const intersectionMock = createIntersectionMock();
  10. // pass the intersection mock to the renderer so it knows to
  11. // replace the original middleware
  12. const r = renderer(() => <App key="app" />, { middleware: [[intersection, intersectionMock]] });
  13. const WrappedRoot = wrap('div');
  14. const assertion = assertion(() => (
  15. <WrappedRoot key="root">{`{"intersectionRatio":0,"isIntersecting":false}`}</WrappedRoot>
  16. ));
  17. // call renderer.expect as usual, asserting the default response
  18. r.expect(assertion);
  19. // use the intersection mock to set the expected return
  20. // of the intersection middleware by key
  21. intersectionMock('root', { isIntersecting: true });
  22. // assert again with the updated expectation
  23. r.expect(assertion.setChildren(WrappedRoot, () => [`{"isIntersecting": true }`]));
  24. });
  25. });

Mock node middleware

Using createNodeMock from @dojo/framework/testing/mocks/middleware/node creates a mock for the node middleware. To set the expected return from the node mock, call the created mock node middleware with a key and expected DOM node.

  1. import createNodeMock from '@dojo/framework/testing/mocks/middleware/node';
  2. // create the mock node middleware
  3. const mockNode = createNodeMock();
  4. // create a mock DOM node
  5. const domNode = {};
  6. // call the mock middleware with a key and the DOM
  7. // to return.
  8. mockNode('key', domNode);

Mock resize middleware

Using createResizeMock from @dojo/framework/testing/mocks/middleware/resize creates a mock resize middleware. To set the expected return from the resize mock, call the created mock resize middleware with a key and expected content rects.

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

Consider the following widget:

  1. import { create, tsx } from '@dojo/framework/core/vdom'
  2. import resize from '@dojo/framework/core/middleware/resize'
  3. const factory = create({ resize });
  4. export const MyWidget = factory(function MyWidget({ middleware }) => {
  5. const { resize } = middleware;
  6. const contentRects = resize.get('root');
  7. return <div key="root">{JSON.stringify(contentRects)}</div>;
  8. });

Using the mock resize middleware:

  1. import { tsx } from '@dojo/framework/core/vdom';
  2. import createResizeMock from '@dojo/framework/testing/mocks/middleware/resize';
  3. import resize from '@dojo/framework/core/middleware/resize';
  4. import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
  5. import MyWidget from './MyWidget';
  6. describe('MyWidget', () => {
  7. it('test', () => {
  8. // create the resize mock
  9. const resizeMock = createResizeMock();
  10. // pass the resize mock to the test renderer so it knows to replace the original
  11. // middleware
  12. const r = renderer(() => <App key="app" />, { middleware: [[resize, resizeMock]] });
  13. const WrappedRoot = wrap('div');
  14. const baseAssertion = assertion(() => <div key="root">null</div>);
  15. // call renderer.expect as usual
  16. r.expect(baseAssertion);
  17. // use the resize mock to set the expected return of the resize middleware
  18. // by key
  19. resizeMock('root', { width: 100 });
  20. // assert again with the updated expectation
  21. r.expect(baseAssertion.setChildren(WrappedRoot, () [`{"width":100}`]);)
  22. });
  23. });

Mock store middleware

Using createMockStoreMiddleware from @dojo/framework/testing/mocks/middleware/store creates a typed mock store middleware, which optionally supports mocking processes. To mock a store process pass a tuple of the original store process and the stub process. The middleware will swap out the call to the original process for the passed stub. If no stubs are passed, the middleware will simply no-op all process calls.

To make changes to the mock store, call the mockStore with a function that returns an array of store operations. This is injected with the stores path function to create the pointer to the state that needs changing.

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

Consider the following widget:

src/MyWidget.tsx

  1. import { create, tsx } from '@dojo/framework/core/vdom'
  2. import { myProcess } from './processes';
  3. import MyState from './interfaces';
  4. // application store middleware typed with the state interface
  5. // Example: `const store = createStoreMiddleware<MyState>();`
  6. import store from './store';
  7. const factory = create({ store }).properties<{ id: string }>();
  8. export default factory(function MyWidget({ properties, middleware: store }) {
  9. const { id } = properties();
  10. const { path, get, executor } = store;
  11. const details = get(path('details');
  12. let isLoading = get(path('isLoading'));
  13. if ((!details || details.id !== id) && !isLoading) {
  14. executor(myProcess)({ id });
  15. isLoading = true;
  16. }
  17. if (isLoading) {
  18. return <Loading />;
  19. }
  20. return <ShowDetails {...details} />;
  21. });

Using the mock store middleware:

tests/unit/MyWidget.tsx

  1. import { tsx } from '@dojo/framework/core/vdom'
  2. import createMockStoreMiddleware from '@dojo/framework/testing/mocks/middleware/store';
  3. import renderer from '@dojo/framework/testing/renderer';
  4. import { myProcess } from './processes';
  5. import MyWidget from './MyWidget';
  6. import MyState from './interfaces';
  7. import store from './store';
  8. // import a stub/mock lib, doesn't have to be sinon
  9. import { stub } from 'sinon';
  10. describe('MyWidget', () => {
  11. it('test', () => {
  12. const properties = {
  13. id: 'id'
  14. };
  15. const myProcessStub = stub();
  16. // type safe mock store middleware
  17. // pass through an array of tuples `[originalProcess, stub]` for mocked processes
  18. // calls to processes not stubbed/mocked get ignored
  19. const mockStore = createMockStoreMiddleware<MyState>([[myProcess, myProcessStub]]);
  20. const r = renderer(() => <MyWidget {...properties} />, {
  21. middleware: [[store, mockStore]]
  22. });
  23. r.expect(/* assertion for `Loading`*/);
  24. // assert again the stubbed process
  25. expect(myProcessStub.calledWith({ id: 'id' })).toBeTruthy();
  26. mockStore((path) => [replace(path('isLoading', true)]);
  27. r.expect(/* assertion for `Loading`*/);
  28. expect(myProcessStub.calledOnce()).toBeTruthy();
  29. // use the mock store to apply operations to the store
  30. mockStore((path) => [replace(path('details', { id: 'id' })]);
  31. mockStore((path) => [replace(path('isLoading', true)]);
  32. r.expect(/* assertion for `ShowDetails`*/);
  33. properties.id = 'other';
  34. r.expect(/* assertion for `Loading`*/);
  35. expect(myProcessStub.calledTwice()).toBeTruthy();
  36. expect(myProcessStub.secondCall.calledWith({ id: 'other' })).toBeTruthy();
  37. mockStore((path) => [replace(path('details', { id: 'other' })]);
  38. r.expect(/* assertion for `ShowDetails`*/);
  39. });
  40. });

Mock validity middleware

Using createValidityMock from @dojo/framework/testing/mocks/middleware/validity creates a mock validity middleware where the return value of the get method can get controlled in a test.

Consider the following example:

src/FormWidget.tsx

  1. import { tsx, create } from '@dojo/framework/core/vdom';
  2. import validity from '@dojo/framework/core/middleware/validity';
  3. import icache from '@dojo/framework/core/middleware/icache';
  4. import * as css from './FormWidget.m.css';
  5. const factory = create({ validity, icache });
  6. export const FormWidget = factory(function FormWidget({ middleware: { validity, icache } }) {
  7. const value = icache.getOrSet('value', '');
  8. const { valid, message } = validity.get('input', value);
  9. return (
  10. <div key="root" classes={[css.root, valid === false ? css.invalid : null]}>
  11. <input type="email" key="input" value={value} onchange={(value) => icache.set('value', value)} />
  12. {message ? <p key="validityMessage">{message}</p> : null}
  13. </div>
  14. );
  15. });

Using validityMock(key: string, value: { valid?: boolean, message?: string; }), the results of the validity mock’s get method can get controlled in a test.

tests/unit/FormWidget.tsx

  1. const { describe, it } = intern.getInterface('bdd');
  2. import { tsx } from '@dojo/framework/core/vdom';
  3. import renderer, { assertion } from '@dojo/framework/testing/renderer';
  4. import validity from '@dojo/framework/core/middleware/validity';
  5. import createValidityMock from '@dojo/framework/testing/mocks/middleware/validity';
  6. import * as css from './FormWidget.m.css';
  7. describe('Validity', () => {
  8. it('adds the "invalid" class to the wrapper when the input is invalid and displays a message', () => {
  9. const validityMock = createValidityMock();
  10. const r = renderer(() => <FormWidget />, {
  11. middleware: [[validity, validityMock]]
  12. });
  13. const WrappedRoot = wrap('div');
  14. const baseAssertion = assertion(() => (
  15. <WrappedRoot key="root" classes={[css.root, null]}>
  16. <input type="email" key="input" value="" onchange={() => {}} />
  17. </WrappedRoot>
  18. ));
  19. r.expect(baseAssertion);
  20. validityMock('input', { valid: false, message: 'invalid message' });
  21. const invalidAssertion = baseAssertion
  22. .append(WrappedRoot, () => [<p key="validityMessage">invalid message</p>])
  23. .setProperty(WrappedRoot, 'classes', [css.root, css.invalid]);
  24. r.expect(invalidAssertion);
  25. });
  26. });

Custom middleware mocks

Not all testing scenarios will be covered by the provided mocks. Custom middleware mocks can also be created. A middleware mock should provide an overloaded interface. The parameterless overload should return the middleware implementation; this is what will be injected into the widget under test. Other overloads are created as needed to provide an interface for the tests.

As an example, consider the framework’s icache mock. The mock provides these overloads:

  1. function mockCache(): MiddlewareResult<any, any, any>;
  2. function mockCache(key: string): Promise<any>;
  3. function mockCache(key?: string): Promise<any> | MiddlewareResult<any, any, any>;

The overload which accepts a key provides the test direct access to cache items. This abbreviated example demonstrates how the mock contains both the middleware implementation and the test interface; this enabled the mock to bridge the gap between the widget and the test.

  1. export function createMockMiddleware() {
  2. const sharedData = new Map<string, any>();
  3. const mockFactory = factory(() => {
  4. // actual middlware implementation; uses `sharedData` to bridge the gap
  5. return {
  6. get(id: string): any {},
  7. set(id: string, value: any): void {}
  8. };
  9. });
  10. function mockMiddleware(): MiddlewareResult<any, any, any>;
  11. function mockMiddleware(id: string): any;
  12. function mockMiddleware(id?: string): any | Middleware<any, any, any> {
  13. if (id) {
  14. // expose access to `sharedData` directly to
  15. return sharedData.get(id);
  16. } else {
  17. // provides the middleware implementation to the widget
  18. return mockFactory();
  19. }
  20. }
  21. }

There are plenty of full mock examples in framework/src/testing/mocks/middlware which can be used for reference.