├── .flowconfig ├── .gitignore ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── flow-typed └── npm │ ├── jest_v20.x.x.js │ ├── react-redux_v4.x.x.js │ └── redux_v3.x.x.js ├── images ├── create-example-next.png ├── delete-example-next.png ├── interact-example.png ├── read-1-example.png ├── read-2-example-descending.png ├── read-3-example-filter.png ├── testing-with-jest-at-ims.pdf ├── update-example-updating.png ├── view-example-ascending.png └── whimper.png ├── package.json ├── public ├── favicon.ico └── index.html ├── scripts ├── build.js ├── start.js └── test.js ├── src ├── actions.js ├── components │ ├── App.css │ ├── App.js │ ├── Header.css │ ├── Header.js │ ├── Table.css │ ├── Table.js │ ├── TableHead.js │ ├── TableRow.js │ ├── __test0__ │ │ ├── Table-C.test.js │ │ ├── Table-D.test.js │ │ ├── Table-U.test.js │ │ ├── Table-V.test.js │ │ ├── TableHead-R.test.js │ │ └── TableHead-U.test.js │ └── __tests__ │ │ ├── App.test.js │ │ ├── Table-C.test.js │ │ ├── Table-D.test.js │ │ ├── Table-R.test.js │ │ ├── Table-U.test.js │ │ ├── Table-V.test.js │ │ ├── TableHead-I.test.js │ │ ├── TableHead-R.test.js │ │ ├── TableHead-U.test.js │ │ ├── TableRow-R.test.js │ │ ├── __snapshots__ │ │ ├── TableHead-R.test.js.snap │ │ └── TableRow-R.test.js.snap │ │ └── shallow.test.js ├── index.css ├── index.js ├── reducers │ ├── __tests__ │ │ ├── fields.test.js │ │ ├── records.test.js │ │ └── view.test.js │ ├── fields.js │ ├── index.js │ ├── records.js │ └── view.js ├── storage.js ├── testing │ ├── react-test-renderer │ │ ├── index.js │ │ └── relevantTestObject.js │ ├── records-data.js │ └── selectors.js └── types.js └── yarn.lock /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Example of purposeful testing with Jest 🃏 2 | 3 | The **whimper** app is a parody of Twitter that I adapted from Whinepad in *React Up & Running* by Stoyan Stefanov. 4 | 5 | ![whimper displays a table with 7 rows and 4 columns](images/whimper.png) 6 | 7 | ## Let’s start with the ~~bug~~ big picture 8 | 9 | > Dave Ceddia: The main reason to write tests is to ensure that your app works the way it should. Test the **high-value** features. You click an “Add to Cart” button. The app had better add that item to the cart. 10 | 11 | [https://daveceddia.com/what-to-test-in-react-app/](https://daveceddia.com/what-to-test-in-react-app/) 12 | 13 | > Marcin Grzywaczewski: When you test React components: Given properties and state, what **structure**? Behavior or **interaction**: is there a possibility to transition from state A to state B? 14 | 15 | [http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/](http://reactkungfu.com/2015/07/approaches-to-testing-react-components-an-overview/) 16 | 17 | > Stephen Scott: Writing tests defines your component’s **contract**. From an outsider’s perspective, is this detail important? Don’t duplicate the application code. 18 | 19 | [https://medium.com/@suchipi/the-right-way-to-test-react-components-548a4736ab22](https://medium.com/@suchipi/the-right-way-to-test-react-components-548a4736ab22) 20 | 21 | ### 4C to communicate when you write a test 22 | 23 | * **C**orrect: what does it assert about the contract? will it be practical to keep test correct when code changes? 24 | * **C**lear: where to fix code when test fails 25 | * **C**omplete: fewer tests that fit your priorities for quality-scope-cost are better than more tests that don’t 26 | * **C**onsistent: apply patterns for 20% of operations which occur 80% of the time 27 | 28 | ### Communicate test criteria like design decisions 29 | 30 | Analogies from *Articulating Design Decisions* by Tom Greever: 31 | 32 | > The most important thing you could ask…the very first thing you should always ask is, “What are we trying to **communicate**?” 33 | 34 | > Become a great designer by **describing** your designs to other people in a way that makes sense to them. s/design/**test**/ 35 | 36 | > Our designs do not speak for themselves. s/design/**snapshot test**/ 37 | 38 | ### Zero configuration testing with Jest 39 | 40 | [Jest](http://facebook.github.io/jest/) is already configured if you create a project with: 41 | 42 | * [create-react-native-app](https://github.com/react-community/create-react-native-app) or [react-native init](facebook.github.io/react-native/docs/getting-started.html) 43 | * [create-react-app](https://github.com/facebookincubator/create-react-app) 44 | Sometimes we eject this project from `create-react-app` and upgrade Jest to use a critical new feature. 45 | When `react-scripts` catches up with Jest, we [recreate an unejected config](https://speakerdeck.com/pedrottimark/uneject-and-recreate-reactjs-charlotte). 46 | 47 | ### Only a few other devDependencies 48 | 49 | * react-dom/test-utils is peer dependency for enzyme 50 | * [enzyme](http://airbnb.io/enzyme/) returns a wrapper, as in jQuery 51 | * [mount](http://airbnb.io/enzyme/docs/api/mount.html) renders component to **maximum** depth in simulated DOM. It contains only DOM nodes, no React components. 52 | * [shallow](http://airbnb.io/enzyme/docs/api/shallow.html) renders component to **minimum** depth, to test it independent of how children are implemented. It might contain DOM nodes, but any children which are React components are leaves of the tree. 53 | * [enzyme-to-json](https://github.com/adriantoine/enzyme-to-json) converts enzyme wrapper to test object compatible with `toMatchSnapshot` assertion 54 | * react-test-renderer renders component as test object compatible with `toMatchSnapshot` assertion 55 | 56 | ## Let’s move to a smaller picture 57 | 58 | Beware of “one size fits all” 59 | 60 | * “Use shallow rendering!” 61 | * “Use snapshot testing!” 62 | 63 | Instead, select tools to fit your **goals**. 64 | 65 | | Patterns for operations | Examples of tests | 66 | |-------------------------|------------------:| 67 | | **R**ead or **r**ender | TableHead-**R**.test.js | 68 | | **I**nteract | TableHead-**I**.test.js | 69 | | **C**reate | Table-**C**.test.js | 70 | | **D**elete | Table-**D**.test.js | 71 | | **V**iew | Table-**V**.test.js | 72 | | **U**pdate or **u**ndo | Table-**U**.test.js | 73 | 74 | ### Read or render, part 1 75 | 76 | Given combinations of **props** and **state** as input, the component renders correct output: 77 | 78 | * what people “see” including accessibility 79 | * what child components receive as props 80 | 81 | This first example contrasts two methods. 82 | 83 | * Baseline: typical “abstract” assertions 84 | 85 | * simulate child components or DOM nodes 86 | * traverse by selector 87 | * assert each expected value, but it can be **harder to see** too many criteria 88 | 89 | * Proposed: the `toMatchSnapshot` assertion matches props and descendants in “descriptive” JSX 90 | 91 | A few snapshots which control changes to a component do more good than harm, 92 | * because it’s **easier to see** descriptive criteria, 93 | * if you know that’s their goal, 94 | * from the name of the test file. For example, TableHead-**R**.test.js 95 | 96 | Example: **table head** renders button, count, and fields 97 | 98 | ![table head has + button in first row; count and fields in second row](images/read-1-example.png) 99 | 100 | * Baseline: […/pre-sort/src/components/\_\_test0\_\_/TableHead-R.test.js](https://github.com/pedrottimark/whimper/blob/pre-sort/src/components/__test0__/TableHead-R.test.js) 101 | * Proposed: […/pre-sort/src/components/\_\_tests\_\_/TableHead-R.test.js](https://github.com/pedrottimark/whimper/blob/pre-sort/src/components/__tests__/TableHead-R.test.js) 102 | 103 | ```js 104 | import React from 'react'; 105 | import renderer from 'react-test-renderer'; 106 | 107 | import TableHead from '../TableHead'; 108 | 109 | describe('TableHead', () => { 110 | it('renders button, count, and fields', () => { 111 | expect(renderer.create( 112 | {}} 114 | count={7} 115 | fields={fields} 116 | /> 117 | ).toJSON()).toMatchSnapshot(); 118 | }); 119 | }); 120 | ``` 121 | 122 | ### Updating tests, part 1 123 | 124 | Example: **sort rows** by fields 125 | 126 | ![second row has down-arrow at the right of the first of three fields](images/read-2-example-descending.png) 127 | 128 | To click a column heading and display an indicator, add to `th` elements: 129 | 130 | * `onClick` property 131 | * `span` and `abbr` children 132 | 133 | ```sh 134 | git checkout diff-sort 135 | 136 | npm test -- TableHead-R 137 | u 138 | q 139 | 140 | git checkout -- src/components/__tests__/__snapshots__/TableHead-R.test.js.snap 141 | git checkout master 142 | ``` 143 | 144 | ### Interlude about snapshot testing 145 | 146 | **Painless** snapshot testing, to control changes in components: 147 | 148 | * Prevent unexpected **regression**. If change is incorrect, then fix code. 149 | * Confirm expected **progress**. If change is correct, then update snapshot. 150 | 151 | **Painful** snapshot testing, if you let the effort get out of balance: 152 | 153 | * Too easy to write a test, which you do once. 154 | * Too hard to understand if it fails, unhappily ever after. 155 | * Which changes are correct or incorrect? 156 | * Overlook a change that should be, but isn’t? 157 | 158 | > Adrien Antoine: The danger of #Jest snapshot testing is **overusing** it, there would be **so much diff** for each code change that you wouldn’t see the actual bug 159 | 160 | > Christoph Pojer: I totally agree. We will need to evolve **patterns** over time and figure out the best **balance**. 161 | 162 | [https://twitter.com/cpojer/status/774427994077048832](https://twitter.com/cpojer/status/774427994077048832) 163 | 164 | > Stephen Scott: A snapshot test does not tell you your code **broke**, only that it **changed**. It is easier to explain exactly which pieces you care about with the imperative approach, but I would love to see **tooling** change that opinion. 165 | 166 | [https://medium.com/@suchipi/thanks-for-your-response-e8e9217db08f](https://medium.com/@suchipi/thanks-for-your-response-e8e9217db08f) 167 | 168 | **Purposeful** testing, when you design a test, minimize: 169 | 170 | * irrelevant details, which cause 171 | * unnecessary updates, which risk 172 | * incorrect decisions, especially 173 | * false negatives, failing to report an error 174 | 175 | The rest of examples replace `toMatchSnapshot` with `toMatchObject` to match a **relevant subset** of props and descendants in **descriptive** JSX. 176 | 177 | | How do you get the relevant JSX? | When | 178 | |:---------------------------------|-----:| 179 | | Type it, before you implement render method | TDD or non-TDD | 180 | | Copy from existing Read snapshot, and delete whatever is irrelevant | TDD or non-TDD | 181 | | Copy from temporary snapshot, and delete… | non-TDD | 182 | | *Maybe someday*, paste by editor integration, and delete… | non-TDD | 183 | 184 | #### Import from `enzyme-to-json` 1.6.0 or later 185 | 186 | * `mountToDeepJson` Given an enzyme `mount` wrapper, especially from selector traversal, return a test object rendered to **maximum** depth. It has only DOM nodes, no React components. Realistic interaction for descendant structure compatible with `react-test-renderer`. 187 | * `mountToShallowJson` Given an enzyme `mount` wrapper, especially from selector traversal, return a test object rendered to **minimum** depth. It might contain DOM nodes, but any children which are React components are leaves of the tree. New deep-event/shallow-test pattern balances realistic interaction with limited structure. 188 | 189 | #### Proposed to import from `react-test-renderer` 190 | 191 | * `irrelevant` String sentinel value to **ignore** children, or absence of children, in received object 192 | * `relevantTestObject` Given a React element, return the **unrendered** element as a test object: 193 | * `$$typeof` is enumerable 194 | * `props` is omitted if there are no properties, not including `children` 195 | * `children` is omitted if it is `[irrelevant]` 196 | 197 | #### Why does `relevantTestObject` omit `props` when it’s empty? 198 | 199 | Suppose the expected value of `th` omits `scope="col"` as an irrelevant detail in a `toMatchObject` assertion, because snapshots already protect against regressions in accessibility. 200 | 201 | And then, you add markup to `th` as illustrated in the next section **Read or render, part 2** 202 | 203 | If the test object for `th` has an empty `props` object, Jest diff displays a detail that you omitted: 204 | 205 | ```diff 206 | - 207 | - when 208 | + 211 | + 212 | + when 213 | + 214 | + 217 | + ↓ 218 | + 219 | 220 | ``` 221 | 222 | If the test object for `th` omits `props` because it’s empty, Jest diff displays the change more clearly: 223 | 224 | ```diff 225 | 226 | - when 227 | + 228 | + when 229 | + 230 | + 233 | + ↓ 234 | + 235 | 236 | ``` 237 | 238 | If Jest understood indentation better, its diff could display the change even more clearly: 239 | 240 | ```diff 241 | 242 | + 243 | when 244 | + 245 | + 248 | + ↓ 249 | + 250 | 251 | ``` 252 | 253 | #### Why does `relevantTestObject` let you omit `irrelevant` children? 254 | 255 | Suppose you replace ordinary plus sign + with heavy plus sign ➕ as text in the `button` to add a row. 256 | 257 | * To confirm expected progress, you need to update snapshots for `TableHead`. 258 | * Because a change to that button is an irrelevant detail in `toMatchObject` assertions about sorting indicators illustrated in the next section, you will see `{irrelevant}` for the first heading cell. 259 | 260 | ### Read or render, part 2 261 | 262 | Example: table head renders ascending or descending **indicator** only in heading of primary sort field 263 | 264 | ![second row has down-arrow at the right of the first of three fields](images/read-2-example-descending.png) 265 | 266 | * Baseline: […/pre-filter/src/components/\_\_test0\_\_/TableHead-R.test.js](https://github.com/pedrottimark/whimper/blob/pre-filter/src/components/__test0__/TableHead-R.test.js) 267 | * Proposed: […/pre-filter/src/components/\_\_tests\_\_/TableHead-R.test.js](https://github.com/pedrottimark/whimper/blob/pre-filter/src/components/__tests__/TableHead-R.test.js) 268 | 269 | ```js 270 | import React from 'react'; 271 | import renderer, { 272 | irrelevant, 273 | relevantTestObject, 274 | } from 'react-test-renderer'; // proposed 275 | 276 | import TableHead, {ascending, descending} from '../TableHead'; 277 | 278 | describe('TableHead', () => { 279 | it('renders descending indicator in `when` heading', () => { 280 | expect(renderer.create( 281 | {}} 283 | count={7} 284 | fields={fields} 285 | sortRecords={() => {}} 286 | view={Object.assign({}, viewInitial, {sorting: [ 287 | {fieldKey: 'when', descending: true}, 288 | {fieldKey: 'what', descending: false}, 289 | ]})} 290 | /> 291 | ).toJSON().children[1]).toMatchObject(relevantTestObject( 292 | 293 | {irrelevant} 294 | 295 | {irrelevant} 296 | {descending} 297 | 298 | 299 | {irrelevant} 300 | {''} 301 | 302 | 303 | {irrelevant} 304 | {''} 305 | 306 | 307 | )); 308 | }); 309 | 310 | // and so on 311 | }); 312 | ``` 313 | 314 | ### Updating tests, part 2 315 | 316 | Example: **filter rows** by substring 317 | 318 | To type a substring, add `input` element at right of first `tr`. 319 | 320 | ![first row has input at right to type filter substring](images/read-3-example-filter.png) 321 | 322 | * Because the baseline assertion passes, you must **remember** to update it. 323 | * Because one proposed snapshot fails, you must **decide** to update it. 324 | 325 | ```sh 326 | git checkout diff-filter 327 | 328 | npm test -- TableHead-R 329 | u 330 | q 331 | 332 | git checkout -- src/components/__tests__/__snapshots__/TableHead-R.test.js.snap 333 | git checkout master 334 | ``` 335 | 336 | ### Interact 337 | 338 | If components render simple views of data, or if you don’t have time to apply other patterns, you might test only 339 | 340 | * **R**ead or **r**ender 341 | * **I**nteract: interface **events** cause correct **actions** 342 | 343 | `jest.fn()` returns a mock function, also known as a spy, to assert **behavior** of calling code, not just output. 344 | 345 | Example: **click cells** in table head 346 | 347 | ![table head has five clickable cells in two rows](images/interact-example.png) 348 | 349 | * Baseline is same as Proposed: […/master/src/components/\_\_tests\_\_/TableHead-I.test.js](https://github.com/pedrottimark/whimper/blob/master/src/components/__tests__/TableHead-I.test.js) 350 | 351 | ```js 352 | import React from 'react'; 353 | import {mount} from 'enzyme'; 354 | 355 | import TableHead from '../TableHead'; 356 | 357 | describe('TableHead', () => { 358 | const addRow = jest.fn(); 359 | const sortRecords = jest.fn(); 360 | const $it = mount( 361 | {}} 366 | sortRecords={sortRecords} 367 | view={viewInitial} 368 | /> 369 | ); 370 | 371 | // Click every cell in table head. 372 | $it.find('thead tr').forEach($tr => { 373 | $tr.find('th').forEach($th => { 374 | $th.simulate('click'); 375 | }); 376 | }); 377 | 378 | // Interface events cause correct actions. 379 | 380 | it('adds a row', () => { 381 | expect(addRow).toHaveBeenCalledTimes(1); 382 | expect(addRow).toHaveBeenCalledWith(); 383 | }); 384 | 385 | it('sorts rows', () => { 386 | // [] from click non-field heading at left to reset sort order. 387 | expect(sortRecords.mock.calls).toEqual([[]].concat( 388 | fields.map(({key}) => [key]) 389 | )); 390 | }) 391 | }); 392 | ``` 393 | 394 | ### Create 395 | 396 | An action **adds a child** to a component. 397 | 398 | * **where**: add to correct place in siblings 399 | * **what**: delegate details about children to their **r**ender tests 400 | * **what else**: update the (derived) state? 401 | 402 | Example: **add row** to table body 403 | 404 | ![table body has a new row preceding one existing row](images/create-example-next.png) 405 | 406 | * Baseline: […/master/src/components/\_\_test0\_\_/Table-C.test.js](https://github.com/pedrottimark/whimper/blob/master/src/components/__test0__/Table-C.test.js) 407 | * Proposed: […/master/src/components/\_\_tests\_\_/Table-C.test.js](https://github.com/pedrottimark/whimper/blob/master/src/components/__tests__/Table-C.test.js) 408 | 409 | ```js 410 | import React from 'react'; 411 | import {mount} from 'enzyme'; 412 | import {relevantTestObject} from 'react-test-renderer'; // proposed 413 | import { 414 | clickAdd, 415 | countRecords, 416 | tbodyShallow, 417 | } from '../../testing/selectors'; 418 | 419 | import Table from '../Table'; 420 | const TableRow = () => {}; // mock, and provide only relevant props 421 | 422 | describe('Table', () => 423 | it('creates a row preceding one existing row', () => { 424 | const store = createStore(reducer); 425 | const records = [recordB]; 426 | store.dispatch(receiveData(fields, records)); 427 | const $it = mount( 428 | 429 | 430 | 431 | ); 432 | 433 | clickAdd($it); 434 | expect(countRows($it)).toBe(records.length + 1); 435 | expect(tbodyShallow($it)).toMatchObject(relevantTestObject( 436 | 437 | 438 | 439 | 440 | )); 441 | }); 442 | 443 | // and so on 444 | }); 445 | ``` 446 | 447 | The `src/testing/selectors.js` file encapsulates traversal in non-snapshot tests to minimize change if there is a change to structure of markup that a component renders. 448 | 449 | The goal is similar to **selectors** for state of a Redux store. 450 | 451 | ```js 452 | import {mountToShallowJson} from 'enzyme-to-json'; 453 | 454 | const clickAdd = ($it) => { 455 | $it.find('thead tr').at(0).find('th').at(0).simulate('click'); 456 | }; 457 | 458 | const countRows = ($it) => 459 | $table.find('TableHead').prop('count'); 460 | // Thanks to Patrick A. for suggesting the preceding shallow traversal, 461 | // to replace the following deep traversal which I had written: 462 | // Number($table.find('thead tr').at(1).find('th').at(0).text()); 463 | 464 | const tbodyShallow = ($it) => 465 | mountToShallowJson($it.find('tbody')); 466 | ``` 467 | 468 | `countRows` is in a typical expected value assertion. I use them when they fit my goal :) 469 | 470 | ### Delete 471 | 472 | An action **removes a child** from a component. 473 | 474 | * **where**: remove from correct place in siblings 475 | * **what**: delegate details about children to their **r**ender tests 476 | * **what else**: update the (derived) state? 477 | 478 | Example: **delete row** from table body 479 | 480 | ![table body has three rows](images/delete-example-next.png) 481 | 482 | * Baseline: […/master/src/components/\_\_test0\_\_/Table-D.test.js](https://github.com/pedrottimark/whimper/blob/master/src/components/__test0__/Table-D.test.js) 483 | * Proposed: […/master/src/components/\_\_tests\_\_/Table-D.test.js](https://github.com/pedrottimark/whimper/blob/master/src/components/__tests__/Table-D.test.js) 484 | 485 | ```js 486 | // Delete is similar to Create 487 | 488 | describe('Table deletes records', () => { 489 | // initialize $it 490 | 491 | test('in the middle', () => { 492 | clickDelete($it, 1); // recordB 493 | expect(countRows($it)).toEqual(records.length - 1); 494 | expect(tbodyShallow($it)).toMatchObject(relevantTestObject( 495 | 496 | 497 | 498 | 499 | 500 | )); 501 | }); 502 | 503 | // and so on 504 | }); 505 | ``` 506 | 507 | ### View 508 | 509 | An action **changes derived state** of a component. Apply other patterns: 510 | 511 | * **C**reate: filter to “add” children 512 | * **D**elete: filter to “remove” children 513 | * **C**reate and **D**elete: sort to reorder children 514 | * **U**pdate: indicate state in user interface 515 | 516 | Example: **sort rows** by fields 517 | 518 | ![table body has four rows sorted ascending by middle field](images/view-example-ascending.png) 519 | 520 | * Baseline: […/master/src/components/\_\_test0\_\_/Table-V.test.js](https://github.com/pedrottimark/whimper/blob/master/src/components/__test0__/Table-V.test.js) 521 | * Proposed: […/master/src/components/\_\_tests\_\_/Table-V.test.js](https://github.com/pedrottimark/whimper/blob/master/src/components/__tests__/Table-V.test.js) 522 | 523 | ```js 524 | // View is similar to Create and Delete 525 | 526 | describe('Table sorting', () => { 527 | // initialize $it 528 | 529 | it('is ascending on click `what` heading', () => { 530 | clickHeading($it, 1); 531 | expect(tbodyShallow($it)).toMatchObject(relevantTestObject( 532 | 533 | 534 | 535 | 536 | 537 | 538 | )); 539 | }); 540 | 541 | // and so on 542 | }); 543 | ``` 544 | 545 | ### Update or undo 546 | 547 | An action **changes the state** of a component. 548 | 549 | Assert relevant attributes, content, or structure: 550 | 551 | * **prev** state: before the action 552 | * **next** state: after the action 553 | * **prev** state: undo the action 554 | 555 | Example: **input or edit text** in table cell 556 | 557 | ![table cell in middle field of third row has tan background color](images/update-example-updating.png) 558 | 559 | * Baseline: […/master/src/components/\_\_test0\_\_/Table-U.test.js](https://github.com/pedrottimark/whimper/blob/master/src/components/__test0__/Table-U.test.js) 560 | * Proposed: […/master/src/components/\_\_tests\_\_/Table-U.test.js](https://github.com/pedrottimark/whimper/blob/master/src/components/__tests__/Table-U.test.js) 561 | 562 | ```js 563 | import React from 'react'; 564 | import {mount} from 'enzyme'; 565 | import {mountToDeepJson} from 'enzyme-to-json'; 566 | import {relevantTestObject} from 'react-test-renderer'; // proposed 567 | 568 | import Table from '../Table'; 569 | 570 | describe('Table', () => { 571 | it('updates a text field', () => { 572 | // initialize $td 573 | 574 | $td.simulate('doubleClick'); 575 | const textPrev = records[rowIndex][fields[fieldIndex].key]; 576 | expect(mountToDeepJson($td)).toMatchObject(relevantTestObject( 577 | 588 | )); 589 | 590 | const textNext = 'ECMAScript 2015'; 591 | $td.find('input').get(0).value = textNext; 592 | 593 | $td.find('form').simulate('submit'); 594 | expect(mountToDeepJson($td)).toMatchObject(relevantTestObject( 595 | 596 | )); 597 | }); 598 | }); 599 | ``` 600 | 601 | ## Conclusion 602 | 603 | * Baseline: add as many abstract assertions as you can? 604 | * Proposed: **delete** as many **irrelevant** details as you can! 605 | 606 | > Il semble que la perfection soit atteinte non quand il n'y a plus rien à ajouter, mais quand il n'y a plus rien à **retrancher**. 607 | 608 | > It seems that perfection is attained not when there is nothing more to add, but when there is nothing more to **remove**. 609 | 610 | — Antoine de Saint Exupéry 611 | 612 | > detect and fix any problem…at the lowest-value stage possible…at the unit test of the pieces…rather than in the test of the final product itself 613 | 614 | — Andrew S. Grove in *High Output Management* 615 | 616 | > testing pyramid in 2016 617 | 618 | [https://twitter.com/abramov_dmitrii/status/805913874704674816](https://twitter.com/abramov_dmitrii/status/805913874704674816) 619 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce((env, key) => { 82 | env[key] = JSON.stringify(raw[key]); 83 | return env; 84 | }, {}), 85 | }; 86 | 87 | return { raw, stringified }; 88 | } 89 | 90 | module.exports = getClientEnvironment; 91 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right
578 |
579 | {textPrev} 580 |
581 | 585 |
586 |
587 |
{textNext}