模拟
一种常见的测试类型是验证小部件的用户界面按预期渲染,而不必关心小部件的底层业务逻辑。这些测试可能希望断言诸如按钮点击调用小部件属性方法之类的场景,而不关心属性方法的实现是什么,只关心界面是否按预期调用。在这种情况下,可以使用像 Sinon 这样的模拟库来提供帮助。
src/widgets/Action.tsx
import { create, tsx } from '@dojo/framework/core/vdom';
import Button from '@dojo/widgets/button';
import * as css from './Action.m.css';
const factory = create().properties<{ fetchItems: () => void }>();
const Action = factory(function Action({ properties }) {
return (
<div classes={[css.root]}>
<Button key="button" onClick={() => properties().fetchItems()}>
Fetch
</Button>
</div>
);
});
export default Action;
要测试当按钮被点击时 properties().fetchItems
方法是否被调用
tests/unit/widgets/Action.tsx
const { describe, it } = intern.getInterface('bdd');
import { tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
import Action from '../../../src/widgets/Action';
import * as css from '../../../src/widgets/Action.m.css';
import Button from '@dojo/widgets/button';
import { stub } from 'sinon';
import { assert } from 'chai';
describe('Action', () => {
const fetchItems = stub();
it('can fetch data on button click', () => {
const WrappedButton = wrap(Button);
const baseAssertion = assertion(() => (
<div classes={[css.root]}>
<WrappedButton key="button" onClick={() => {}}>
Fetch
</WrappedButton>
</div>
));
const r = renderer(() => <Action fetchItems={fetchItems} />);
r.expect(baseAssertion);
r.property(WrappedButton, 'onClick');
r.expect(baseAssertion);
assert.isTrue(fetchItems.calledOnce);
});
});
在这种情况下,为需要获取项目的 Action 小部件提供了 fetchItems
方法的模拟。然后针对 @button
键来触发按钮的 onClick
,之后验证 fetchItems
模拟只被调用了一次。
有关模拟的更多详细信息,请参阅 Sinon 文档。
提供的中间件模拟
有许多模拟中间件可用于支持测试使用相应 Dojo 中间件的小部件。模拟导出一个工厂,用于创建要在每个测试中使用的作用域模拟中间件。
breakpoint
中间件
模拟 使用 @dojo/framework/testing/mocks/middleware/breakpoint
中的 createBreakpointMock
,测试可以手动控制调整大小事件以触发断点测试。
考虑以下小部件,当 LG
断点被激活时,它会显示一个额外的 h2
src/Breakpoint.tsx
import { tsx, create } from '@dojo/framework/core/vdom';
import breakpoint from '@dojo/framework/core/middleware/breakpoint';
const factory = create({ breakpoint });
export default factory(function Breakpoint({ middleware: { breakpoint } }) {
const bp = breakpoint.get('root');
const isLarge = bp && bp.breakpoint === 'LG';
return (
<div key="root">
<h1>Header</h1>
{isLarge && <h2>Subtitle</h2>}
<div>Longer description</div>
</div>
);
});
通过在 breakpoint
中间件模拟上使用 mockBreakpoint(key: string, contentRect: Partial<DOMRectReadOnly>)
方法,测试可以显式触发给定的调整大小。
tests/unit/Breakpoint.tsx
const { describe, it } = intern.getInterface('bdd');
import { tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
import breakpoint from '@dojo/framework/core/middleware/breakpoint';
import createBreakpointMock from '@dojo/framework/testing/mocks/middleware/breakpoint';
import Breakpoint from '../../src/Breakpoint';
describe('Breakpoint', () => {
it('resizes correctly', () => {
const WrappedHeader = wrap('h1');
const mockBreakpoint = createBreakpointMock();
const baseAssertion = assertion(() => (
<div key="root">
<WrappedHeader>Header</WrappedHeader>
<div>Longer description</div>
</div>
));
const r = renderer(() => <Breakpoint />, {
middleware: [[breakpoint, mockBreakpoint]]
});
r.expect(baseAssertion);
mockBreakpoint('root', { breakpoint: 'LG', contentRect: { width: 800 } });
r.expect(baseAssertion.insertAfter(WrappedHeader, () => [<h2>Subtitle</h2>]);
});
});
focus
中间件
模拟 使用 @dojo/framework/testing/middleware/focus
中的 createFocusMock
,测试可以手动控制 focus
中间件何时报告具有指定键的节点获得焦点。
考虑以下小部件
src/FormWidget.tsx
import { tsx, create } from '@dojo/framework/core/vdom';
import focus, { FocusProperties } from '@dojo/framework/core/middleware/focus';
import * as css from './FormWidget.m.css';
export interface FormWidgetProperties extends FocusProperties {}
const factory = create({ focus }).properties<FormWidgetProperties>();
export const FormWidget = factory(function FormWidget({ middleware: { focus } }) {
return (
<div key="wrapper" classes={[css.root, focus.isFocused('text') ? css.focused : null]}>
<input type="text" key="text" value="focus me" />
</div>
);
});
通过调用 focusMock(key: string | number, value: boolean)
,可以在测试期间控制 focus
中间件的 isFocused
方法的结果。
tests/unit/FormWidget.tsx
const { describe, it } = intern.getInterface('bdd');
import { tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
import focus from '@dojo/framework/core/middleware/focus';
import createFocusMock from '@dojo/framework/testing/mocks/middleware/focus';
import * as css from './FormWidget.m.css';
describe('Focus', () => {
it('adds a "focused" class to the wrapper when the input is focused', () => {
const focusMock = createFocusMock();
const WrappedRoot = wrap('div');
const baseAssertion = assertion(() => (
<WrappedRoot key="wrapper" classes={[css.root, null]}>
<input type="text" key="text" value="focus me" />
</WrappedRoot>
));
const r = renderer(() => <FormWidget />, {
middleware: [[focus, focusMock]]
});
r.expect(baseAssertion);
focusMock('text', true);
r.expect(baseAssertion.setProperty(WrappedRoot, 'classes', [css.root, css.focused]));
});
});
icache
中间件
模拟 使用 @dojo/framework/testing/mocks/middleware/icache
中的 createICacheMiddleware
允许测试直接访问缓存项,而模拟为被测试的小部件提供了足够的 icache
体验。当 icache
用于异步检索数据时,这特别有用。直接访问缓存使测试能够 await
与小部件相同的承诺。
考虑以下小部件,它从 API 检索数据
src/MyWidget.tsx
import { tsx, create } from '@dojo/framework/core/vdom';
import { icache } from '@dojo/framework/core/middleware/icache';
import fetch from '@dojo/framework/shim/fetch';
const factory = create({ icache });
export default factory(function MyWidget({ middleware: { icache } }) {
const value = icache.getOrSet('users', async () => {
const response = await fetch('url');
return await response.json();
});
return value ? <div>{value}</div> : <div>Loading</div>;
});
使用模拟 icache
中间件测试异步结果很简单
tests/unit/MyWidget.tsx
const { describe, it, afterEach } = intern.getInterface('bdd');
import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
import { tsx } from '@dojo/framework/core/vdom';
import * as sinon from 'sinon';
import global from '@dojo/framework/shim/global';
import icache from '@dojo/framework/core/middleware/icache';
import createICacheMock from '@dojo/framework/testing/mocks/middleware/icache';
import MyWidget from '../../src/MyWidget';
describe('MyWidget', () => {
afterEach(() => {
sinon.restore();
});
it('test', async () => {
// stub the fetch call to return a known value
global.fetch = sinon.stub().returns(Promise.resolve({ json: () => Promise.resolve('api data') }));
const WrappedRoot = wrap('div');
const baseAssertion = assertion(() => <WrappedRoot>Loading</WrappedRoot>);
const mockICache = createICacheMock();
const r = renderer(() => <Home />, { middleware: [[icache, mockICache]] });
r.expect(baseAssertion);
// await the async method passed to the mock cache
await mockICache('users');
r.expect(baseAssertion.setChildren(WrappedRoot, () => ['api data']));
});
});
intersection
中间件
模拟 使用 @dojo/framework/testing/mocks/middleware/intersection
中的 createIntersectionMock
创建一个模拟交叉点中间件。要设置交叉点模拟的预期返回值,请使用 key
和预期交叉点详细信息调用创建的模拟交叉点中间件。
考虑以下小部件
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>;
});
使用模拟 intersection
中间件
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 renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
import MyWidget from './MyWidget';
describe('MyWidget', () => {
it('test', () => {
// create the intersection mock
const intersectionMock = createIntersectionMock();
// pass the intersection mock to the renderer so it knows to
// replace the original middleware
const r = renderer(() => <App key="app" />, { middleware: [[intersection, intersectionMock]] });
const WrappedRoot = wrap('div');
const assertion = assertion(() => (
<WrappedRoot key="root">{`{"intersectionRatio":0,"isIntersecting":false}`}</WrappedRoot>
));
// call renderer.expect as usual, asserting the default response
r.expect(assertion);
// 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
r.expect(assertion.setChildren(WrappedRoot, () => [`{"isIntersecting": true }`]));
});
});
node
中间件
模拟 使用 @dojo/framework/testing/mocks/middleware/node
中的 createNodeMock
为节点中间件创建一个模拟。要设置节点模拟的预期返回值,请使用 key
和预期 DOM 节点调用创建的模拟节点中间件。
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);
resize
中间件
模拟 使用 @dojo/framework/testing/mocks/middleware/resize
中的 createResizeMock
创建一个模拟调整大小中间件。要设置调整大小模拟的预期返回值,请使用 key
和预期内容矩形调用创建的模拟调整大小中间件。
const mockResize = createResizeMock();
mockResize('key', { width: 100 });
考虑以下小部件
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>;
});
使用模拟 resize
中间件
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 renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
import MyWidget from './MyWidget';
describe('MyWidget', () => {
it('test', () => {
// create the resize mock
const resizeMock = createResizeMock();
// pass the resize mock to the test renderer so it knows to replace the original
// middleware
const r = renderer(() => <App key="app" />, { middleware: [[resize, resizeMock]] });
const WrappedRoot = wrap('div');
const baseAssertion = assertion(() => <div key="root">null</div>);
// call renderer.expect as usual
r.expect(baseAssertion);
// 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
r.expect(baseAssertion.setChildren(WrappedRoot, () [`{"width":100}`]);)
});
});
store
中间件
模拟 使用 @dojo/framework/testing/mocks/middleware/store
中的 createMockStoreMiddleware
创建一个类型化的模拟存储中间件,它可以选择支持模拟进程。要模拟存储进程,请传递原始存储进程和存根进程的元组。中间件将用传递的存根替换对原始进程的调用。如果没有传递存根,中间件将简单地对所有进程调用执行无操作。
要对模拟存储进行更改,请使用返回存储操作数组的函数调用 mockStore
。这与存储的 path
函数一起注入,以创建指向需要更改的状态的指针。
mockStore((path) => [replace(path('details', { id: 'id' })]);
考虑以下小部件
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} />;
});
使用模拟 store
中间件
tests/unit/MyWidget.tsx
import { tsx } from '@dojo/framework/core/vdom'
import createMockStoreMiddleware from '@dojo/framework/testing/mocks/middleware/store';
import renderer from '@dojo/framework/testing/renderer';
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 r = renderer(() => <MyWidget {...properties} />, {
middleware: [[store, mockStore]]
});
r.expect(/* assertion for `Loading`*/);
// assert again the stubbed process
expect(myProcessStub.calledWith({ id: 'id' })).toBeTruthy();
mockStore((path) => [replace(path('isLoading', true)]);
r.expect(/* assertion 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)]);
r.expect(/* assertion for `ShowDetails`*/);
properties.id = 'other';
r.expect(/* assertion for `Loading`*/);
expect(myProcessStub.calledTwice()).toBeTruthy();
expect(myProcessStub.secondCall.calledWith({ id: 'other' })).toBeTruthy();
mockStore((path) => [replace(path('details', { id: 'other' })]);
r.expect(/* assertion for `ShowDetails`*/);
});
});
validity
中间件
模拟 使用 @dojo/framework/testing/mocks/middleware/validity
中的 createValidityMock
创建一个模拟有效性中间件,其中 get
方法的返回值可以在测试中进行控制。
考虑以下示例
src/FormWidget.tsx
import { tsx, create } from '@dojo/framework/core/vdom';
import validity from '@dojo/framework/core/middleware/validity';
import icache from '@dojo/framework/core/middleware/icache';
import * as css from './FormWidget.m.css';
const factory = create({ validity, icache });
export const FormWidget = factory(function FormWidget({ middleware: { validity, icache } }) {
const value = icache.getOrSet('value', '');
const { valid, message } = validity.get('input', value);
return (
<div key="root" classes={[css.root, valid === false ? css.invalid : null]}>
<input type="email" key="input" value={value} onchange={(value) => icache.set('value', value)} />
{message ? <p key="validityMessage">{message}</p> : null}
</div>
);
});
使用 validityMock(key: string, value: { valid?: boolean, message?: string; })
,可以在测试中控制 validity
模拟的 get
方法的结果。
tests/unit/FormWidget.tsx
const { describe, it } = intern.getInterface('bdd');
import { tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion } from '@dojo/framework/testing/renderer';
import validity from '@dojo/framework/core/middleware/validity';
import createValidityMock from '@dojo/framework/testing/mocks/middleware/validity';
import * as css from './FormWidget.m.css';
describe('Validity', () => {
it('adds the "invalid" class to the wrapper when the input is invalid and displays a message', () => {
const validityMock = createValidityMock();
const r = renderer(() => <FormWidget />, {
middleware: [[validity, validityMock]]
});
const WrappedRoot = wrap('div');
const baseAssertion = assertion(() => (
<WrappedRoot key="root" classes={[css.root, null]}>
<input type="email" key="input" value="" onchange={() => {}} />
</WrappedRoot>
));
r.expect(baseAssertion);
validityMock('input', { valid: false, message: 'invalid message' });
const invalidAssertion = baseAssertion
.append(WrappedRoot, () => [<p key="validityMessage">invalid message</p>])
.setProperty(WrappedRoot, 'classes', [css.root, css.invalid]);
r.expect(invalidAssertion);
});
});
自定义中间件模拟
并非所有测试场景都将被提供的模拟覆盖。也可以创建自定义中间件模拟。中间件模拟应该提供一个重载接口。无参数重载应该返回中间件实现;这是将被注入到被测试的小部件中的内容。根据需要创建其他重载以提供测试接口。
例如,考虑框架的 icache
模拟。模拟提供了这些重载
function mockCache(): MiddlewareResult<any, any, any>;
function mockCache(key: string): Promise<any>;
function mockCache(key?: string): Promise<any> | MiddlewareResult<any, any, any>;
接受 key
的重载为测试提供了对缓存项的直接访问。这个简化的示例演示了模拟如何包含中间件实现和测试接口;这使模拟能够弥合小部件和测试之间的差距。
export function createMockMiddleware() {
const sharedData = new Map<string, any>();
const mockFactory = factory(() => {
// actual middleware implementation; uses `sharedData` to bridge the gap
return {
get(id: string): any {},
set(id: string, value: any): void {}
};
});
function mockMiddleware(): MiddlewareResult<any, any, any>;
function mockMiddleware(id: string): any;
function mockMiddleware(id?: string): any | Middleware<any, any, any> {
if (id) {
// expose access to `sharedData` directly to
return sharedData.get(id);
} else {
// provides the middleware implementation to the widget
return mockFactory();
}
}
}
在 framework/src/testing/mocks/middleware
中有很多完整的模拟示例,可以作为参考。