85 | );
86 |
87 | describe('CoolComponent', () => {
88 | it('should render the greeting', () => {
89 | const renderer = TestUtils.createRenderer();
90 | renderer.render(
);
91 | const actual = renderer.getRenderOutput();
92 | const expected = (
93 |
97 | );
98 | expect(actual).toEqual(expected)
99 | });
100 | })
101 | ```
102 |
103 | * What about when the test fails? We get a giant diff dump of internal react structure. We need better diffing of JSX output.
104 |
105 | We can get this with [expect-jsx](https://github.com/algolia/expect-jsx).
106 |
107 | Some things we can do with `expect-jsx`: `toEqualJSX`, `toNotEqualJSX`, `toIncludeJSX` (recursive children search). This last one means you don't have to tightly couple test-selectors to your HTML structure.
108 |
109 | ```sh
110 | npm install --save-dev expect-jsx
111 | ```
112 |
113 | ```js
114 | import React from 'react';
115 | import TestUtils from 'react-addon-test-utils';
116 | import expect from 'expect';
117 | import expectJSX from 'expect-jsx';
118 | expect.extend(expectJSX);
119 |
120 | const CoolComponent = ({greeting}) => (
121 |
122 |
Greeting
123 |
{greeting}
124 |
125 | );
126 |
127 | describe('CoolComponent', () => {
128 | it('should render the greeting', () => {
129 | const renderer = TestUtils.createRenderer();
130 | renderer.render(
);
131 | const actual = renderer.getRenderOutput();
132 | const expected =
hello world
;
133 |
134 | expect(actual).toIncludeJSX(expected);
135 | });
136 | })
137 | ```
138 |
139 | # 6 - Element types with Shallow Rendering
140 |
141 | This is about using the `type` attribute of `getRenderOutput()`. We can assert that the rendered output is a certain type of tag.
142 |
143 | ```js
144 | import React from 'react';
145 | import TestUtils from 'react-addon-test-utils';
146 | import expect from 'expect';
147 | import LikeCounter from './LikeCounter';
148 |
149 | describe('LikeCounter', () => {
150 | it('should be a link', () => {
151 | const renderer = TestUtils.createRenderer();
152 | renderer.render(
);
153 |
154 | const actual = renderer.getRenderOutput().type;
155 | const expected = 'a';
156 | expect(actual).toEqual(expected);
157 |
158 | });
159 | });
160 |
161 | ```
162 |
163 | # 7 - className with shallow rendering
164 |
165 | We want to write tests to ensure our icons are rendering correctly.
166 |
167 | ```js
168 | import React from 'react';
169 | import TestUtils from 'react-addon-test-utils';
170 | import expect from 'expect';
171 | import Icon from './Icon';
172 |
173 | describe('Icon', () => {
174 | it('should render the icon', () => {
175 | const renderer = TestUtils.createRenderer();
176 | renderer.render(
);
177 |
178 | // `includes` is an ES6 String.prototype function
179 | const actual = renderer.getRenderOutput().props.className.includes('facebook');
180 | const expected = true;
181 |
182 | expect(actual).toEqual(expected);
183 |
184 | });
185 | });
186 | ```
187 |
188 | # 8 - Conditional className with shallow rendering
189 |
190 | Same idea as #7, but with a conditional on the component (`isActive=[bool]`). Test both true and false.
191 |
192 | ```js
193 | import React from 'react';
194 | import TestUtils from 'react-addon-test-utils';
195 | import expect from 'expect';
196 | import LikeCounter from './LikeCounter';
197 |
198 | describe('LikeCounter', () => {
199 | it('should show the like count as active', () => {
200 | const renderer = TestUtils.createRenderer();
201 | renderer.render(
);
202 |
203 | const actual = renderer.getRenderOutput().props.className.includes('LikeCounter--active');
204 | const expected = true;
205 | expect(actual).toEqual(expected);
206 | });
207 |
208 | it('should show the like count as inactive', () => {
209 | const renderer = TestUtils.createRenderer();
210 | renderer.render(
);
211 |
212 | const actual = renderer.getRenderOutput().props.className.includes('LikeCounter--active');
213 | const expected = false;
214 | expect(actual).toEqual(expected);
215 | });
216 | });
217 |
218 | ```
219 |
220 | # 9 - Reusing test boilerplate
221 |
222 | Let's refactor #8 using some shared logic in the describe block. He calls it a factory function. OK.
223 |
224 | ```js
225 | import React from 'react';
226 | import TestUtils from 'react-addon-test-utils';
227 | import expect from 'expect';
228 | import LikeCounter from './LikeCounter';
229 |
230 | describe('LikeCounter', () => {
231 | function renderLikeCounter(isActive) {
232 | const renderer = TestUtils.createRenderer();
233 | renderer.render(
);
234 |
235 | return renderer
236 | .getRenderOutput()
237 | .props
238 | .className
239 | .includes('LikeCounter--active');
240 | }
241 |
242 | describe('isActive', () => {
243 | it('should show the like count as active', () => {
244 | expect(renderLikeCounter(true)).toEqual(true);
245 | });
246 |
247 | it('should show the like count as inactive', () => {
248 | expect(renderLikeCounter(false)).toEqual(false);
249 | });
250 | });
251 |
252 | });
253 |
254 | ```
255 |
256 | # 10 - Children with shallow rendering
257 |
258 | ```js
259 | import React from 'react';
260 | import TestUtils from 'react-addon-test-utils';
261 | import expect from 'expect';
262 | import expectJSX from 'expect-jsx';
263 | expect.extend(expectJSX);
264 | import LikeCounter from './LikeCounter';
265 |
266 | describe('LikeCounter', () => {
267 | it('should render like counts', () => {
268 | const renderer = TestUtils.createRenderer();
269 | renderer.render(
);
270 |
271 | // const children = renderer.getRenderOutput().props.children;
272 | // We could just keep chaining .props.children ... on and on
273 | // But this is ugly. How else to do it? (A: toIncludeJSX)
274 |
275 | const expected = '5 likes';
276 | const actual = renderer.getRenderOutput();
277 | expect(actual).toIncludeJSX(expected);
278 | });
279 | });
280 |
281 | ```
282 |
283 | # 11 - The Redux Store - Multiple Actions
284 |
285 | In this sort-of-integration-test we dispatch multiple actions, and only make one assertion at the end to verify the final state.
286 |
287 | ```js
288 | import { store } from './store';
289 | import expect from 'expect';
290 |
291 | describe('store', () => {
292 | it('should work with a series of actions', () => {
293 | const actions = [
294 | {
295 | type: 'ADD_QUOTE_BY_ID',
296 | payload: {
297 | text: 'The best way to cheer yourself up is to try to cheer someone else up.',
298 | author: 'Mark Twain',
299 | id: 1,
300 | likeCount: 24
301 | }
302 | },
303 | {
304 | type: 'ADD_QUOTE_BY_ID',
305 | payload: {
306 | text: 'Whatever you are, be a good one.',
307 | author: 'Abraham Lincoln',
308 | id: 2,
309 | likeCount: 0
310 | }
311 | },
312 | {
313 | type: 'REMOVE_QUOTE_BY_ID',
314 | payload: { id: 1 }
315 | },
316 | {
317 | type: 'LIKE_QUOTE_BY_ID',
318 | payload: { id: 2 }
319 | },
320 | {
321 | type: 'LIKE_QUOTE_BY_ID',
322 | payload: { id: 2 }
323 | },
324 | {
325 | type: 'UNLIKE_QUOTE_BY_ID',
326 | payload: { id: 2 }
327 | },
328 | {
329 | type: 'UPDATE_THEME_COLOR',
330 | payload: { color: '#777777' }
331 | }
332 | ];
333 |
334 | actions.forEach(action => store.dispatch(action));
335 |
336 | const actual = store.getState();
337 | const expected = {
338 | quotes: [
339 | {
340 | text: 'Whatever you are, be a good one.',
341 | author: 'Abraham Lincoln',
342 | id: 2,
343 | likeCount: 1
344 | }
345 | ],
346 | theme: {
347 | color: '#777777'
348 | }
349 | };
350 | expect(actual).toEqual(expected);
351 | });
352 | });
353 |
354 | ```
355 |
356 | # 12 - The Redux Store - Initial state
357 |
358 | In Redux, reducers must provide an initial/default state.
359 |
360 | ```js
361 | import { store } from './store';
362 | import expect from 'expect';
363 |
364 | describe('store', () => {
365 | it('should initialize', () => {
366 | const actual = store.getState();
367 | const expected = {
368 | quotes: [],
369 | theme: { color: '#ffffff' }
370 | };
371 | expect(actual).toEqual(expected);
372 | });
373 | });
374 | ```
375 |
376 | # 13 - Redux Testing - Redux reducers
377 |
378 | ```js
379 | import expect from 'expect';
380 | import themeReducer from './themeReducer';
381 |
382 | describe('themeReducer', () => {
383 | function stateBefore() {
384 | return {
385 | color: '#ffffff'
386 | };
387 | }
388 |
389 | const action = {
390 | type: 'UPDATE_THEME_COLOR',
391 | payload: { color: '#56ddff' }
392 | }
393 |
394 | it('should change the theme color', () => {
395 | const actual = themeReducer(stateBefore(), action)
396 |
397 | const actual = store.getState();
398 | const expected = { color: '#56ddff' }
399 | expect(actual).toEqual(expected);
400 | });
401 | });
402 |
403 | ```
404 |
405 | The use of `stateBefore` as a function is overkill here since it's just a single action. Let's try another reducer to go a bit further:
406 |
407 |
408 | ```js
409 | import expect from 'expect';
410 | import quoteReducer from './quoteReducer';
411 |
412 | describe('quoteReducer', () => {
413 | function stateBefore() {
414 | return [
415 | {
416 | text: 'Lorem ipsum',
417 | author: 'Jane Doe',
418 | id: 1,
419 | likeCount: 7
420 | },
421 | {
422 | text: 'Ullamco laboris nisi ut aliquip',
423 | author: 'John Smith',
424 | id: 2,
425 | likeCount: 0
426 | },
427 | ]
428 | };
429 |
430 |
431 |
432 | it('should add quotes by id', () => {
433 | const action = {
434 | type: 'ADD_QUOTE_BY_ID',
435 | payload: {
436 | text: 'This is a new quote',
437 | author: 'Someone awesome',
438 | id: 3,
439 | likeCount: 0
440 | }
441 | }
442 |
443 | const actual = quoteReducer(stateBefore(), action)
444 | const expected = [
445 | {
446 | text: 'Lorem ipsum',
447 | author: 'Jane Doe',
448 | id: 1,
449 | likeCount: 7
450 | },
451 | {
452 | text: 'Ullamco laboris nisi ut aliquip',
453 | author: 'John Smith',
454 | id: 2,
455 | likeCount: 0
456 | },
457 | {
458 | text: 'This is a new quote',
459 | author: 'Someone awesome',
460 | id: 3,
461 | likeCount: 0
462 | }
463 | ];
464 | expect(actual).toEqual(expected);
465 | });
466 |
467 | it('should return prev state when trying to make likeCount negative', () => {
468 | const action = {
469 | type: 'UNLIKE_QUOTE_BY_ID',
470 | payload: { id: 2 }
471 | };
472 | const actual = quoteReducer(stateBefore(), action);
473 | const expected = stateBefore();
474 | expect(actual).toEqual(expected);
475 | });
476 | });
477 |
478 | ```
479 |
--------------------------------------------------------------------------------