@samitouri / QOS-React / commits / 71b4e99901

[react-test-renderer] Jest matchers for async tests (#13236)

Adds custom Jest matchers that help with writing async tests: - `toFlushThrough` - `toFlushAll` - `toFlushAndThrow` - `toClearYields` Each one accepts an array of expected yielded values, to prevent false negatives. Eventually I imagine we'll want to publish this on npm.

Andrew Clark committed Jul 19, 2018 at 10:26 UTC 71b4e99901a98396ca34655e0cc1c03eadcbc49b
7 files changed +202 -144
packages/react-test-renderer/src/ReactTestRenderer.js
+1 -1
@@ -472,7 +472,7 @@ const ReactTestRendererFiber = {
472 TestRendererScheduling.clearYields();
473 return TestRenderer.flushSync(fn);
474 },
475 - unstable_flushThrough: TestRendererScheduling.flushThrough,
475 + unstable_flushNumberOfYields: TestRendererScheduling.flushNumberOfYields,
476 unstable_clearYields: TestRendererScheduling.clearYields,
477 };
478
packages/react-test-renderer/src/ReactTestRendererScheduling.js
+3 -36
@@ -31,38 +31,7 @@ export function setNowImplementation(implementation: () => number): void {
31 nowImplementation = implementation;
32 }
33
34 -function verifyExpectedValues(expectedValues: Array<mixed>): void {
35 - for (let i = 0; i < expectedValues.length; i++) {
36 - const expectedValue = `"${(expectedValues[i]: any)}"`;
37 - const yieldedValue =
38 - i < yieldedValues.length ? `"${(yieldedValues[i]: any)}"` : 'nothing';
39 - if (yieldedValue !== expectedValue) {
40 - const error = new Error(
41 - `Flush expected to yield ${(expectedValue: any)}, but ${(yieldedValue: any)} was yielded`,
42 - );
43 - // Attach expected and yielded arrays,
44 - // So the caller could pretty print the diff (if desired).
45 - (error: any).expectedValues = expectedValues;
46 - (error: any).actualValues = yieldedValues;
47 - throw error;
48 - }
49 - }
50 -
51 - if (expectedValues.length !== yieldedValues.length) {
52 - const error = new Error(
53 - `Flush expected to yield ${expectedValues.length} values, but yielded ${
54 - yieldedValues.length
55 - }`,
56 - );
57 - // Attach expected and yielded arrays,
58 - // So the caller could pretty print the diff (if desired).
59 - (error: any).expectedValues = expectedValues;
60 - (error: any).actualValues = yieldedValues;
61 - throw error;
62 - }
63 -}
64 -
65 -export function flushAll(expectedValues: Array<mixed>): Array<mixed> {
34 +export function flushAll(): Array<mixed> {
35 yieldedValues = [];
36 while (scheduledCallback !== null) {
37 const cb = scheduledCallback;
@@ -78,11 +47,10 @@ export function flushAll(expectedValues: Array<mixed>): Array<mixed> {
47 didTimeout: false,
48 });
49 }
81 - verifyExpectedValues(expectedValues);
50 return yieldedValues;
51 }
52
85 -export function flushThrough(expectedValues: Array<mixed>): Array<mixed> {
53 +export function flushNumberOfYields(count: number): Array<mixed> {
54 let didStop = false;
55 yieldedValues = [];
56 while (scheduledCallback !== null && !didStop) {
@@ -90,7 +58,7 @@ export function flushThrough(expectedValues: Array<mixed>): Array<mixed> {
58 scheduledCallback = null;
59 cb({
60 timeRemaining() {
93 - if (yieldedValues.length >= expectedValues.length) {
61 + if (yieldedValues.length >= count) {
62 // We at least as many values as expected. Stop rendering.
63 didStop = true;
64 return 0;
@@ -104,7 +72,6 @@ export function flushThrough(expectedValues: Array<mixed>): Array<mixed> {
72 didTimeout: false,
73 });
74 }
107 - verifyExpectedValues(expectedValues);
75 return yieldedValues;
76 }
77
packages/react-test-renderer/src/__tests__/ReactTestRendererAsync-test.js
+121 -77
@@ -32,7 +32,7 @@ describe('ReactTestRendererAsync', () => {
32 expect(renderer.toJSON()).toEqual(null);
33
34 // Flush initial mount.
35 - renderer.unstable_flushAll([]);
35 + expect(renderer).toFlushAll([]);
36 expect(renderer.toJSON()).toEqual('Hi');
37
38 // Update
@@ -40,7 +40,7 @@ describe('ReactTestRendererAsync', () => {
40 // Not yet updated.
41 expect(renderer.toJSON()).toEqual('Hi');
42 // Flush update.
43 - renderer.unstable_flushAll([]);
43 + expect(renderer).toFlushAll([]);
44 expect(renderer.toJSON()).toEqual('Bye');
45 });
46
@@ -62,11 +62,11 @@ describe('ReactTestRendererAsync', () => {
62 unstable_isAsync: true,
63 });
64
65 - renderer.unstable_flushAll(['A:1', 'B:1', 'C:1']);
65 + expect(renderer).toFlushAll(['A:1', 'B:1', 'C:1']);
66 expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
67
68 renderer.update(<Parent step={2} />);
69 - renderer.unstable_flushAll(['A:2', 'B:2', 'C:2']);
69 + expect(renderer).toFlushAll(['A:2', 'B:2', 'C:2']);
70 expect(renderer.toJSON()).toEqual(['A:2', 'B:2', 'C:2']);
71 });
72
@@ -89,15 +89,12 @@ describe('ReactTestRendererAsync', () => {
89 });
90
91 // Flush the first two siblings
92 - expect(renderer.unstable_flushThrough(['A:1', 'B:1'])).toEqual([
93 - 'A:1',
94 - 'B:1',
95 - ]);
92 + expect(renderer).toFlushThrough(['A:1', 'B:1']);
93 // Did not commit yet.
94 expect(renderer.toJSON()).toEqual(null);
95
96 // Flush the remaining work
100 - renderer.unstable_flushAll(['C:1']);
97 + expect(renderer).toFlushAll(['C:1']);
98 expect(renderer.toJSON()).toEqual(['A:1', 'B:1', 'C:1']);
99 });
100
@@ -129,7 +126,7 @@ describe('ReactTestRendererAsync', () => {
126 });
127
128 // Flush the some of the changes, but don't commit
132 - expect(renderer.unstable_flushThrough(['A:1'])).toEqual(['A:1']);
129 + expect(renderer).toFlushThrough(['A:1']);
130 expect(renderer.toJSON()).toEqual(null);
131
132 // Interrupt with higher priority properties
@@ -141,89 +138,136 @@ describe('ReactTestRendererAsync', () => {
138 expect(renderer.toJSON()).toEqual(['A:2', 'B:2']);
139 });
140
144 - it('should error if flushThrough params dont match yielded values', () => {
145 - const Yield = ({id}) => {
146 - ReactTestRenderer.unstable_yield(id);
147 - return id;
148 - };
141 + describe('Jest matchers', () => {
142 + it('toFlushThrough', () => {
143 + const Yield = ({id}) => {
144 + ReactTestRenderer.unstable_yield(id);
145 + return id;
146 + };
147 +
148 + const renderer = ReactTestRenderer.create(
149 + <div>
150 + <Yield id="foo" />
151 + <Yield id="bar" />
152 + <Yield id="baz" />
153 + </div>,
154 + {
155 + unstable_isAsync: true,
156 + },
157 + );
158
150 - const renderer = ReactTestRenderer.create(
151 - <div>
152 - <Yield id="foo" />
153 - <Yield id="bar" />
154 - <Yield id="baz" />
155 - </div>,
156 - {
157 - unstable_isAsync: true,
158 - },
159 - );
159 + expect(() => expect(renderer).toFlushThrough(['foo', 'baz'])).toThrow(
160 + 'Expected value to equal:',
161 + );
162 + });
163
161 - expect(() => renderer.unstable_flushThrough(['foo', 'baz'])).toThrow(
162 - 'Flush expected to yield "baz", but "bar" was yielded',
163 - );
164 - });
164 + it('toFlushAll', () => {
165 + const Yield = ({id}) => {
166 + ReactTestRenderer.unstable_yield(id);
167 + return id;
168 + };
169 +
170 + const renderer = ReactTestRenderer.create(
171 + <div>
172 + <Yield id="foo" />
173 + <Yield id="bar" />
174 + <Yield id="baz" />
175 + </div>,
176 + {
177 + unstable_isAsync: true,
178 + },
179 + );
180
166 - it('should error if flushAll params dont match yielded values', () => {
167 - const Yield = ({id}) => {
168 - ReactTestRenderer.unstable_yield(id);
169 - return id;
170 - };
181 + expect(() => expect(renderer).toFlushAll([])).toThrowError(
182 + 'Expected value to equal:',
183 + );
184 +
185 + renderer.update(
186 + <div>
187 + <Yield id="foo" />
188 + <Yield id="bar" />
189 + <Yield id="baz" />
190 + </div>,
191 + );
192 +
193 + expect(() => expect(renderer).toFlushAll(['foo', 'baz'])).toThrow(
194 + 'Expected value to equal:',
195 + );
196 + });
197 +
198 + it('toFlushAndThrow', () => {
199 + const Yield = ({id}) => {
200 + ReactTestRenderer.unstable_yield(id);
201 + return id;
202 + };
203 +
204 + function BadRender() {
205 + throw new Error('Oh no!');
206 + }
207
172 - const renderer = ReactTestRenderer.create(
173 - <div>
174 - <Yield id="foo" />
175 - <Yield id="bar" />
176 - <Yield id="baz" />
177 - </div>,
178 - {
208 + function App() {
209 + return (
210 + <div>
211 + <Yield id="A" />
212 + <Yield id="B" />
213 + <BadRender />
214 + <Yield id="C" />
215 + <Yield id="D" />
216 + </div>
217 + );
218 + }
219 +
220 + const renderer = ReactTestRenderer.create(<App />, {
221 unstable_isAsync: true,
180 - },
181 - );
222 + });
223
183 - expect(() => renderer.unstable_flushAll([])).toThrow(
184 - 'Flush expected to yield 0 values, but yielded 3',
185 - );
224 + expect(() => {
225 + expect(renderer).toFlushAndThrow(
226 + // Wrong expected values
227 + ['A', 'B'],
228 + 'Oh no!',
229 + );
230 + }).toThrow('Expected value to equal:');
231
187 - renderer.update(
188 - <div>
189 - <Yield id="foo" />
190 - <Yield id="bar" />
191 - <Yield id="baz" />
192 - </div>,
193 - );
232 + renderer.update(<App />);
233
195 - expect(() => renderer.unstable_flushAll(['foo', 'baz'])).toThrow(
196 - 'Flush expected to yield "baz", but "bar" was yielded',
197 - );
234 + expect(() => {
235 + expect(renderer).toFlushAndThrow(
236 + ['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D'],
237 + // Wrong error message
238 + 'Oops!',
239 + );
240 + }).toThrow('Expected the function to throw an error matching:');
241 +
242 + renderer.update(<App />);
243 +
244 + // Passes
245 + expect(renderer).toFlushAndThrow(
246 + ['A', 'B', 'C', 'D', 'A', 'B', 'C', 'D'],
247 + 'Oh no!',
248 + );
249 + });
250 });
251
200 - it('should error if flushThrough yields the wrong number of values', () => {
252 + it('toClearYields', () => {
253 const Yield = ({id}) => {
254 ReactTestRenderer.unstable_yield(id);
255 return id;
256 };
257
206 - const renderer = ReactTestRenderer.create(
207 - <div>
208 - <Yield id="foo" />
209 - </div>,
210 - {
211 - unstable_isAsync: true,
212 - },
213 - );
214 -
215 - expect(() => renderer.unstable_flushThrough(['foo', 'bar'])).toThrow(
216 - 'Flush expected to yield "bar", but nothing was yielded',
217 - );
218 - });
219 -
220 - it('should error if flushThrough yields no values', () => {
221 - const renderer = ReactTestRenderer.create(null, {
222 - unstable_isAsync: true,
223 - });
258 + function App() {
259 + return (
260 + <div>
261 + <Yield id="A" />
262 + <Yield id="B" />
263 + <Yield id="C" />
264 + </div>
265 + );
266 + }
267
225 - expect(() => renderer.unstable_flushThrough(['foo'])).toThrow(
226 - 'Flush expected to yield "foo", but nothing was yielded',
268 + ReactTestRenderer.create(<App />);
269 + expect(() => expect(ReactTestRenderer).toClearYields(['A', 'B'])).toThrow(
270 + 'Expected value to equal:',
271 );
272 });
273 });
packages/react/src/__tests__/ReactProfiler-test.internal.js
+18 -30
@@ -178,9 +178,9 @@ describe('Profiler', () => {
178 );
179
180 // Times are logged until a render is committed.
181 - renderer.unstable_flushThrough(['first']);
181 + expect(renderer).toFlushThrough(['first']);
182 expect(callback).toHaveBeenCalledTimes(0);
183 - renderer.unstable_flushAll(['last']);
183 + expect(renderer).toFlushAll(['last']);
184 expect(callback).toHaveBeenCalledTimes(1);
185 });
186
@@ -528,13 +528,11 @@ describe('Profiler', () => {
528 </React.unstable_Profiler>,
529 {unstable_isAsync: true},
530 );
531 - expect(renderer.unstable_flushThrough(['Yield:2'])).toEqual([
532 - 'Yield:2',
533 - ]);
531 + expect(renderer).toFlushThrough(['Yield:2']);
532 expect(callback).toHaveBeenCalledTimes(0);
533
534 // Resume render for remaining children.
537 - renderer.unstable_flushAll(['Yield:3']);
535 + expect(renderer).toFlushAll(['Yield:3']);
536
537 // Verify that logged times include both durations above.
538 expect(callback).toHaveBeenCalledTimes(1);
@@ -568,9 +566,7 @@ describe('Profiler', () => {
566 </React.unstable_Profiler>,
567 {unstable_isAsync: true},
568 );
571 - expect(renderer.unstable_flushThrough(['Yield:5'])).toEqual([
572 - 'Yield:5',
573 - ]);
569 + expect(renderer).toFlushThrough(['Yield:5']);
570 expect(callback).toHaveBeenCalledTimes(0);
571
572 // Simulate time moving forward while frame is paused.
@@ -578,7 +574,7 @@ describe('Profiler', () => {
574
575 // Flush the remaninig work,
576 // Which should take an additional 10ms of simulated time.
581 - renderer.unstable_flushAll(['Yield:10', 'Yield:17']);
577 + expect(renderer).toFlushAll(['Yield:10', 'Yield:17']);
578 expect(callback).toHaveBeenCalledTimes(2);
579
580 const [innerCall, outerCall] = callback.mock.calls;
@@ -617,9 +613,7 @@ describe('Profiler', () => {
613 </React.unstable_Profiler>,
614 {unstable_isAsync: true},
615 );
620 - expect(renderer.unstable_flushThrough(['Yield:10'])).toEqual([
621 - 'Yield:10',
622 - ]);
616 + expect(renderer).toFlushThrough(['Yield:10']);
617 expect(callback).toHaveBeenCalledTimes(0);
618
619 // Simulate time moving forward while frame is paused.
@@ -634,7 +628,7 @@ describe('Profiler', () => {
628 </React.unstable_Profiler>,
629 );
630 });
637 - expect(ReactTestRenderer.unstable_clearYields()).toEqual(['Yield:5']);
631 + expect(ReactTestRenderer).toClearYields(['Yield:5']);
632
633 // The initial work was thrown away in this case,
634 // So the actual and base times should only include the final rendered tree times.
@@ -648,7 +642,7 @@ describe('Profiler', () => {
642 callback.mockReset();
643
644 // Verify no more unexpected callbacks from low priority work
651 - renderer.unstable_flushAll([]);
645 + expect(renderer).toFlushAll([]);
646 expect(callback).toHaveBeenCalledTimes(0);
647 });
648
@@ -673,7 +667,7 @@ describe('Profiler', () => {
667
668 // Render everything initially.
669 // This should take 21 seconds of actual and base time.
676 - renderer.unstable_flushAll(['Yield:6', 'Yield:15']);
670 + expect(renderer).toFlushAll(['Yield:6', 'Yield:15']);
671 expect(callback).toHaveBeenCalledTimes(1);
672 let call = callback.mock.calls[0];
673 expect(call[2]).toBe(21); // actual time
@@ -694,18 +688,14 @@ describe('Profiler', () => {
688 <Yield renderTime={9} />
689 </React.unstable_Profiler>,
690 );
697 - expect(renderer.unstable_flushThrough(['Yield:3'])).toEqual([
698 - 'Yield:3',
699 - ]);
691 + expect(renderer).toFlushThrough(['Yield:3']);
692 expect(callback).toHaveBeenCalledTimes(0);
693
694 // Simulate time moving forward while frame is paused.
695 advanceTimeBy(100); // 59 -> 159
696
697 // Render another 5ms of simulated time.
706 - expect(renderer.unstable_flushThrough(['Yield:5'])).toEqual([
707 - 'Yield:5',
708 - ]);
698 + expect(renderer).toFlushThrough(['Yield:5']);
699 expect(callback).toHaveBeenCalledTimes(0);
700
701 // Simulate time moving forward while frame is paused.
@@ -720,7 +710,7 @@ describe('Profiler', () => {
710 </React.unstable_Profiler>,
711 );
712 });
723 - expect(ReactTestRenderer.unstable_clearYields()).toEqual(['Yield:11']);
713 + expect(ReactTestRenderer).toClearYields(['Yield:11']);
714
715 // The actual time should include only the most recent render,
716 // Because this lets us avoid a lot of commit phase reset complexity.
@@ -733,7 +723,7 @@ describe('Profiler', () => {
723 expect(call[5]).toBe(275); // commit time
724
725 // Verify no more unexpected callbacks from low priority work
736 - renderer.unstable_flushAll([]);
726 + expect(renderer).toFlushAll([]);
727 expect(callback).toHaveBeenCalledTimes(1);
728 });
729
@@ -784,7 +774,7 @@ describe('Profiler', () => {
774 // Render everything initially.
775 // This simulates a total of 14ms of actual render time.
776 // The base render time is also 14ms for the initial render.
787 - renderer.unstable_flushAll([
777 + expect(renderer).toFlushAll([
778 'FirstComponent:1',
779 'Yield:4',
780 'SecondComponent:2',
@@ -804,9 +794,7 @@ describe('Profiler', () => {
794 // Render a partially update, but don't finish.
795 // This partial render will take 10ms of actual render time.
796 first.setState({renderTime: 10});
807 - expect(renderer.unstable_flushThrough(['FirstComponent:10'])).toEqual([
808 - 'FirstComponent:10',
809 - ]);
797 + expect(renderer).toFlushThrough(['FirstComponent:10']);
798 expect(callback).toHaveBeenCalledTimes(0);
799
800 // Simulate time moving forward while frame is paused.
@@ -815,7 +803,7 @@ describe('Profiler', () => {
803 // Interrupt with higher priority work.
804 // This simulates a total of 37ms of actual render time.
805 renderer.unstable_flushSync(() => second.setState({renderTime: 30}));
818 - expect(ReactTestRenderer.unstable_clearYields()).toEqual([
806 + expect(ReactTestRenderer).toClearYields([
807 'SecondComponent:30',
808 'Yield:7',
809 ]);
@@ -842,7 +830,7 @@ describe('Profiler', () => {
830 // The tree contains 42ms of base render time at this point,
831 // Reflecting the most recent (longer) render durations.
832 // TODO: This actual time should decrease by 10ms once the scheduler supports resuming.
845 - renderer.unstable_flushAll(['FirstComponent:10', 'Yield:4']);
833 + expect(renderer).toFlushAll(['FirstComponent:10', 'Yield:4']);
834 expect(callback).toHaveBeenCalledTimes(1);
835 call = callback.mock.calls[0];
836 expect(call[2]).toBe(14); // actual time
scripts/jest/matchers/testRenderer.js new
+57
@@ -0,0 +1,57 @@
1 +'use strict';
2 +
3 +function captureAssertion(fn) {
4 + // Trick to use a Jest matcher inside another Jest matcher. `fn` contains an
5 + // assertion; if it throws, we capture the error and return it, so the stack
6 + // trace presented to the user points to the original assertion in the
7 + // test file.
8 + try {
9 + fn();
10 + } catch (error) {
11 + return {
12 + pass: false,
13 + message: () => error.message,
14 + };
15 + }
16 + return {pass: true};
17 +}
18 +
19 +function toFlushAll(renderer, expectedYields) {
20 + const actualYields = renderer.unstable_flushAll();
21 + return captureAssertion(() => expect(actualYields).toEqual(expectedYields));
22 +}
23 +
24 +function toFlushThrough(renderer, expectedYields) {
25 + const actualYields = renderer.unstable_flushNumberOfYields(
26 + expectedYields.length
27 + );
28 + return captureAssertion(() => expect(actualYields).toEqual(expectedYields));
29 +}
30 +
31 +function toClearYields(ReactTestRenderer, expectedYields) {
32 + const actualYields = ReactTestRenderer.unstable_clearYields();
33 + return captureAssertion(() => expect(actualYields).toEqual(expectedYields));
34 +}
35 +
36 +function toFlushAndThrow(renderer, expectedYields, ...rest) {
37 + return captureAssertion(() => {
38 + try {
39 + expect(() => {
40 + renderer.unstable_flushAll();
41 + }).toThrow(...rest);
42 + } catch (error) {
43 + const actualYields = renderer.unstable_clearYields();
44 + expect(actualYields).toEqual(expectedYields);
45 + throw error;
46 + }
47 + const actualYields = renderer.unstable_clearYields();
48 + expect(actualYields).toEqual(expectedYields);
49 + });
50 +}
51 +
52 +module.exports = {
53 + toFlushAll,
54 + toFlushThrough,
55 + toFlushAndThrow,
56 + toClearYields,
57 +};
scripts/jest/setupTests.js
+1
@@ -44,6 +44,7 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
44
45 expect.extend({
46 ...require('./matchers/toWarnDev'),
47 + ...require('./matchers/testRenderer'),
48 });
49
50 // We have a Babel transform that inserts guards against infinite loops.
scripts/jest/spec-equivalence-reporter/setupTests.js
+1
@@ -47,6 +47,7 @@ global.spyOnProd = function(...args) {
47
48 expect.extend({
49 ...require('../matchers/toWarnDev'),
50 + ...require('../matchers/testRenderer'),
51 });
52
53 beforeEach(() => (numExpectations = 0));