dojo 龙主 logo

测试渲染器

Dojo 提供了一个简单且类型安全的测试渲染器,用于浅层断言小部件的预期输出和行为。测试渲染器的 API 从一开始就被设计为鼓励单元测试最佳实践,以确保 Dojo 应用程序的高置信度。

使用 断言 和测试渲染器是通过使用在断言结构中定义的 包装测试节点 来完成的,确保在整个测试生命周期中类型安全。

小部件的预期结构是使用断言定义的,并传递给测试渲染器的 .expect() 函数,该函数执行断言。

src/MyWidget.spec.tsx

import { tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion } from '@dojo/framework/testing/renderer';

import MyWidget from './MyWidget';

const baseAssertion = assertion(() => (
	<div>
		<h1>Heading</h1>
		<h2>Sub Heading</h2>
		<div>Content</div>
	</div>
));

const r = renderer(() => <MyWidget />);

r.expect(baseAssertion);

包装测试节点

为了使测试渲染器和断言能够识别预期节点结构和实际节点结构中的节点,必须使用特殊的包装节点。包装节点可以在预期断言结构中代替真实节点使用,从而保持所有正确的属性和子节点类型。

要创建包装测试节点,请使用 @dojo/framework/testing/renderer 中的 wrap 函数

src/MyWidget.spec.tsx

import { wrap } from '@dojo/framework/testing/renderer';

import MyWidget from './MyWidget';

// Create a wrapped node for a widget
const WrappedMyWidget = wrap(MyWidget);

// Create a wrapped node for a vnode
const WrappedDiv = wrap('div');

测试渲染器使用包装测试节点在预期树中的位置来尝试对被测小部件的实际输出执行任何请求的操作(无论是 r.property() 还是 r.child())。如果包装测试节点与实际输出树中对应的节点不匹配,则不会执行任何操作,并且断言将报告失败。

注意:包装测试节点应该在一个断言中只使用一次,如果在断言期间检测到同一个测试节点多次,则会抛出错误并导致测试失败。

断言

断言用于构建预期的小部件输出结构,以便与 renderer.expect() 一起使用。断言公开了一系列 API,这些 API 使预期输出能够在测试之间有所不同。

给定一个小部件,它根据属性值渲染不同的输出

src/Profile.tsx

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

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

export interface ProfileProperties {
	username?: string;
}

const factory = create().properties<ProfileProperties>();

const Profile = factory(function Profile({ properties }) {
	const { username = 'Stranger' } = properties();
	return <h1 classes={[css.root]}>{`Welcome ${username}!`}</h1>;
});

export default Profile;

使用 @dojo/framework/testing/renderer#assertion 创建断言

src/Profile.spec.tsx

const { describe, it } = intern.getInterface('bdd');
import { tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';

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

// Create a wrapped node
const WrappedHeader = wrap('h1');

// Create an assertion using the `WrappedHeader` in place of the `h1`
const baseAssertion = assertion(() => <WrappedHeader classes={[css.root]}>Welcome Stranger!</WrappedHeader>);

describe('Profile', () => {
	it('Should render using the default username', () => {
		const r = renderer(() => <Profile />);

		// Test against the base assertion
		r.expect(baseAssertion);
	});
});

要测试何时将 username 属性传递给 Profile 小部件,我们可以使用更新的预期用户名创建一个新的断言。但是,随着小部件功能的增加,为每个场景重新创建整个断言会变得冗长且难以维护,因为对公共小部件结构的任何更改都需要更新每个断言。

为了帮助避免维护开销并减少重复,断言提供了一个全面的 API,用于从基本断言创建变体。断言 API 使用包装测试节点来识别要更新的预期结构中的节点。

src/Profile.spec.tsx

const { describe, it } = intern.getInterface('bdd');
import { tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';

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

// Create a wrapped node
const WrappedHeader = wrap('h1');

// Create an assertion using the `WrappedHeader` in place of the `h1`
const baseAssertion = assertion(() => <WrappedHeader classes={[css.root]}>Welcome Stranger!</WrappedHeader>);

describe('Profile', () => {
	it('Should render using the default username', () => {
		const r = renderer(() => <Profile />);

		// Test against the base assertion
		r.expect(baseAssertion);
	});

	it('Should render using the passed username', () => {
		const r = renderer(() => <Profile username="Dojo" />);

		// Create a variation of the base assertion
		const usernameAssertion = baseAssertion.setChildren(WrappedHeader, () => ['Dojo']);

		// Test against the username assertion
		r.expect(usernameAssertion);
	});
});

从基本断言创建断言意味着,如果对默认小部件输出有更改,则只需要更改 baseAssertion 即可更新所有小部件的测试。

断言 API

assertion.setChildren()

返回一个新的断言,其中包含新的子节点,这些子节点根据传递的 type 预先附加、附加或替换。

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

assertion.append()

返回一个新的断言,其中新的子节点附加到节点的现有子节点。

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

assertion.prepend()

返回一个新的断言,其中新的子节点预先附加到节点的现有子节点。

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

assertion.replaceChildren()

返回一个新的断言,其中新的子节点替换节点的现有子节点。

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

assertion.insertSiblings()

返回一个新的断言,其中传递的子节点根据传递的 type 插入 beforeafter

.insertSiblings(
  wrapped: Wrapped,
  children: () => RenderResult,
  type: 'before' | 'after' = 'before'
): AssertionResult;

assertion.insertBefore()

返回一个新的断言,其中传递的子节点插入到现有节点的子节点之前。

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

assertion.insertAfter()

返回一个新的断言,其中传递的子节点插入到现有节点的子节点之后。

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

assertion.replace()

返回一个新的断言,用传递的节点替换现有节点。请注意,如果您需要在断言或测试渲染器中与新节点交互,则它应该是一个包装测试节点。

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

assertion.remove()

返回一个新的断言,完全删除目标包装节点。

.remove(wrapped: Wrapped): AssertionResult;

assertion.setProperty()

返回一个新的断言,其中包含目标包装节点的更新属性。

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

assertion.setProperties()

返回一个新的断言,其中包含目标包装节点的更新属性。

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

可以在属性对象的位置设置一个函数,以根据实际属性返回预期属性。

触发属性

除了断言小部件的输出之外,还可以使用 renderer.property() 函数测试小部件的行为。property() 函数接受一个 包装测试节点 和要在下一次调用 expect() 之前调用的属性的键。

src/MyWidget.tsx

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

import MyWidgetWithChildren from './MyWidgetWithChildren';

const factory = create({ icache }).properties<{ onClick: () => void }>();

export const MyWidget = factory(function MyWidget({ properties, middleware: { icache } }) {
	const count = icache.getOrSet('count', 0);
	return (
		<div>
			<h1>Header</h1>
			<span>{`${count}`}</span>
			<button
				onclick={() => {
					icache.set('count', icache.getOrSet('count', 0) + 1);
					properties().onClick();
				}}
			>
				Increase Counter!
			</button>
		</div>
	);
});

src/MyWidget.spec.tsx

const { describe, it } = intern.getInterface('bdd');
import { tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';
import * as sinon from 'sinon';

import MyWidget from './MyWidget';

// Create a wrapped node for the button
const WrappedButton = wrap('button');

const WrappedSpan = wrap('span');

const baseAssertion = assertion(() => (
    <div>
      <h1>Header</h1>
      <WrappedSpan>0</WrappedSpan>
      <WrappedButton onclick={() => {
        icache.set('count', icache.getOrSet('count', 0) + 1);
        properties().onClick();
      }}>Increase Counter!</button>
    </WrappedButton>
));

describe('MyWidget', () => {
    it('render', () => {
        const onClickStub = sinon.stub();
        const r = renderer(() => <MyWidget onClick={onClickStub} />);

        // assert against the base assertion
        r.expect(baseAssertion);

        // register a call to the button's onclick property
        r.property(WrappedButton, 'onclick');

        // create a new assertion with the updated count
        const counterAssertion = baseAssertion.setChildren(WrappedSpan, () => ['1']);

        // expect against the new assertion, the property will be called before the test render
        r.expect(counterAssertion);

        // once the assertion is complete, check that the stub property was called
        assert.isTrue(onClickStub.calledOnce);
    });
});

函数的参数可以在函数名称之后传递,例如 r.property(WrappedButton, 'onclick', { target: { value: 'value' }})。当函数有多个参数时,它们一个接一个地传递 r.property(WrappedButton, 'onclick', 'first-arg', 'second-arg', 'third-arg')

断言函数式子组件

为了断言函数式子组件的输出,测试渲染器需要理解如何解析子组件渲染函数。这包括传入任何预期的注入值。

测试渲染器 renderer.child() 函数允许解析子组件,以便将它们包含在断言中。使用 .child() 函数要求在断言中包含具有函数式子组件的小部件时进行包装,并且包装的节点将传递给 .child 函数。

src/MyWidget.tsx

import { create, tsx } from '@dojo/framework/core/vdom';
import { RenderResult } from '@dojo/framework/core/interfaces';

import MyWidgetWithChildren from './MyWidgetWithChildren';

const factory = create().children<(value: string) => RenderResult>();

export const MyWidget = factory(function MyWidget() {
	return (
		<div>
			<h1>Header</h1>
			<MyWidgetWithChildren>{(value) => <div>{value}</div>}</MyWidgetWithChildren>
		</div>
	);
});

src/MyWidget.spec.tsx

const { describe, it } = intern.getInterface('bdd');
import { tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer';

import MyWidgetWithChildren from './MyWidgetWithChildren';
import MyWidget from './MyWidget';

// Create a wrapped node for the widget with functional children
const WrappedMyWidgetWithChildren = wrap(MyWidgetWithChildren);

const baseAssertion = assertion(() => (
    <div>
      <h1>Header</h1>
      <WrappedMyWidgetWithChildren>{() => <div>Hello!</div>}</MyWidgetWithChildren>
    </div>
));

describe('MyWidget', () => {
    it('render', () => {
        const r = renderer(() => <MyWidget />);

        // instruct the test renderer to resolve the children
        // with the provided params
        r.child(WrappedMyWidgetWithChildren, ['Hello!']);

        r.expect(baseAssertion);
    });
});

自定义属性比较器

在某些情况下,属性的精确值在测试期间是未知的,因此需要使用自定义比较器。自定义比较器用于任何包装的小部件,以及 @dojo/framework/testing/renderer#compare 函数,而不是通常的小部件或节点属性。

compare(comparator: (actual) => boolean)
import { assertion, wrap, compare } from '@dojo/framework/testing/renderer';

// create a wrapped node the `h1`
const WrappedHeader = wrap('h1');

const baseAssertion = assertion(() => (
	<div>
		<WrappedHeader id={compare((actual) => typeof actual === 'string')}>Header!</WrappedHeader>
	</div>
));

断言期间忽略节点

在处理渲染多个项目的部件(例如列表)时,可能希望能够指示测试渲染器忽略输出的某些部分。例如,断言第一个和最后一个项目有效,然后忽略中间项目的详细信息,只需断言它们是预期的类型。要使用测试渲染器执行此操作,可以使用 ignore 函数,该函数指示测试渲染器忽略节点,只要它是相同的类型,即匹配标签名称或匹配小部件工厂/构造函数。

import { create, tsx } from '@dojo/framework/core/vdom';
import renderer, { assertion, ignore } from '@dojo/framework/testing/renderer';

const factory = create().properties<{ items: string[] }>();

const ListWidget = create(function ListWidget({ properties }) {
	const { items } = properties();
	return (
		<div>
			<ul>{items.map((item) => <li>{item}</li>)}</ul>
		</div>
	);
});

const r = renderer(() => <ListWidget items={['a', 'b', 'c', 'd']} />);
const IgnoredItem = ignore('li');
const listAssertion = assertion(() => (
	<div>
		<ul>
			<li>a</li>
			<IgnoredItem />
			<IgnoredItem />
			<li>d</li>
		</ul>
	</div>
));
r.expect(listAssertion);

模拟中间件

初始化测试渲染器时,可以将模拟中间件指定为 RendererOptions 的一部分。模拟中间件被定义为原始中间件和模拟中间件实现的元组。模拟中间件的创建方式与任何其他中间件相同。

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

import MyWidget from './MyWidget';

describe('MyWidget', () => {
	it('renders', () => {
		const r = renderer(() => <MyWidget />, { middleware: [[myMiddleware, myMockMiddleware]] });

		h
			.expect
			/** assertion that executes the mock middleware instead of the normal middleware **/
			();
	});
});

测试渲染器会自动模拟一些核心中间件,这些中间件将被注入到任何需要它们的中间件中。

  • invalidator
  • setProperty
  • destroy

此外,还有一些模拟中间件可用于支持使用相应提供的 Dojo 中间件的小部件。有关提供的模拟中间件的更多信息,请参阅 模拟 部分。