├── .gitignore ├── LICENSE.md ├── README.md ├── unit-testing-jest-enzyme ├── .gitignore ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── style │ │ └── bootstrap.min.css └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── actions │ ├── index.js │ ├── index.test.js │ └── types.js │ ├── components │ ├── comment_box.js │ ├── comment_box.test.js │ ├── comment_list.js │ └── comment_list.test.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reducers │ ├── comments.js │ ├── comments.test.js │ └── index.js │ └── registerServiceWorker.js └── unit-testing-mocha-chai ├── .babelrc ├── .gitignore ├── favicon.ico ├── index.html ├── logo.svg ├── manifest.json ├── package-lock.json ├── package.json ├── src ├── actions │ ├── index.js │ └── types.js ├── components │ ├── app.js │ ├── comment_box.js │ └── comment_list.js ├── index.js ├── reducers │ ├── comments.js │ └── index.js └── registerServiceWorker.js ├── style ├── bootstrap.min.css └── style.css ├── test ├── actions │ └── index_test.js ├── components │ ├── app_test.js │ ├── comment_box_test.js │ └── comment_list_test.js ├── reducers │ └── comments_test.js └── test_helper.js ├── webpack.config.dev.js └── webpack.config.prod.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .temp 3 | .idea 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Holger Kraatz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sorry !! Gosh ... 4 years went by. 2 | I hope to find time soon to update this tutorial. 3 | 4 | # React Unit Testing 5 | 6 | This tutorial shall help setting up Unit Tests for your React/Redux Application. 7 | 8 | We will do Unit Testing on React Components and the Redux State within these 2 Stacks: 9 | 10 | 1. Mocha/Chai (our own Stack) 11 | 12 | 2. Jest/Enzyme (Create React App Starter Kit: Jest is included) 13 | 14 | 15 | *Notes:* 16 | - Unit Tests on Redux: 17 | 18 | - Most Redux code are functions anyway, so you don't need to mock anything. 19 | - Test your Action Creators, Reducers, and the resulting application state by passing test values/no values/wrong values into/through them. 20 | - Docs: https://redux.js.org/recipes/writing-tests 21 | 22 | - Unit Tests on React Components: 23 | - Should focus on bullet-proof answers of these questions: 24 | - is component's content (hierarchy) rendered correctly (findable in DOM) and contains ALL necessary children? 25 | - is component's content really NOT rendered if I wanted to hide it? 26 | - have passed Props the right impact? 27 | - are passed Props forwarded down to the component's child if the child is the recipient of the Props? 28 | - is the component's state (NOT application state! This is Redux!) updated correctly and holds the right state after an event/passed Props? 29 | - are CSS classes/selectors attached to the component? 30 | - is Dynamic Styling working as expected? 31 | - do lifecycle events work correctly, like componentDidMount, componentWillUnmount? 32 | - and surely some more 33 | 34 | - Should NOT focus on the business logic of your React Components as business logic should be pulled out of your React Components. 35 | - Jest Snapshots can save time by just comparing the rendered Outputs of Test A (actual test) and Test B (test which ran immediately before): 36 | - Jest will "complain/inform" if the snapshot differs from the previous test, so you will be reminded to doublecheck the differences and, if you did a change on purpose, you can run jest with the -u flag (to update the previous shapshot with the actual one). 37 | - Installation: `npm install --save-dev react-test-renderer` 38 | - Links: 39 | - https://reactjs.org/docs/test-renderer.html 40 | - https://github.com/facebook/react/tree/master/packages/react-test-renderer 41 | 42 | 43 | - Testing Interactions between Redux and React Components: 44 | 45 | - This is already an Integration Test, but: 46 | - We will cover it shortly below: [Integration Tests between React Component and the Redux State](#chapter3) :-) 47 | 48 | ### IMPORTANT CAUTION: 49 | The npm packages might have discovered vulnerabilities by now. 50 | So do not use the combination of our npm versions in your production environment, but within a test environment instead that is sandboxed from your production network. 51 | Unfortunately the author has no time right now to always keep the npm packages at their latest versions and ensure that their combination still plays successfully together. The tuturial is supposed to show how it works and should help building working prototypes to make life easier for you. 52 | 53 | ## Table of Contents 54 | 55 | 1. [Getting started](#chapter1) 56 | 2. [Two Test Stacks](#chapter2) 57 | 1. [Unit Testing with Stack `Webpack`, `Babel`, `Mocha`, `Chai`](#chapter2a) 58 | 1. [Unit Testing React Components](#chapter2a1) 59 | 2. [Unit Testing the Redux State](#chapter2a2) 60 | 2. [Unit Testing with `Create React App` Starter Kit (Integrated: `Webpack`, `Babel`, `Jest`) plus `Enzyme`](#chapter2b) 61 | 1. [Unit Testing React Components](#chapter2b1) 62 | 2. [Unit Testing the Redux State](#chapter2b2) 63 | 3. [Integration Tests between React Component and the Redux State](#chapter3) 64 | 4. [Links](#chapter4) 65 | 66 | 67 | ## 1. Getting started 68 | 69 | ### Testing in general 70 | 71 | Besides Unit Tests, there are Integration Tests, End-to-end Tests (E2E), and User Acceptance Tests. 72 | These are the main tests in software development. If you want to check out other tests, go here: https://en.wikipedia.org/wiki/Software_testing 73 | 74 | This is the usual order of testing BEFORE delivering a new software or software version to the client: 75 | 76 | 1. Unit Tests: 77 | - A single piece of code (usually a function or a class/object) is tested, separate from other pieces. 78 | - Unit Tests are very helpful if you need to change your code: If your set of Unit Tests verify that your code works, you can safely change your code and have confidence that the other parts of your program will still work as expected. 79 | - Unit Tests wording can be used for documentation. 80 | - Save time because you don't have to always repeat manual testing after working on your code. 81 | - Examples (in general, not React specific): 82 | - Testing a function without passing any argument 83 | - Testing a function by passing a wrong argument type 84 | - Passing the correct argument type to a function: Testing if the outcome is valid 85 | - Testing if function returns a valid JSON structure 86 | 87 | TDD: TDD (Test-Driven Development / or Design) was developed out of Unit Testing: 88 | - Some quotes: 89 | - "Keep it simple, stupid!" (KISS) 90 | - "You aren't gonna need it!" (YAGNI) 91 | - You can better focus on a demanded requirement by writing very specific test cases first. This way you are forced to work and concentrate on the demanded requirement only, NOT on functionality that is NOT proven to meet the requirement or was never demanded by the client (see MVP: https://en.wikipedia.org/wiki/Minimum_viable_product). 92 | - By focusing on writing only the code necessary to pass the tests, designs can often be cleaner and clearer than it is achieved by other methods. 93 | - TDD Cycle: 94 | 1. Add a test 95 | 2. Run all tests and new test will fail as code not written yet: This way we check if the new test really works 96 | 3. Write the code 97 | 4. Run tests: If all test cases pass, go to next step 98 | 5. Refactor code: Clean up 99 | - Critics: 100 | - The difficult thing about TDD for many developers is that you have to write tests BEFORE writing any code. 101 | - If you're the client and the developer in one person, and you're not quite sure yet how your software should convince and appear to users, better focus on options, brainstorming, and branding than on writing test cases. 102 | 103 | BDD: BDD (Behaviour-Driven Development / or Design) was developed out of TDD: 104 | - Sets focus on User Stories and their scenarios/events. 105 | - Sets more focus on the wording for better understanding and documentation. 106 | - Therefore makes it easier to show test results to the business side to make the client happy - and yourself. 107 | 108 | 2. Integration Tests: 109 | - Not a single function like above, but logical groups (e.g. a login component, or a Service/API to handle authentication) are tested to see, if they (still) work together successfully. 110 | - To simulate user actions in Node.js, NOT needing a real browser, we will use jsdom module, which is a great DOM implementation in Node.js and gives us a "fake" DOM entirely in JavaScript to run our automated tests against. 111 | - Examples (Apps in general, not React specific): 112 | - Testing if a login component running on a browser connects to a Service/API running on a server successfully. 113 | - Testing if a Service/API returns data from a database successfully. 114 | - Testing if a message component running in a progressive web app returns a success message from the database after writing to it. 115 | 116 | 3. End-to-end Tests (E2E): 117 | - End-to-end Test make sure that ALL logical groups of an application and all Third-Party Software work together as expected. 118 | - The application should be tested with production data under real-time (stress) conditions against the test setup in order to do check performance as well. 119 | - The name End-to-end means that the communication and data transfer of the application works as expected from the one end (the client interface) throughout the other end (the database or another client interface) and vice versa. 120 | - Also called System Test 121 | - Examples (Apps in general, not React specific): 122 | - Testing ALL Use Cases/User Stories: 123 | - User signs up, creating an account and getting Welcome message on screen / receiving an email to confirm. 124 | - User signs in, getting Welcome Back message and New Offers on screen. 125 | - User logs out, getting Good-Bye message. 126 | - User adds item to shopping cart to prepare checkout and to continue shopping. 127 | - User checks out. 128 | - User cancels account. 129 | - ... 130 | - Testing Performance 131 | - Testing Security 132 | 133 | 134 | #### When all tests above are successful: 135 | 136 | 4. User Acceptance Tests: 137 | - The Client/Purchaser/Sponsor is involved. 138 | - Many manual / decision making steps. 139 | - Questions: 140 | - Are all requirements of a specification or contract met? 141 | - Is performance good enough? 142 | - Did QA Department test edge cases manually? 143 | - Did QA Department test security manually? 144 | - Is Software/New Version ready to be released/integrated into IT landscape? 145 | - Regression Tests successfully after integrating New Version into IT landscape? Do all previous features still work as expected? 146 | 147 | 148 | Ok, now let's dive into Unit Testing using BDD. 149 | 150 | ## 2. Two Test Stacks 151 | 152 | We want to show Unit Testing along these two BDD frameworks, but with the same outcome: 153 | 154 | 1. Mocha/Chai 155 | 156 | 2. Jest/Enzyme 157 | 158 | It will of course depend on your stack and your preference which framework to use. 159 | 160 | *Note:* 161 | - For a list of other Unit Test JavaScript frameworks, see https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#JavaScript 162 | 163 | ## i. Unit Testing with Stack `Webpack`, `Babel`, `Mocha`, `Chai` 164 | 165 | ### A Note to Mocha/Chai: 166 | 167 | Mocha is the container in which Chai code is running. 168 | 169 | ### Preparation 170 | 171 | First install all dependencies: 172 | 173 | ``` 174 | > cd unit-testing-mocha-chai 175 | > npm install 176 | ``` 177 | 178 | When done, run the tests: 179 | 180 | ``` 181 | > npm run test 182 | ``` 183 | 184 | The outcome will be: 185 | 186 | ``` 187 | > npm run test 188 | 189 | > testing-with-webpack-and-chai-mocha@1.0.0 test /Users/piano/Desktop/Projects/react-testing/unit-testing-mocha-chai 190 | > mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive ./test 191 | 192 | actions 193 | saveComment 194 | ✓ has the correct type 195 | ✓ has the correct payload 196 | 197 | App 198 | ✓ shows comment box 199 | ✓ shows comment list 200 | 201 | CommentBox 202 | ✓ has the correct class 203 | ✓ has a text area 204 | ✓ has a button 205 | entering some text 206 | ✓ shows that text in the textarea 207 | ✓ when submitted, clears the input 208 | 209 | CommentList 210 | ✓ shows an LI for each comment 211 | ✓ shows each comment that is provided 212 | 213 | Comments Reducer 214 | ✓ handles action with unknown type 215 | ✓ SAVE_COMMENT 216 | 217 | 218 | 13 passing (943ms) 219 | 220 | > 221 | ``` 222 | 223 | To add a watcher to always re-run all tests after a change in code: 224 | 225 | ``` 226 | > npm run test:watch 227 | ``` 228 | 229 | ### a. Unit Testing React Components 230 | 231 | As example we will dive into the simple Unit Test of the App Component and play with it. 232 | 233 | Let's explore `test/components/app_test.js`: 234 | 235 | ``` 236 | import { renderComponent, expect } from '../test_helper'; 237 | import App from '../../src/components/app'; 238 | 239 | // Use 'describe' to group together similar tests 240 | describe('App', () => { 241 | 242 | let component; 243 | 244 | // First create an instance of App 245 | beforeEach(() => { 246 | component = renderComponent(App); 247 | }); 248 | 249 | // Use 'it' to test a single attribute of a target 250 | it('shows comment box', () => { 251 | 252 | // Use 'expect' to make an 'assertion' about a target 253 | expect(component.find('.comment-box')).to.exist; 254 | }); 255 | 256 | it('shows comment list', () =>{ 257 | expect(component.find('.comment-list')).to.exist; 258 | }); 259 | }); 260 | ``` 261 | 262 | 1. First we import the test_helper.js file to enable Mocha/Chai and to render the Component. 263 | 2. We need to also import `app.js` as well, but we leave it WITHOUT CommentList and CommentBox Components ... 264 | 265 | ``` 266 | 267 | 268 | ``` 269 | 270 | ... because we want our test to FAIL initially ! 271 | 272 | ``` 273 | import React, {Component} from 'react'; 274 | 275 | class App extends Component { 276 | render() { 277 | return ( 278 |
279 |
280 | logo 281 |

Welcome to React

282 |
283 |
284 |
285 |
286 | ); 287 | } 288 | } 289 | 290 | export default App; 291 | ``` 292 | 3. Description of Chai commands: 293 | 294 | - The `describe('App', () => {});` command groups together different tests within the same context, for better readability. 295 | - The `beforeEach(() => {})` command is called before EACH test = `it('attribute to test', () => {});` command is executed. 296 | - The `it('shows comment box', () => {});` command groups together the final Unit Test(s). 297 | - The `expect(component.find('.comment-box')).to.exist;` command executes the final Unit Test, testing our assertion that, in this case, a CommentBox exists within our App Component. 298 | - Check out http://chaijs.com/api/bdd for many many things you can test ! 299 | 300 | 301 | 4. Next, we run the test, and the outcome will be: 302 | 303 | ``` 304 | ... 305 | 306 | 11 passing (841ms) 307 | 2 failing 308 | 309 | 1) App 310 | shows comment box: 311 | AssertionError: expected undefined to exist 312 | at Context. (test/components/app_test.js:18:9) 313 | 314 | 2) App 315 | shows comment list: 316 | AssertionError: expected undefined to exist 317 | at Context. (test/components/app_test.js:22:9) 318 | 319 | npm ERR! code ELIFECYCLE 320 | npm ERR! errno 2 321 | npm ERR! testing-with-webpack-and-chai-mocha@1.0.0 test: `mocha --compilers js:babel-core/register --require ./test/test_helper.js --recursive ./test` 322 | npm ERR! Exit status 2 323 | npm ERR! 324 | npm ERR! Failed at the testing-with-webpack-and-chai-mocha@1.0.0 test script. 325 | npm ERR! This is probably not a problem with npm. There is likely additional logging output above. 326 | 327 | npm ERR! A complete log of this run can be found in: 328 | npm ERR! /Users/piano/.npm/_logs/2018-06-30T14_47_30_101Z-debug.log 329 | > 330 | ``` 331 | 332 | 5. Put back CommentList and CommentBox Components in `app.js` and all tests will again pass. 333 | 334 | For more Unit Tests on React Components, see 335 | - the other test files in `test/components` folder and 336 | - this great tutorial: https://medium.freecodecamp.org/the-right-way-to-test-react-components-548a4736ab22 337 | 338 | ### b. Unit Testing the Redux State 339 | 340 | As second example we'll check if our Reducer `src/reducers/comments.js` works correctly. 341 | We do it through test file `test/reducers/comments_test.js`: 342 | 343 | Here are 2 things we want to test: 344 | 345 | - Will reducer return an empty array as new state (an empty array to not to break anything) if we pass it a state of undefined? 346 | - Will action-payload of 'new comment' really return 'new comment' as new state? 347 | 348 | ``` 349 | describe('Comments Reducer', () => { 350 | 351 | // in case there is a weird input, we react with the default (initial) state 352 | it('handles action with unknown type', () => { 353 | 354 | expect(commentReducer(undefined, {})).to.eql([]); 355 | }); 356 | 357 | it('SAVE_COMMENT', () => { 358 | 359 | const action = { type: SAVE_COMMENT, payload: 'new comment'}; 360 | expect(commentReducer([], action)).to.eql(['new comment']); 361 | }); 362 | 363 | }); 364 | ``` 365 | 366 | For more Unit Tests on Redux, see 367 | - the action creator test file `test/actions/index_test.js` 368 | - Example: https://hackernoon.com/low-effort-high-value-integration-tests-in-redux-apps-d3a590bd9fd5 369 | - Docs: https://redux.js.org/recipes/writing-tests 370 | 371 | ## ii. Unit Testing with `Create React App` Starter Kit plus `Enzyme` 372 | 373 | First install all dependencies: 374 | 375 | ``` 376 | > cd unit-testing-jest-enzyme 377 | > npm install 378 | ``` 379 | 380 | *Note:* 381 | `npm install` will install the `Create React App` Starter Kit plus: 382 | 383 | - enzyme 384 | - enzyme-adapter-react-16 385 | - redux-mock-store 386 | 387 | `enzyme` and `enzyme-adapter-react-16` are modules of AirBnb and recommended by Facebook. They are needed to easier mount your React Components, also enabling to connect your Redux Store. 388 | 389 | `redux-mock-store` will be installed to give you the option to use a mock Redux Store, independent of your own Redux Store. This may make sense in some scenarios. 390 | 391 | When done, run the tests: 392 | 393 | ``` 394 | > npm run test 395 | ``` 396 | 397 | Jest will ask you: 398 | 399 | ``` 400 | Press `a` to run all tests, or run Jest with `--watchAll`. 401 | 402 | Watch Usage 403 | › Press a to run all tests. 404 | › Press p to filter by a filename regex pattern. 405 | › Press t to filter by a test name regex pattern. 406 | › Press q to quit watch mode. 407 | › Press Enter to trigger a test run. 408 | ``` 409 | 410 | So press `a` to run all tests, and the outcome will be: 411 | 412 | ``` 413 | PASS src/components/comment_box.test.js 414 | CommentBox 415 | ✓ has the correct class (36ms) 416 | ✓ has a text area (6ms) 417 | ✓ has a button (2ms) 418 | entering some text 419 | ✓ shows that text in the textarea (9ms) 420 | ✓ when submitted, clears the input (6ms) 421 | 422 | PASS src/App.test.js 423 | App 424 | ✓ shows comment box (15ms) 425 | ✓ shows comment list (5ms) 426 | 427 | PASS src/components/comment_list.test.js 428 | CommentList 429 | ✓ shows an LI for each comment (6ms) 430 | ✓ shows each comment that is provided (3ms) 431 | 432 | PASS src/reducers/comments.test.js 433 | Comments Reducer 434 | ✓ handles action with unknown type (2ms) 435 | ✓ SAVE_COMMENT (1ms) 436 | 437 | PASS src/actions/index.test.js 438 | actions 439 | saveComment 440 | ✓ has the correct type (3ms) 441 | ✓ has the correct payload (1ms) 442 | 443 | Test Suites: 5 passed, 5 total 444 | Tests: 13 passed, 13 total 445 | Snapshots: 0 total 446 | Time: 1.616s, estimated 2s 447 | Ran all test suites. 448 | 449 | Watch Usage: Press w to show more. 450 | ``` 451 | 452 | You are in watch mode now, which means that anytime you do a JavaScript code change in any file under root, 453 | the test will automatically re-run. 454 | 455 | If you want to see the overall coverage of files tested vs. NOT tested, add flag `--coverage` to package.json: 456 | 457 | ``` 458 | "test": "react-scripts test --env=jsdom --verbose --coverage" 459 | ``` 460 | 461 | This will create a coverage folder under root. 462 | 463 | The additional output in the Terminal will look like this: 464 | 465 | ``` 466 | ---------------------------|----------|----------|----------|----------|-------------------| 467 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | 468 | ---------------------------|----------|----------|----------|----------|-------------------| 469 | All files | 26.76 | 7.41 | 38.46 | 40.91 | | 470 | src | 1.89 | 0 | 5.88 | 3.7 | | 471 | App.js | 100 | 100 | 100 | 100 | | 472 | index.js | 0 | 0 | 0 | 0 |... 6,7,8,10,12,18 | 473 | registerServiceWorker.js | 0 | 0 | 0 | 0 |... 36,137,138,139 | 474 | src/actions | 100 | 100 | 100 | 100 | | 475 | index.js | 100 | 100 | 100 | 100 | | 476 | types.js | 100 | 100 | 100 | 100 | | 477 | src/components | 100 | 100 | 100 | 100 | | 478 | comment_box.js | 100 | 100 | 100 | 100 | | 479 | comment_list.js | 100 | 100 | 100 | 100 | | 480 | src/reducers | 100 | 100 | 100 | 100 | | 481 | comments.js | 100 | 100 | 100 | 100 | | 482 | index.js | 100 | 100 | 100 | 100 | | 483 | ---------------------------|----------|----------|----------|----------|-------------------| 484 | > 485 | ``` 486 | 487 | ### a. Unit Testing React Components 488 | 489 | As example we will show again the Unit Test of the App Component: `src/App.test.js` 490 | 491 | ``` 492 | import React from 'react'; 493 | import App from './App'; 494 | import { mount } from 'enzyme'; 495 | 496 | // We need to wrap CommentBox with tag in first beforeEach(() => {}) below; 497 | // otherwise we receive this error message: 498 | // Invariant Violation: Could not find “store” in either the context or props of “Connect(CommentBox)” 499 | // https://stackoverflow.com/questions/36211739/invariant-violation-could-not-find-store-in-either-the-context-or-props-of-c 500 | // Also see comment_list.test.js 501 | import {configure} from 'enzyme'; 502 | import Adapter from 'enzyme-adapter-react-16'; 503 | import {Provider} from "react-redux"; 504 | import {createStore, applyMiddleware} from 'redux'; 505 | import reducers from './reducers'; 506 | 507 | configure({adapter: new Adapter()}); 508 | const createStoreWithMiddleware = applyMiddleware()(createStore); 509 | 510 | // Use 'describe' to group together similar tests 511 | describe('App', () => { 512 | 513 | let component; 514 | 515 | beforeEach(() => { 516 | component = mount(); 517 | }); 518 | 519 | // Use 'test' or it' (both possible) to test a single attribute of a target 520 | test('shows comment box', () => { 521 | 522 | expect(component.find('.comment-box').length).toBe(1); 523 | }); 524 | 525 | test('shows comment list', () => { 526 | expect(component.find('.comment-list').length).toBe(1); 527 | }); 528 | }); 529 | ``` 530 | 531 | Again, for more Unit Tests on React Components, see 532 | - the other test files in `src/components` folder with suffix `.test.js` and 533 | - this great tutorial: https://medium.freecodecamp.org/the-right-way-to-test-react-components-548a4736ab22 534 | 535 | ### b. Unit Testing the Redux State 536 | 537 | As second example again we'll check if our Reducer `src/reducers/comments.js` works correctly. 538 | We do it through test file `src/reducers/comments.test.js`: 539 | 540 | 541 | ``` 542 | import commentReducer from './comments'; 543 | import { SAVE_COMMENT } from '../actions/types'; 544 | 545 | describe('Comments Reducer', () => { 546 | 547 | // in case there is a weird input, we react with the default state 548 | test('handles action with unknown type', () => { 549 | 550 | expect(commentReducer(undefined, {})).toEqual([]); 551 | }); 552 | 553 | test('SAVE_COMMENT', () => { 554 | 555 | const action = { type: SAVE_COMMENT, payload: 'new comment'}; 556 | expect(commentReducer([], action)).toEqual(['new comment']); 557 | 558 | }); 559 | 560 | }); 561 | ``` 562 | 563 | Again, for more Unit Tests on Redux, see 564 | - the action creator test file `src/actions/index.test.js` 565 | - Example: https://hackernoon.com/low-effort-high-value-integration-tests-in-redux-apps-d3a590bd9fd5 566 | - Docs: https://redux.js.org/recipes/writing-tests 567 | 568 | ## 3. Integration Tests between React Component and the Redux State 569 | 570 | In the `CommentBox Component` and `CommentList Component` you might have discovered some Integration Tests already. 571 | 572 | Let's have a look at `CommentBox Component`: 573 | 574 | ### Mocha/Chai: 575 | 576 | ``` 577 | describe('entering some text', () => { 578 | 579 | beforeEach(() => { 580 | 581 | component.find('textarea').simulate('change', 'new comment'); 582 | }); 583 | 584 | it('shows that text in the textarea', () => { 585 | 586 | expect(component.find('textarea')).to.have.value('new comment'); 587 | }); 588 | 589 | it('when submitted, clears the input', () => { 590 | 591 | component.simulate('submit'); 592 | expect(component.find('textarea')).to.have.value(''); 593 | }); 594 | }); 595 | ``` 596 | 597 | ### Jest/Enzyme: 598 | 599 | ``` 600 | describe('entering some text', () => { 601 | 602 | beforeEach(() => { 603 | 604 | component.find('textarea').simulate('change', {target: {value: 'new comment'}}); 605 | }); 606 | 607 | test('shows that text in the textarea', () => { 608 | 609 | expect(component.find('textarea').prop('value')).toEqual('new comment'); 610 | }); 611 | 612 | test('when submitted, clears the input', () => { 613 | 614 | component.simulate('submit'); 615 | expect(component.find('textarea').prop('value')).toEqual(''); 616 | }); 617 | }); 618 | ``` 619 | 620 | What happens here: 621 | 622 | 1. `beforeEach(() => {})` injects the string `new comment` into the React Component's textarea BEFORE each following tests `test('...', () => {})` are executed. 623 | This causes an Update of the Redux State and therefore causes a Re-Render of the React Component. 624 | 2. Then we make an assertion whether the React Component got Re-Rendered as we expect within the `expect` statement. 625 | 3. The test will either pass or fail. In our case it will pass. 626 | 627 | So with Mocha/Chai and Jest/Enzyme we already have an Integration Test engine as we can accomplish even a full round-trip: 628 | 629 | React Component >> Redux State >> React Component 630 | 631 | #### Isn't that great ?! 632 | 633 | The reasons: 634 | - Everything in React is a Component, also the Redux Store Provider wrapped around our React Components. 635 | - Mocha/Chai and Jest/Enzyme render the entire React App into memory. 636 | - Within our Node.js Setup, `jsdom` module simulates a "fake" DOM for us, to simulate (user) interactions. 637 | 638 | Some manuals: 639 | - https://medium.freecodecamp.org/real-integration-tests-with-react-redux-and-react-router-417125212638 640 | - http://engineering.pivotal.io/post/react-integration-tests-with-enzyme/ 641 | - https://medium.com/homeaway-tech-blog/integration-testing-in-react-21f92a55a894 642 | 643 | 644 | ### What's next ? 645 | 646 | Next step are End-to-end Tests. If you want to stay within Jest ecosystem, you can think of adding Puppeteer as your browser engine for testing user interactions: 647 | - Puppeteer (Headless Chrome Node API): https://github.com/GoogleChrome/puppeteer 648 | - Jest + Puppeteer: https://blog.logrocket.com/end-to-end-testing-react-apps-with-puppeteer-and-jest-ce2f414b4fd7 649 | 650 | Of course there are other tools out there. Please check them out as well: 651 | - Selenium 652 | - TestCafé and 653 | - Cypress 654 | 655 | You'll find the links in the link list below. 656 | 657 | Happy Testing ! 658 | 659 | ## 4. Links 660 | 661 | ### Have a look ! 662 | 663 | Our React-Redux App: 664 | - Stephen Grider, Repo: https://github.com/StephenGrider/AdvancedReduxCode 665 | - Stephen Grider, Udemy Course "Advanced React and Redux": https://www.udemy.com/react-redux-tutorial 666 | 667 | Mocha/Chai: 668 | - https://github.com/chaijs/chai-jquery 669 | - http://chaijs.com/api/bdd 670 | 671 | Jest/Enzyme: 672 | - Jest: 673 | - https://github.com/facebook/jest 674 | - http://jestjs.io/docs/en/expect.html#content 675 | - Jest Snapshots (React Test Renderer): 676 | - https://reactjs.org/docs/test-renderer.html 677 | - https://github.com/facebook/react/tree/master/packages/react-test-renderer 678 | - Enzyme: https://airbnb.io/enzyme/docs/api/ 679 | - Manuals: 680 | - https://www.sitepoint.com/test-react-components-jest/ 681 | - https://github.com/facebook/create-react-app/blob/master/packages/react-scripts/template/README.md#running-tests 682 | - https://hackernoon.com/low-effort-high-value-integration-tests-in-redux-apps-d3a590bd9fd5 683 | 684 | Unit Tests Redux: 685 | - Docs: https://redux.js.org/recipes/writing-tests 686 | - Example: https://hackernoon.com/low-effort-high-value-integration-tests-in-redux-apps-d3a590bd9fd5 687 | 688 | Unit Tests React Components: 689 | - Example: https://medium.freecodecamp.org/the-right-way-to-test-react-components-548a4736ab22 690 | 691 | Unit Tests General: 692 | - https://en.wikipedia.org/wiki/Unit_testing 693 | - https://en.wikipedia.org/wiki/List_of_unit_testing_frameworks#JavaScript 694 | - https://hackernoon.com/testing-your-frontend-code-part-ii-unit-testing-1d05f8d50859 695 | - https://codeutopia.net/blog/2015/03/01/unit-testing-tdd-and-bdd/ 696 | - https://en.wikipedia.org/wiki/Behavior-driven_development 697 | - https://en.wikipedia.org/wiki/Test-driven_development 698 | 699 | Integration Tests: 700 | - https://hackernoon.com/testing-your-frontend-code-part-iv-integration-testing-f1f4609dc4d9 701 | - https://github.com/jsdom/jsdom 702 | - Manuals: 703 | - https://medium.freecodecamp.org/real-integration-tests-with-react-redux-and-react-router-417125212638 704 | - http://engineering.pivotal.io/post/react-integration-tests-with-enzyme/ 705 | - https://medium.com/homeaway-tech-blog/integration-testing-in-react-21f92a55a894 706 | 707 | End-2-end Tests: 708 | - https://hackernoon.com/testing-your-frontend-code-part-iii-e2e-testing-e9261b56475 709 | - https://medium.freecodecamp.org/why-end-to-end-testing-is-important-for-your-team-cb7eb0ec1504 710 | 711 | End-2-end Testing Tools: 712 | - Selenium: 713 | - https://www.fullstackreact.com/30-days-of-react/day-26/ 714 | - https://www.seleniumhq.org/ 715 | - Jest + Puppeteer: 716 | - Puppeteer (Headless Chrome Node API): https://github.com/GoogleChrome/puppeteer 717 | - Jest + Puppeteer: https://blog.logrocket.com/end-to-end-testing-react-apps-with-puppeteer-and-jest-ce2f414b4fd7 718 | - TestCafé: https://github.com/DevExpress/testcafe 719 | - Cypress: https://www.cypress.io/ 720 | 721 | Acceptance Tests: 722 | - https://en.wikipedia.org/wiki/Acceptance_testing 723 | 724 | Testing General: 725 | - https://en.wikipedia.org/wiki/Software_testing 726 | 727 | 728 | ### Credits to the authors of above links ! Thank you very much ! 729 | 730 | ### And credits to the reader: Thanks for your visit ! 731 | -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/.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.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "testing-with-cra-and-jest-enzyme", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "lodash": "^4.17.10", 7 | "react": "^16.4.1", 8 | "react-dom": "^16.4.1", 9 | "react-redux": "^5.0.7", 10 | "react-router": "^4.3.1", 11 | "react-scripts": "1.1.4", 12 | "redux": "^4.0.0" 13 | }, 14 | "scripts": { 15 | "start": "react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --env=jsdom --verbose", 18 | "eject": "react-scripts eject" 19 | }, 20 | "devDependencies": { 21 | "enzyme": "^3.3.0", 22 | "enzyme-adapter-react-16": "^1.1.1", 23 | "redux-mock-store": "^1.5.3" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/herrkraatz/react-unit-testing/38a2789ccc3fb0e91b368874af3d64eb580cff07/unit-testing-jest-enzyme/public/favicon.ico -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 29 |
30 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | animation: App-logo-spin infinite 20s linear; 7 | height: 80px; 8 | } 9 | 10 | .App-header { 11 | background-color: #222; 12 | height: 150px; 13 | padding: 20px; 14 | color: white; 15 | } 16 | 17 | .App-title { 18 | font-size: 1.5em; 19 | } 20 | 21 | .App-intro { 22 | font-size: large; 23 | } 24 | 25 | @keyframes App-logo-spin { 26 | from { transform: rotate(0deg); } 27 | to { transform: rotate(360deg); } 28 | } 29 | -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/src/App.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import CommentBox from './components/comment_box'; 3 | import CommentList from './components/comment_list'; 4 | import logo from './logo.svg'; 5 | import './App.css'; 6 | 7 | class App extends Component { 8 | render() { 9 | return ( 10 |
11 |
12 | logo 13 |

Welcome to React

14 |
15 |
16 | 17 | 18 |
19 |
20 | ); 21 | } 22 | } 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import App from './App'; 3 | import { mount } from 'enzyme'; 4 | 5 | // We need to wrap CommentBox with tag in first beforeEach(() => {}) below; 6 | // otherwise we receive this error message: 7 | // Invariant Violation: Could not find “store” in either the context or props of “Connect(CommentBox)” 8 | // https://stackoverflow.com/questions/36211739/invariant-violation-could-not-find-store-in-either-the-context-or-props-of-c 9 | // Also see comment_list.test.js 10 | import {configure} from 'enzyme'; 11 | import Adapter from 'enzyme-adapter-react-16'; 12 | import {Provider} from "react-redux"; 13 | import {createStore, applyMiddleware} from 'redux'; 14 | import reducers from './reducers'; 15 | 16 | configure({adapter: new Adapter()}); 17 | const createStoreWithMiddleware = applyMiddleware()(createStore); 18 | 19 | // Use 'describe' to group together similar tests 20 | describe('App', () => { 21 | 22 | let component; 23 | 24 | beforeEach(() => { 25 | component = mount(); 26 | }); 27 | 28 | // Use 'test' or 'it' (both possible) to test a single attribute of a target 29 | test('shows comment box', () => { 30 | 31 | expect(component.find('.comment-box').length).toBe(1); 32 | }); 33 | 34 | test('shows comment list', () => { 35 | expect(component.find('.comment-list').length).toBe(1); 36 | }); 37 | }); 38 | 39 | -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/src/actions/index.js: -------------------------------------------------------------------------------- 1 | import { SAVE_COMMENT } from './types'; 2 | 3 | // action creator: 4 | export function saveComment(comment){ 5 | return { 6 | type: SAVE_COMMENT, 7 | payload: comment 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/src/actions/index.test.js: -------------------------------------------------------------------------------- 1 | import { SAVE_COMMENT } from './types'; 2 | import { saveComment } from './'; 3 | 4 | describe('actions', () => { 5 | describe('saveComment', () => { 6 | test('has the correct type', () => { 7 | 8 | // action creator returns an action 9 | const action = saveComment(); 10 | expect(action.type).toEqual(SAVE_COMMENT); 11 | }); 12 | 13 | test('has the correct payload', () => { 14 | 15 | const action = saveComment('new comment'); 16 | expect(action.payload).toEqual('new comment'); 17 | }); 18 | 19 | }); 20 | 21 | }); -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/src/actions/types.js: -------------------------------------------------------------------------------- 1 | export const SAVE_COMMENT = 'save_comment'; -------------------------------------------------------------------------------- /unit-testing-jest-enzyme/src/components/comment_box.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | // to turn a component into a container to have access to the state: 4 | import { connect } from 'react-redux'; 5 | // imports ALL action creators and stores them in variable actions 6 | import * as actions from '../actions'; 7 | 8 | // not exporting our component any more, but exporting a container instead: 9 | // export default class CommentBox extends Component { 10 | class CommentBox extends Component { 11 | constructor(props) { 12 | super(props); 13 | 14 | this.state = { comment: '' }; 15 | } 16 | 17 | handleChange(event){ 18 | this.setState({ comment: event.target.value }); 19 | } 20 | 21 | handleSubmit(event){ 22 | // keep the form from submitting to itself 23 | event.preventDefault(); 24 | 25 | // new after having wired up the Container with the action creators 26 | this.props.saveComment(this.state.comment); 27 | this.setState({ comment: ''}); 28 | } 29 | 30 | render() { 31 | return ( 32 |
33 |

Add a comment

34 | 38 | // 39 | //
40 | component.simulate('submit'); 41 | expect(component.find('textarea')).to.have.value(''); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /unit-testing-mocha-chai/test/components/comment_list_test.js: -------------------------------------------------------------------------------- 1 | import { renderComponent, expect } from '../test_helper'; 2 | import CommentList from '../../src/components/comment_list'; 3 | 4 | describe('CommentList', () => { 5 | 6 | let component; 7 | 8 | beforeEach(() => { 9 | // renderComponents makes props available 10 | // const props = { comments: ['New Comment', 'New Other Comment'] }; 11 | // component = renderComponent(CommentList, null, props); 12 | const initialState = { comments: ['New Comment', 'New Other Comment'] }; // better call it initialState, not props 13 | component = renderComponent(CommentList, null, initialState); 14 | }); 15 | 16 | it('shows an LI for each comment', () => { 17 | expect(component.find('li').length).to.equal(2); 18 | 19 | }); 20 | 21 | it('shows each comment that is provided', () => { 22 | expect(component).to.contain('New Comment'); 23 | expect(component).to.contain('New Other Comment'); 24 | }); 25 | 26 | }); -------------------------------------------------------------------------------- /unit-testing-mocha-chai/test/reducers/comments_test.js: -------------------------------------------------------------------------------- 1 | import { expect } from '../test_helper'; 2 | import commentReducer from '../../src/reducers/comments'; 3 | import { SAVE_COMMENT } from '../../src/actions/types'; 4 | 5 | describe('Comments Reducer', () => { 6 | 7 | // in case there is a weird input, we react with the default state 8 | it('handles action with unknown type', () => { 9 | 10 | // expect(commentReducer()).to.be.instanceof(Array); 11 | // better: 12 | // eql compares deeply 13 | // so we are sure the array is really empty 14 | // expect(commentReducer()).to.eql([]); 15 | // failed after we completed the reducer: TypeError: Cannot read property 'type' of undefined 16 | // so we have to pass in values here: 17 | expect(commentReducer(undefined, {})).to.eql([]); 18 | }); 19 | 20 | // it('handles action of type SAVE_COMMENT', () => { 21 | // 22 | // }); 23 | // better: 24 | it('SAVE_COMMENT', () => { 25 | 26 | const action = { type: SAVE_COMMENT, payload: 'new comment'}; 27 | expect(commentReducer([], action)).to.eql(['new comment']); 28 | 29 | }); 30 | 31 | }); -------------------------------------------------------------------------------- /unit-testing-mocha-chai/test/test_helper.js: -------------------------------------------------------------------------------- 1 | // https://github.com/jsdom/jsdom 2 | // A fake HTML Document run in the terminal (Node.js) 3 | // A JavaScript implementation of the WHATWG DOM and HTML standards, for use with Node.js 4 | import jsdom from 'jsdom'; 5 | import _$ from 'jquery'; 6 | import TestUtils from 'react-addons-test-utils'; 7 | import ReactDOM from 'react-dom'; 8 | import chai, { expect } from 'chai'; 9 | import React from 'react'; // needed whenever we use JSX 10 | import { Provider } from 'react-redux'; 11 | import { createStore } from 'redux'; 12 | import reducers from '../src/reducers'; 13 | import chaiJquery from 'chai-jquery'; 14 | 15 | // Set up testing environment to run like a browser in the command line 16 | // not window, but global variable 17 | 18 | const { JSDOM } = jsdom; 19 | const { document } = (new JSDOM('')).window; 20 | global.document = document; 21 | 22 | global.window = global.document.defaultView; 23 | // overwrite default $ of jquery / hook up our fake dom to jquery 24 | const $ = _$(global.window); 25 | 26 | // build 'renderComponent' helper that should render a given react class 27 | function renderComponent(ComponentClass, props, state){ 28 | 29 | // https://reactjs.org/docs/test-utils.html 30 | const componentInstance = TestUtils.renderIntoDocument( 31 | 32 | 33 | 34 | ); 35 | 36 | return $(ReactDOM.findDOMNode(componentInstance)); // produces HTML and wraps it into jquery to be accessible 37 | 38 | 39 | } 40 | 41 | // build helper to for simulating events 42 | // makes this possible for all jquery elements: $('div').simulate() 43 | $.fn.simulate = function(eventName, value){ 44 | 45 | if (value){ 46 | this.val(value); // val() is a jquery function 47 | } 48 | 49 | // this is the reference to the dom element, and we need the first one of the array 50 | TestUtils.Simulate[eventName](this[0]); 51 | 52 | }; 53 | 54 | // Set up chai-jquery 55 | chaiJquery(chai, chai.util, $); 56 | 57 | export { renderComponent, expect }; 58 | -------------------------------------------------------------------------------- /unit-testing-mocha-chai/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'development', 3 | entry: [ 4 | './src/index.js' 5 | ], 6 | output: { 7 | path: __dirname, 8 | publicPath: '/', 9 | filename: 'bundle.js' 10 | }, 11 | module: { 12 | rules: [{ 13 | exclude: /node_modules/, 14 | loader: 'babel-loader' 15 | }] 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.jsx'] 19 | }, 20 | devServer: { 21 | historyApiFallback: true, 22 | contentBase: './' 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /unit-testing-mocha-chai/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | module.exports = { 3 | mode: 'production', 4 | entry: [ 5 | './src/index.js' 6 | ], 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | publicPath: '/', 10 | filename: 'bundle.js' 11 | }, 12 | module: { 13 | rules: [{ 14 | exclude: /node_modules/, 15 | loader: 'babel-loader' 16 | }] 17 | }, 18 | resolve: { 19 | extensions: ['.js', '.jsx'] 20 | }, 21 | devServer: { 22 | historyApiFallback: true, 23 | contentBase: './' 24 | } 25 | }; 26 | --------------------------------------------------------------------------------