├── README.md └── translations └── zh-cn └── README.md /README.md: -------------------------------------------------------------------------------- 1 | # A guide to unit testing in JavaScript 2 | 3 | **This is a living document. New ideas are always welcome. Contribute: fork, clone, branch, commit, push, pull request.** 4 | 5 | > All the information provided has been compiled & adapted from many references, some of them cited at the end of the document. 6 | > The guidelines are illustrated by my own examples, fruit of my personal experience writing and reviewing unit tests. 7 | > Many thanks to all of the sources of information & contributors. 8 | 9 | 📅 _Last edit: September 2023._ 10 | 11 | ## 📖 Table of contents 12 | 13 | 1. General principles 14 | 15 | - [Unit tests](#unit-tests) 16 | - [Design principles](#design-principles) 17 | 18 | 2. Guidelines 19 | 20 | - [Whenever possible, use TDD](#-whenever-possible-use-tdd) 21 | - [When applying TDD, always start by writing the simplest failing test](#-when-applying-tdd-always-start-by-writing-the-simplest-failing-test) 22 | - [When applying TDD, always make baby steps in each cycle](#-when-applying-tdd-always-make-baby-steps-in-each-cycle) 23 | - [Structure your tests properly](#-structure-your-tests-properly) 24 | - [Name your tests properly](#-name-your-tests-properly) 25 | - [Use the Arrange-Act-Assert pattern](#-use-the-arrange-act-assert-pattern) 26 | - [Avoid logic in your tests](#-avoid-logic-in-your-tests) 27 | - [Don't write unnecessary expectations](#-dont-write-unnecessary-expectations) 28 | - [Test the behaviour, not the internal implementation](#-test-the-behaviour-not-the-internal-implementation) 29 | - [Consider using factory functions](#-consider-using-factory-functions) 30 | - [Don't test multiple concerns in the same test](#-dont-test-multiple-concerns-in-the-same-test) 31 | - [Cover the general case and the edge cases](#-cover-the-general-case-and-the-edge-cases) 32 | - [Use dependency injection](#-use-dependency-injection) 33 | - [Don't mock everything](#-dont-mock-everything) 34 | - [Don't write unit tests for complex user interactions](#-dont-write-unit-tests-for-complex-user-interactions) 35 | - [Test simple user actions](#-test-simple-user-actions) 36 | - [Create new tests for every defect](#-create-new-tests-for-every-defect) 37 | - [Don't comment out tests](#-dont-comment-out-tests) 38 | - [Know your testing framework API](#-know-your-testing-framework-api) 39 | - [Review test code first](#-review-test-code-first) 40 | - [Practice code katas, learn with pair programming](#-practice-code-katas-learn-with-pair-programming) 41 | 42 | 3. [Resources](#-resources) 43 | 4. [Translations](#-translations) 44 | 5. [Contributors](#-contributors) 45 | 46 | ## ⛩️ General principles 47 | 48 | ### Unit tests 49 | 50 | **Unit = Unit of work** 51 | 52 | The work can involve **multiple methods and classes**, invoked by some public API that can: 53 | 54 | - Return a value or throw an exception 55 | - Change the state of the system 56 | - Make 3rd party calls (API, database, ...) 57 | 58 | A unit test should test the behaviour of a unit of work: for a given input, it expects an end result that can be any of the above. 59 | 60 | **Unit tests are isolated and independent of each other** 61 | 62 | - Any given behaviour should be specified in **one and only one test** 63 | - The execution/order of execution of one test **cannot affect the others** 64 | 65 | The code is designed to support this independence (see "Design principles" below). 66 | 67 | **Unit tests are lightweight tests** 68 | 69 | - Repeatable 70 | - Fast 71 | - Consistent 72 | - Easy to write and read 73 | 74 | **Unit tests are code too** 75 | 76 | They should be easily **readable** and **maintainable**. 77 | 78 | Don't hesitate to refactor them to help your future self. For instance, it should be trivial to understand why a test is failing just by looking at its own code, without having to search in many places in the test file (variables declared in the top-level scope, closures, test setup & teardown hooks, etc.). 79 | 80 | • [Back to ToC](#-table-of-contents) • 81 | 82 | ### Design principles 83 | 84 | The key to good unit testing is to write **testable code**. Applying simple design principles can help, in particular: 85 | 86 | - Use a **good, consistent naming convention**. 87 | - **D**on't **R**epeat **Y**ourself: avoid code duplication. 88 | - **Single responsibility**: each software component (function, class, component, module) should focus on a single task. 89 | - Keep a **single level of abstraction** in the same component. For example, do not mix business logic with lower-level technical details in the same method. 90 | - **Minimize dependencies** between components: encapsulate, interchange less information between components. 91 | - **Support configurability** rather than hard-coding: to prevent having to replicate the exact same environment when testing. 92 | - Apply adequate **design patterns**: especially **dependency injection**, to be able to easily substitue the component's dependencies when testing. 93 | - **Avoid global mutable state**: to keep things easy to understand and predictable. 94 | 95 | • [Back to ToC](#-table-of-contents) • 96 | 97 | ## 🧭 Guidelines 98 | 99 | The goal of these guidelines is to make your tests: 100 | 101 | - **Readable** 102 | - **Maintainable** 103 | - **Trustworthy** 104 | 105 | These are the 3 pillars of good unit testing. 106 | 107 | All the following examples assume the usage of the [Jest testing framework](https://jestjs.io/). 108 | 109 | • [Back to ToC](#-table-of-contents) • 110 | 111 | ### ✨ Whenever possible, use TDD 112 | 113 | **Test-Driven Development is a design process, not a testing process.** It's a highly-iterative process in which you design, test, and code more or less at the same time. It goes like this: 114 | 115 | 1. **Think:** Figure out what test will best move your code towards completion. (Take as much time as you need. This is the hardest step for beginners.) 116 | 2. **Red:** Write a very small amount of test code. Only a few lines... Run the tests and watch the new test fail: the test bar should turn red. 117 | 3. **Green:** Write a very small amount of production code. Again, usually no more than a few lines of code. Don't worry about design purity or conceptual elegance. Sometimes you can just hardcode the answer. This is okay because you'll be refactoring in a moment. Run the tests and watch them pass: the test bar will turn green. 118 | 4. **Refactor:** Now that your tests are passing, you can make changes without worrying about breaking anything. Pause for a moment, look at the code you've written, and ask yourself if you can improve it. Look for duplication and other "code smells." If you see something that doesn't look right, but you're not sure how to fix it, that's okay. Take a look at it again after you've gone through the cycle a few more times. (Take as much time as you need on this step.) After each little refactoring, run the tests and make sure they still pass. 119 | 5. **Repeat:** Do it again. You'll repeat this cycle dozens of times in an hour. Typically, you'll run through several cycles (three to five) very quickly, then find yourself slowing down and spending more time on refactoring. Then you'll speed up again. 120 | 121 | This process works well for two reasons: 122 | 123 | 1. You're working in baby steps, constantly forming hypotheses and checking them. Whenever you make a mistake, you catch it right away. It's only been a few lines of code since you made the mistake, which makes the mistake very easy to find and fix. We all know that finding mistakes, not fixing them, is the most expensive part of programming. 124 | 2. You're always thinking about design. Either you're deciding which test you're going to write next, which is an interface design process, or you're deciding how to refactor, which is a code design process. All of this thought on design is immediately tested by turning it into code, which very quickly shows you if the design is good or bad. 125 | 126 | Notice also how code written without a test-first approach is often very hard to test! 127 | 128 | • [Back to ToC](#-table-of-contents) • 129 | 130 | ### ✨ When applying TDD, always start by writing the simplest failing test 131 | 132 | **:(** 133 | 134 | ```js 135 | it("calculates a RPN expression", () => { 136 | const result = RPN("5 1 2 + 4 * - 10 /"); 137 | expect(result).toBe(-0.7); 138 | }); 139 | ``` 140 | 141 | **:)** 142 | 143 | ```js 144 | it("returns null when the expression is an empty string", () => { 145 | const result = RPN(""); 146 | expect(result).toBeNull(); 147 | }); 148 | ``` 149 | 150 | From there, start building the functionalities incrementally. 151 | 152 | • [Back to ToC](#-table-of-contents) • 153 | 154 | ### ✨ When applying TDD, always make baby steps in each cycle 155 | 156 | Build your tests suite from the simple case to the more complex ones. Keep in mind the incremental design. Deliver new code fast, incrementally, and in short iterations: 157 | 158 | **:(** 159 | 160 | ```js 161 | it("returns null when the expression is an empty string", () => { 162 | const result = RPN(""); 163 | expect(result).toBeNull(); 164 | }); 165 | 166 | it("calculates a RPN expression", () => { 167 | const result = RPN("5 1 2 + 4 * - 10 /"); 168 | expect(result).toBe(-0.7); 169 | }); 170 | ``` 171 | 172 | **:)** 173 | 174 | ```js 175 | describe("The RPN expression evaluator", () => { 176 | it("returns null when the expression is an empty string", () => { 177 | const result = RPN(""); 178 | expect(result).toBeNull(); 179 | }); 180 | 181 | it("returns the same value when the expression holds a single value", () => { 182 | const result = RPN("42"); 183 | expect(result).toBe(42); 184 | }); 185 | 186 | describe("Additions", () => { 187 | it("calculates a simple addition", () => { 188 | const result = RPN("41 1 +"); 189 | expect(result).toBe(42); 190 | }); 191 | 192 | // ... 193 | 194 | it("calculates a complex addition", () => { 195 | const result = RPN("2 9 + 15 3 + + 7 6 + +"); 196 | expect(result).toBe(42); 197 | }); 198 | }); 199 | 200 | // ... 201 | 202 | describe("Complex expressions", () => { 203 | it("calculates an expression containing all 4 operators", () => { 204 | const result = RPN("5 1 2 + 4 * - 10 /"); 205 | expect(result).toBe(-0.7); 206 | }); 207 | }); 208 | }); 209 | ``` 210 | 211 | • [Back to ToC](#-table-of-contents) • 212 | 213 | ### ✨ Structure your tests properly 214 | 215 | Don't hesitate to nest your suites to structure logically your tests in subsets: 216 | 217 | **:(** 218 | 219 | ```js 220 | describe("A set of functionalities", () => { 221 | it("does something nice", () => {}); 222 | 223 | it("a subset of functionalities does something great", () => {}); 224 | 225 | it("a subset of functionalities does something awesome", () => {}); 226 | 227 | it("another subset of functionalities also does something great", () => {}); 228 | }); 229 | ``` 230 | 231 | **:)** 232 | 233 | ```js 234 | describe("A set of functionalities", () => { 235 | it("does something nice", () => {}); 236 | 237 | describe("A subset of functionalities", () => { 238 | it("does something great", () => {}); 239 | 240 | it("does something awesome", () => {}); 241 | }); 242 | 243 | describe("Another subset of functionalities", () => { 244 | it("also does something great", () => {}); 245 | }); 246 | }); 247 | ``` 248 | 249 | • [Back to ToC](#-table-of-contents) • 250 | 251 | ### ✨ Name your tests properly 252 | 253 | Tests names should be concise, explicit, descriptive and in correct English. Read the output of the test runner and verify that it is understandable! 254 | 255 | Keep in mind that someone else will read it too and that tests can be the live documentation of the code: 256 | 257 | **:(** 258 | 259 | ```js 260 | describe("myGallery", () => { 261 | it("init set correct property when called (thumb size, thumbs count)", () => {}); 262 | }); 263 | ``` 264 | 265 | **:)** 266 | 267 | ```js 268 | describe("The Gallery instance", () => { 269 | it("calculates the thumb size when initialized", () => {}); 270 | 271 | it("calculates the thumbs count when initialized", () => {}); 272 | }); 273 | ``` 274 | 275 | In order to help you write test names properly, you can use the **"unit of work - scenario/context - expected behaviour"** pattern: 276 | 277 | ```js 278 | describe("[unit of work]", () => { 279 | it("[expected behaviour] when [scenario/context]", () => {}); 280 | }); 281 | ``` 282 | 283 | Or if there are many tests that follow the same scenario or are related to the same context: 284 | 285 | ```js 286 | describe("[unit of work]", () => { 287 | describe("when [scenario/context]", () => { 288 | it("[expected behaviour]", () => {}); 289 | }); 290 | }); 291 | ``` 292 | 293 | For example: 294 | 295 | **:) :)** 296 | 297 | ```js 298 | describe("The Gallery instance", () => { 299 | describe("when initialized", () => { 300 | it("calculates the thumb size", () => {}); 301 | 302 | it("calculates the thumbs count", () => {}); 303 | 304 | // ... 305 | }); 306 | }); 307 | ``` 308 | 309 | You might also want to use this pattern to describe a class and its methods: 310 | 311 | ```js 312 | describe("Gallery", () => { 313 | describe("init()", () => { 314 | it("calculates the thumb size", () => {}); 315 | 316 | it("calculates the thumbs count", () => {}); 317 | }); 318 | 319 | describe("goTo(index)", () => {}); 320 | 321 | // ... 322 | }); 323 | ``` 324 | 325 | Also, tests ["should not begin with should"](https://github.com/spotify/should-up). 326 | 327 | • [Back to ToC](#-table-of-contents) • 328 | 329 | ### ✨ Use the Arrange-Act-Assert pattern 330 | 331 | This pattern is a good support to help you read and understand tests more easily: 332 | 333 | - The **arrange** part is where you set up the objects to be tested: initializing input variables, setting up spies, etc. 334 | - The **act** part is where you act upon the code under test: calling a function or a class method, storing the result, ... 335 | - The **assert** part is where you test your expectations. 336 | 337 | ```js 338 | describe("Gallery", () => { 339 | describe("goTo(index)", () => { 340 | it("displays the image identified by its index", () => { 341 | // arrange 342 | const myGallery = new Gallery(); 343 | const index = 1; 344 | 345 | // act 346 | myGallery.goTo(index); 347 | 348 | // assert 349 | expect(document.getElementById("image-1")).toBeVisible(); 350 | }); 351 | }); 352 | }); 353 | ``` 354 | 355 | This pattern is also named "Given-When-Then" or "Setup-Exercise-Verify". 356 | 357 | • [Back to ToC](#-table-of-contents) • 358 | 359 | ### ✨ Avoid logic in your tests 360 | 361 | Always use simple statements. Don't use loops and/or conditionals. If you do, you add a possible entry point for bugs in the test itself: 362 | 363 | - Conditionals: you don't know which path the test will take. 364 | - Loops: you could be sharing state between tests. 365 | 366 | • [Back to ToC](#-table-of-contents) • 367 | 368 | ### ✨ Don't write unnecessary expectations 369 | 370 | Remember, unit tests are a design specification of how a certain _behaviour_ should work, not a list of observations of everything the code happens to do: 371 | 372 | **:(** 373 | 374 | ```js 375 | it("computes the result of an expression", () => { 376 | const multiplySpy = jest.spyOn(Calculator, "multiple"); 377 | const subtractSpy = jest.spyOn(Calculator, "subtract"); 378 | 379 | const result = Calculator.compute("(21.5 x 2) - 1"); 380 | 381 | expect(multiplySpy).toHaveBeenCalledWith(21.5, 2); 382 | expect(subtractSpy).toHaveBeenCalledWith(43, 1); 383 | expect(result).toBe(42); 384 | }); 385 | ``` 386 | 387 | **:)** 388 | 389 | ```js 390 | it("computes the result of the expression", () => { 391 | const result = Calculator.compute("(21.5 x 2) - 1"); 392 | 393 | expect(result).toBe(42); 394 | }); 395 | ``` 396 | 397 | • [Back to ToC](#-table-of-contents) • 398 | 399 | ### ✨ Test the behaviour, not the internal implementation 400 | 401 | **:(** 402 | 403 | ```js 404 | it("adds a user in memory", () => { 405 | usersManager.addUser("Dr. Falker"); 406 | 407 | expect(usersManager._users[0].name).toBe("Dr. Falker"); 408 | }); 409 | ``` 410 | 411 | A better approach is to test at the same level of the API: 412 | 413 | **:)** 414 | 415 | ```js 416 | it("adds a user in memory", () => { 417 | usersManager.addUser("Dr. Falker"); 418 | 419 | expect(usersManager.hasUser("Dr. Falker")).toBe(true); 420 | }); 421 | ``` 422 | 423 | - **Pro:** changing the internal implementation will not necessarily force you to refactor the tests. 424 | - **Con:** when a test is failing, you might have to debug to know which part of the code needs to be fixed. 425 | 426 | Here, a balance has to be found, unit-testing some key parts can be beneficial. 427 | 428 | • [Back to ToC](#-table-of-contents) • 429 | 430 | ### ✨ Consider using factory functions 431 | 432 | Factories can: 433 | 434 | - help reduce the setup code, especially if you use dependency injection, 435 | - make each test more readable by favoring cohesion, since the creation is a single function call in the test itself instead of the setup, 436 | - provide flexibility when creating new instances (setting an initial state, for example). 437 | 438 | **:(** 439 | 440 | ```js 441 | describe("The UserProfile class", () => { 442 | let userProfile; 443 | let pubSub; 444 | 445 | beforeEach(() => { 446 | const element = document.getElementById("my-profile"); 447 | 448 | pubSub = { notify() {} }; 449 | 450 | userProfile = new UserProfile({ 451 | element, 452 | pubSub, 453 | likes: 0, 454 | }); 455 | }); 456 | 457 | it('publishes a topic when a new "like" is given', () => { 458 | jest.spyOn(pubSub, "notify"); 459 | 460 | userProfile.incLikes(); 461 | 462 | expect(pubSub.notify).toHaveBeenCalledWith("likes:inc", { count: 1 }); 463 | }); 464 | 465 | it("retrieves the number of likes", () => { 466 | userProfile.incLikes(); 467 | userProfile.incLikes(); 468 | 469 | expect(userProfile.getLikes()).toBe(2); 470 | }); 471 | }); 472 | ``` 473 | 474 | **:)** 475 | 476 | ```js 477 | function createUserProfile({ likes = 0 } = {}) { 478 | const element = document.getElementById("my-profile"),; 479 | const pubSub = { notify: jest.fn() }; 480 | 481 | const userProfile = new UserProfile({ 482 | element, 483 | pubSub 484 | likes, 485 | }); 486 | 487 | return { 488 | pubSub, 489 | userProfile, 490 | }; 491 | } 492 | 493 | describe("The UserProfile class", () => { 494 | it('publishes a topic when a new "like" is given', () => { 495 | const { 496 | userProfile, 497 | pubSub, 498 | } = createUserProfile(); 499 | 500 | userProfile.incLikes(); 501 | 502 | expect(pubSub.notify).toHaveBeenCalledWith("likes:inc"); 503 | }); 504 | 505 | it("retrieves the number of likes", () => { 506 | const { userProfile } = createUserProfile({ likes: 40 }); 507 | 508 | userProfile.incLikes(); 509 | userProfile.incLikes(); 510 | 511 | expect(userProfile.getLikes()).toBe(42); 512 | }); 513 | }); 514 | ``` 515 | 516 | Factories can be particularly useful when dealing with the DOM: 517 | 518 | **:(** 519 | 520 | ```js 521 | describe("The search component", () => { 522 | describe("when the search button is clicked", () => { 523 | let container; 524 | let form; 525 | let searchInput; 526 | let submitInput; 527 | 528 | beforeEach(() => { 529 | fixtures.inject(`
530 |
531 | 532 | 533 |
534 |
`); 535 | 536 | container = document.getElementById("container"); 537 | form = container.getElementsByClassName("js-form")[0]; 538 | searchInput = form.querySelector("input[type=search]"); 539 | submitInput = form.querySelector("input[type=submith]"); 540 | }); 541 | 542 | it("validates the text entered", () => { 543 | const search = new Search({ container }); 544 | jest.spyOn(search, "validate"); 545 | 546 | search.init(); 547 | 548 | input(searchInput, "peace"); 549 | click(submitInput); 550 | 551 | expect(search.validate).toHaveBeenCalledWith("peace"); 552 | }); 553 | }); 554 | }); 555 | ``` 556 | 557 | **:)** 558 | 559 | ```js 560 | function createHTMLFixture() { 561 | fixtures.inject(`
562 |
563 | 564 | 565 |
566 |
`); 567 | 568 | const container = document.getElementById("container"); 569 | const form = container.getElementsByClassName("js-form")[0]; 570 | const searchInput = form.querySelector("input[type=search]"); 571 | const submitInput = form.querySelector("input[type=submith]"); 572 | 573 | return { 574 | container, 575 | form, 576 | searchInput, 577 | submitInput, 578 | }; 579 | } 580 | 581 | describe("The search component", () => { 582 | describe("when the search button is clicked", () => { 583 | it("validates the text entered", () => { 584 | const { container, searchInput, submitInput } = createHTMLFixture(); 585 | 586 | const search = new Search({ container }); 587 | 588 | jest.spyOn(search, "validate"); 589 | 590 | search.init(); 591 | 592 | input(searchInput, "peace"); 593 | click(submitInput); 594 | 595 | expect(search.validate).toHaveBeenCalledWith("peace"); 596 | }); 597 | }); 598 | }); 599 | ``` 600 | 601 | Here also, there's a trade-off to find between applying the DRY principle and readability. 602 | 603 | • [Back to ToC](#-table-of-contents) • 604 | 605 | ### ✨ Don't test multiple concerns in the same test 606 | 607 | If a method has several end results, each one should be tested separately so that whenever a bug occurs, it will help you locate the source of the problem directly: 608 | 609 | **:(** 610 | 611 | ```js 612 | it("sends the profile data to the API and updates the profile view", () => { 613 | // expect(...)to(...); 614 | // expect(...)to(...); 615 | }); 616 | ``` 617 | 618 | **:)** 619 | 620 | ```js 621 | it("sends the profile data to the API", () => { 622 | // expect(...)to(...); 623 | }); 624 | 625 | it("updates the profile view", () => { 626 | // expect(...)to(...); 627 | }); 628 | ``` 629 | 630 | Pay attention when writing "and" or "or" in your test names ;) 631 | 632 | • [Back to ToC](#-table-of-contents) • 633 | 634 | ### ✨ Cover the general case and the edge cases 635 | 636 | Having edge cases covered will: 637 | 638 | - clarify what the code does in a wide range of situations, 639 | - capture regressions early when the code is refactored, 640 | - help the future reader fully understand what the code fully does, as tests can be the live documentation of the code. 641 | 642 | **:(** 643 | 644 | ```js 645 | it("calculates the value of an expression", () => { 646 | const result = RPN("5 1 2 + 4 * - 10 /"); 647 | expect(result).toBe(-0.7); 648 | }); 649 | ``` 650 | 651 | **:)** 652 | 653 | ```js 654 | describe("The RPN expression evaluator", () => { 655 | // edge case 656 | it("returns null when the expression is an empty string", () => { 657 | const result = RPN(""); 658 | expect(result).toBeNull(); 659 | }); 660 | 661 | // edge case 662 | it("returns the same value when the expression holds a single value", () => { 663 | const result = RPN("42"); 664 | expect(result).toBe(42); 665 | }); 666 | 667 | // edge case 668 | it("throws an error whenever an invalid expression is passed", () => { 669 | const compute = () => RPN("1 + - 1"); 670 | expect(compute).toThrow(); 671 | }); 672 | 673 | // general case 674 | it("calculates the value of an expression", () => { 675 | const result = RPN("5 1 2 + 4 * - 10 /"); 676 | expect(result).toBe(-0.7); 677 | }); 678 | }); 679 | ``` 680 | 681 | • [Back to ToC](#-table-of-contents) • 682 | 683 | ### ✨ Use dependency injection 684 | 685 | **:(** 686 | 687 | ```js 688 | describe("when the user has already visited the page", () => { 689 | // storage.getItem('page-visited', '1') === '1' 690 | describe("when the survey is not disabled", () => { 691 | // storage.getItem('survey-disabled') === null 692 | it("displays the survey", () => { 693 | const storage = window.localStorage; 694 | storage.setItem("page-visited", "1"); 695 | storage.setItem("survey-disabled", null); 696 | 697 | const surveyManager = new SurveyManager(); 698 | jest.spyOn(surveyManager, "display"); 699 | 700 | surveyManager.start(); 701 | 702 | expect(surveyManager.display).toHaveBeenCalled(); 703 | }); 704 | }); 705 | }); 706 | ``` 707 | 708 | We created a permanent storage of data. What happens if we do not properly clean it between tests? We might affect the result of other tests. By using dependency injection, we can prevent this behaviour: 709 | 710 | **:)** 711 | 712 | ```js 713 | describe("when the user has already visited the page", () => { 714 | // storage.getItem('page-visited', '1') === '1' 715 | describe("when the survey is not disabled", () => { 716 | // storage.getItem('survey-disabled') === null 717 | it("displays the survey", () => { 718 | // E.g. https://github.com/tatsuyaoiw/webstorage 719 | const storage = new MemoryStorage(); 720 | storage.setItem("page-visited", "1"); 721 | storage.setItem("survey-disabled", null); 722 | 723 | const surveyManager = new SurveyManager(storage); 724 | jest.spyOn(surveyManager, "display"); 725 | 726 | surveyManager.start(); 727 | 728 | expect(surveyManager.display).toHaveBeenCalled(); 729 | }); 730 | }); 731 | }); 732 | ``` 733 | 734 | • [Back to ToC](#-table-of-contents) • 735 | 736 | ### ✨ Don't mock everything 737 | 738 | The idea to keep in mind is that **dependencies can still be real objects**. Don't mock everything because you can. Consider using the real version if: 739 | 740 | - it does not create a shared state between the tests, causing unexpected side effects, 741 | - the code being tested does not make HTTP requests or browser page reloads, 742 | - the speed of execution of the tests stays within the limits you fixed, 743 | - it leads to a simple, nice and easy tests setup. 744 | 745 | • [Back to ToC](#-table-of-contents) • 746 | 747 | ### ✨ Don't write unit tests for complex user interactions 748 | 749 | Examples of complex user interactions: 750 | 751 | - Filling a form, drag and dropping some items then submitting the form. 752 | - Clicking a tab, clicking an image thumbnail then navigating through a gallery of images loaded on-demand from an API. 753 | 754 | These interactions involve many units of work and should be handled at a higher level by **end-to-end tests**. They will usually take more time to execute, they could be flaky (false negatives) and they will require debugging whenever a failure is reported. 755 | 756 | For these complex user scenarios, consider using tools like [Playwright](https://playwright.dev/) or [Cypress](https://www.cypress.io/), or manual QA testing. 757 | 758 | • [Back to ToC](#-table-of-contents) • 759 | 760 | ### ✨ Test simple user actions 761 | 762 | Example of simple user actions: 763 | 764 | - Clicking on a link that toggles the visibility of a DOM element 765 | - Clicking on a button that performs an API call (like sending a tracking event). 766 | 767 | These actions can be easily tested by simulating DOM events, for example: 768 | 769 | ```js 770 | describe('when clicking on the "Preview profile" link', () => { 771 | it("shows the preview if it is hidden", () => { 772 | const { userProfile, previewLink } = createUserProfile({ 773 | previewIsVisible: false, 774 | }); 775 | 776 | jest.spyOn(userProfile, "showPreview"); 777 | 778 | click(previewLink); 779 | 780 | expect(userProfile.showPreview).toHaveBeenCalled(); 781 | }); 782 | 783 | it("hides the preview if it is visible", () => { 784 | const { userProfile, previewLink } = createUserProfile({ 785 | previewIsVisible: true, 786 | }); 787 | 788 | jest.spyOn(userProfile, "hidePreview"); 789 | 790 | click(previewLink); 791 | 792 | expect(userProfile.hidePreview).toHaveBeenCalled(); 793 | }); 794 | }); 795 | ``` 796 | 797 | Note how simple the tests are because the UI (DOM) layer does not mix with the business logic layer: 798 | 799 | - a "click" event occurs 800 | - a public method is called 801 | 802 | The next step could be to test the logic implemented in `showPreview()` or `hidePreview()`. 803 | 804 | • [Back to ToC](#-table-of-contents) • 805 | 806 | ### ✨ Create new tests for every defect 807 | 808 | Whenever a bug is found, create a test that replicates the problem **before touching any code**. Then fix it. 809 | 810 | • [Back to ToC](#-table-of-contents) • 811 | 812 | ### ✨ Don't comment out tests 813 | 814 | Never. Ever. Tests have a reason to be or not. 815 | 816 | Don't comment them out because they are too slow, too complex or produce false negatives. Instead, make them fast, simple and trustworthy. If not, remove them completely. 817 | 818 | • [Back to ToC](#-table-of-contents) • 819 | 820 | ### ✨ Know your testing framework API 821 | 822 | Take time to read the API documentation of the testing framework that you have chosen to work with. 823 | 824 | Having a good knowledge of the framework will help you in reducing the size and complexity of your test code and, in general, will help you during development. 825 | 826 | ### ✨ Review test code first 827 | 828 | [When reviewing code](https://github.com/mawrkus/pull-request-review-guide), always start by reading the code of the tests. Tests are mini use cases of the code that you can drill into. 829 | 830 | It will help you understand the intent of the developer very quickly (could be just by looking at the names of the tests). 831 | 832 | • [Back to ToC](#-table-of-contents) • 833 | 834 | ### ✨ Practice code katas, learn with pair programming 835 | 836 | Because **experience is the only teacher**. Ultimately, greatness comes from practicing; applying the theory over and over again, using feedback to get better every time. 837 | 838 | • [Back to ToC](#-table-of-contents) • 839 | 840 | ## 📙 Resources 841 | 842 | There's a ton of resources available out there, here are just a few I've found useful... 843 | 844 | ### Reading 845 | 846 | - Yoni Goldberg - "Writing clean JavaScript tests with the BASIC principles": https://yonigoldberg.medium.com/fighting-javascript-tests-complexity-with-the-basic-principles-87b7622eac9a 847 | - Testim - "Unit Testing Best Practices: 9 to Ensure You Do It Right": https://www.testim.io/blog/unit-testing-best-practices/ 848 | - Vladimir Khorikov - "DRY vs DAMP in Unit Tests": https://enterprisecraftsmanship.com/posts/dry-damp-unit-tests/ 849 | - Georgina McFadyen - "TDD - From the Inside Out or the Outside In?": https://8thlight.com/blog/georgina-mcfadyen/2016/06/27/inside-out-tdd-vs-outside-in.html 850 | - Sandi Metz - "Make Everything The Same": https://sandimetz.com/blog/2016/6/9/make-everything-the-same 851 | - Varun Vachhar - "How to actually test UIs": https://storybook.js.org/blog/how-to-actually-test-uis/ 852 | - Rebecca Murphy - "Writing Testable JavaScript": http://alistapart.com/article/writing-testable-javascript 853 | - James Shore - "Red-Green-Refactor 854 | ": https://www.jamesshore.com/v2/blog/2005/red-green-refactor 855 | - Martin Fowler - "Refactoring": https://refactoring.com/ 856 | 857 | ### Listening 858 | 859 | - Making Tech Better Podcast - "When is a test not a test? with Daniel Terhorst-North": https://www.madetech.com/resources/podcasts/episode-18-daniel-terhorst-north/ 860 | 861 | ### Watching 862 | 863 | - Assert(js) Conference (2019): https://www.youtube.com/watch?v=UOOuW5tqT8M&list=PLZ66c9_z3umMtAboEKsHXQWB0YMJje7Tl 864 | - Assert(js) Conference (2018): https://www.youtube.com/watch?v=zqdCM8zR6Mc&list=PLZ66c9_z3umNSrKSb5cmpxdXZcIPNvKGw 865 | - James Shore - "TDD Lunch & Learn 866 | ": https://www.jamesshore.com/v2/projects/lunch-and-learn 867 | - Roy Osherove - "JS Unit Testing Good Practices and Horrible Mistakes": https://www.youtube.com/watch?v=iP0Vl-vU3XM 868 | - José Armesto - "Unit Testing sucks (and it’s our fault)": https://www.youtube.com/watch?v=GZ9iZsMAZFQ 869 | 870 | ### Tools 871 | 872 | #### Unit testing libraries 873 | 874 | - Jest: https://jestjs.io/ 875 | - Mocha: https://mochajs.org/ 876 | - Node TAP: https://github.com/tapjs/node-tap 877 | - Tape: https://github.com/substack/tape 878 | 879 | #### End-to-end testing tools 880 | 881 | - Cypress: https://www.cypress.io/ 882 | - Playwright: https://playwright.dev/ 883 | 884 | • [Back to ToC](#-table-of-contents) • 885 | 886 | ### Code katas 887 | 888 | - https://kata-log.rocks/index.html 889 | - http://codekata.com/ 890 | - https://github.com/gamontal/awesome-katas 891 | - https://github.com/cesalberca/katas 892 | - https://github.com/emilybache 893 | 894 | ## 🌐 Translations 895 | 896 | This style guide is also available in other languages: 897 | 898 | - 🇨🇳 [Chinese (Simplified)](https://github.com/mawrkus/js-unit-testing-guide/tree/master/translations/zh-cn/README.md) - Thanks to [GabrielchenCN](https://github.com/GabrielchenCN)! 899 | 900 | • [Back to ToC](#-table-of-contents) • 901 | 902 | ## 🫶🏾 Contributors 903 | 904 | 905 | 906 | 907 | -------------------------------------------------------------------------------- /translations/zh-cn/README.md: -------------------------------------------------------------------------------- 1 | # 使用 JavaScript 进行单元测试的指南 2 | 3 | ## 这是一个持续维护的文件。新思想总是受欢迎的。贡献可通过:fork, clone, branch, commit, push, pull request 4 | 5 | ### 免责声明 6 | 7 | > 所提供的所有信息均已根据文件末尾引用的参考文献进行编辑和改编。 8 | 9 | > 这些指导原则通过我自己的例子、我个人编写和审查单元测试的经验成果来说明。 10 | 11 | > 非常感谢所有的信息来源和贡献者。 12 | 13 | 📅 _Last edit: January 2020._ 14 | 15 | ## 📖 目录 16 | 17 | 1. 一般原则 18 | 19 | - [单元测试](#单元测试) 20 | - [设计原则](#设计原则) 21 | 22 | 2. 规范 23 | 24 | - [只要可能,就使用 TDD](#只要可能就使用tdd) 25 | - [正确地组织测试](#正确地组织测试) 26 | - [正确地命名您的测试](#正确地命名您的测试) 27 | - [不要注释掉测试](#不要注释掉测试) 28 | - [在测试中避免逻辑](#在测试中避免逻辑) 29 | - [不要写不必要的测试期望](#不要写不必要的测试期望) 30 | - [正确地设置应用于所有相关测试的操作](#正确地设置应用于所有相关测试的操作) 31 | - [考虑在测试中使用工厂模式](#考虑在测试中使用工厂模式) 32 | - [了解你的测试框架 API](#了解你的测试框架api) 33 | - [不要在同一个测试中测试多个功能点](#不要在同一个测试中测试多个功能点) 34 | - [涵盖一般情况和边缘情况](#涵盖一般情况和边缘情况) 35 | - [在应用 TDD 时,总是从编写最简单的失败测试开始](#在应用tdd时总是从编写最简单的失败测试开始) 36 | - [在应用 TDD 时,总是在每个测试优先的周期中执行一些小步骤](#在应用tdd时总是在每个测试优先的周期中执行一些小步骤) 37 | - [测试行为,而不是内部实现](#测试行为而不是内部实现) 38 | - [不要模拟所有东西](#不要模拟所有东西) 39 | - [为每个缺陷创建新的测试](#为每个缺陷创建新的测试) 40 | - [不要为复杂的用户交互编写单元测试](#不要为复杂的用户交互编写单元测试) 41 | - [测试简单的用户操作](#测试简单的用户操作) 42 | - [首先检查测试代码](#首先检查测试代码) 43 | - [练习代码,学习结对编程](#练习代码学习结对编程) 44 | 45 | 3. [资源](#-resources) 46 | 47 | ## 一般原则 48 | 49 | ### 单元测试 50 | 51 | **Unit = Unit of work** 52 | 53 | 这可能涉及**多个方法和类**调用一些公共 API,可以: 54 | 55 | - 返回一个值或抛出一个异常 56 | - 改变系统的状态 57 | - 进行第三方调用(API,数据库,…) 58 | 59 | 单元测试应该测试工作单元的行为:对于给定的输入,它期望的最终结果可以是上面的任意一个。 60 | 61 | **单元测试是相互独立的** 62 | 63 | - 任何给定的行为都应该在**一个且只有一个测试**中指定 64 | 65 | - 一个测试的执行/执行顺序**不会影响其他** 66 | 67 | 代码的设计目的就是支持这种独立性(参见下面的“设计原则”)。 68 | 69 | **单元测试是轻量级测试** 70 | 71 | - 可重复的 72 | - 快速的 73 | - 一致的 74 | - 容易读写的 75 | 76 | **单元测试也是代码** 77 | 78 | 它们应该达到与正在测试的代码相同的质量级别。还可以对它们进行重构,使它们更易于维护和可读。 79 | 80 | • [返回目录](#-目录) • 81 | 82 | ### 设计原则 83 | 84 | 好的单元测试的关键是编写**可测试代码**。应用简单的设计原则会有所帮助,特别是: 85 | 86 | - 使用**良好的命名**约定和**注释**您的代码(表明“为什么”这样写而不是“如何”这样写),请记住,注释不能代替糟糕的命名或糟糕的设计 87 | 88 | - **DRY**:不要重复自己,避免代码重复 89 | 90 | - **单一职责**每个对象/函数必须专注于一个任务 91 | 92 | - 在同一组件中保持**单一抽象级别**(例如,不要在同一方法中混合业务逻辑和较低级别的技术细节) 93 | 94 | - 在组件之间使用**最小依赖**:封装组件之间的信息,减少组件之间的信息交换 95 | 96 | - **支持可配置性**而不是硬编码,这避免了在测试时必须复制完全相同的环境(例如:标记)。 97 | 98 | - 应用适当的**设计模式**,特别是**依赖项注入**,它允许将对象的创建职责与业务逻辑分离 99 | 100 | - 避免全局可变状态 101 | 102 | • [返回目录](#-目录) • 103 | 104 | ## 指南 105 | 106 | 这些指南的目的是让你的测试: 107 | 108 | - **可读** 109 | - **可维护** 110 | - **可信赖** 111 | 112 | 这是好的单元测试的三大支柱。 113 | 114 | 以下所有示例都假设使用了[Jasmine](http://jasmine.github.io)框架。 115 | 116 | • [返回目录](#-目录) • 117 | 118 | --- 119 | 120 | ### 只要可能,就使用 TDD 121 | 122 | TDD 是一个设计过程,而不是一个测试过程。TDD 是一种健壮的交互设计软件组件(“单元”)的方法,以便通过单元测试指定它们的行为。 123 | 124 | 怎么做?为什么这样做 125 | 126 | #### 测试优先的周期 127 | 128 | 1. 编写一个简单的失败测试 129 | 130 | 2. 通过编写最少数量的代码来通过测试,而不必担心代码质量 131 | 132 | 3. 通过应用设计原则/设计模式重构代码 133 | 134 | #### 测试优先周期的结果 135 | 136 | - 首先编写测试用例使代码设计实际上是可测试的 137 | - 只需编写实现所需功能所需的代码量就可以使生成的代码库最小化,从而提高可维护性 138 | - 可以使用重构机制来增强代码库,测试使您确信新代码不会修改现有功能 139 | - 在每个周期中清理代码使代码库更容易维护,频繁地、小幅度地更改代码要简单得多 140 | - 对开发人员的快速反馈,您知道您没有破坏任何东西,并且您正在朝着一个好的方向发展系统 141 | - 拥有添加特性、修复 bug 或探索新设计的信心 142 | 143 | 注意,不使用测试优先方法编写的代码通常很难测试。 144 | 145 | • [返回目录](#-目录) • 146 | 147 | ### 正确地组织测试 148 | 149 | 不要犹豫将您的套件嵌套在子集中,以便在逻辑上构造您的测试。 150 | 151 | **:(** 152 | 153 | ```js 154 | // 不好的示范 155 | // 一组功能 156 | describe("A set of functionalities", () => { 157 | // 一组功能的测试 158 | it("a set of functionalities should do something nice", () => {}); 159 | // 一组功能子集的测试 160 | it("a subset of functionalities should do something great", () => {}); 161 | 162 | it("a subset of functionalities should do something awesome", () => {}); 163 | // 一组功能另一子集的测试 164 | it("another subset of functionalities should also do something great", () => {}); 165 | }); 166 | ``` 167 | 168 | **:)** 169 | 170 | ```js 171 | // 正确的示范 172 | // 一组功能的测试 173 | describe("A set of functionalities", () => { 174 | it("should do something nice", () => {}); 175 | // 一组功能子集的测试 176 | describe("A subset of functionalities", () => { 177 | it("should do something great", () => {}); 178 | 179 | it("should do something awesome", () => {}); 180 | }); 181 | // 一组功能另一子集的测试 182 | describe("Another subset of functionalities", () => { 183 | it("should also do something great", () => {}); 184 | }); 185 | }); 186 | ``` 187 | 188 | • [返回目录](#-目录) • 189 | 190 | ### 正确地命名您的测试 191 | 192 | 测试用例的名称应简洁、明确、描述性强,英文正确。阅读 spec runner 的输出并验证它是可理解的!记住,别人也会读它。测试可以是代码的实时文档。 193 | 194 | **:(** 195 | 196 | ```js 197 | // 不好的示范 198 | describe("MyGallery", () => { 199 | it("init set correct property when called (thumb size, thumbs count)", () => {}); 200 | 201 | // ... 202 | }); 203 | ``` 204 | 205 | **:)** 206 | 207 | ```js 208 | // 正确的示范 209 | describe("The Gallery instance", () => { 210 | it("should properly calculate the thumb size when initialized", () => {}); 211 | 212 | it("should properly calculate the thumbs count when initialized", () => {}); 213 | 214 | // ... 215 | }); 216 | ``` 217 | 218 | 为了帮助您正确地编写测试名称,您可以使用**“工作单元[unit of work]—场景/上下文[scenario/context]—期望行为[expected behaviour]”**模式: 219 | 220 | ```js 221 | // 不好的示范 222 | describe("[unit of work]", () => { 223 | it("should [expected behaviour] when [scenario/context]", () => {}); 224 | }); 225 | ``` 226 | 227 | 或者当你有很多测试遵循相同的场景或与相同的上下文相关: 228 | 229 | ```js 230 | // 正确的示范 231 | describe("[unit of work]", () => { 232 | describe("when [scenario/context]", () => { 233 | it("should [expected behaviour]", () => {}); 234 | }); 235 | }); 236 | ``` 237 | 238 | 例如: 239 | 240 | **:) :)** 241 | 242 | ```js 243 | // 优秀的示范 244 | describe("The Gallery instance", () => { 245 | describe("when initialized", () => { 246 | it("should properly calculate the thumb size", () => {}); 247 | 248 | it("should properly calculate the thumbs count", () => {}); 249 | }); 250 | 251 | // ... 252 | }); 253 | ``` 254 | 255 | • [返回目录](#-目录) • 256 | 257 | ### 不要注释掉测试 258 | 259 | 永不,绝不!测试是有原因的。 260 | 261 | 不要因为它们太慢、太复杂或产生错误的结果而把它们注释掉。相反,让他们快速,简单和值得信赖。如果没有,就把它们完全移除。 262 | 263 | • [返回目录](#-目录) • 264 | 265 | ### 在测试中避免逻辑 266 | 267 | 使用简单的语句。不要使用循环或条件语句。如果你这样做了,你就为测试本身添加了一个可能的 bug 点: 268 | 269 | - 条件:您不知道测试将采用哪条路径 270 | - 循环:你可能在测试之间共享状态 271 | 272 | **:(** 273 | 274 | ```js 275 | it("should properly sanitize strings", () => { 276 | let result; 277 | const testValues = { 278 | Avion: "Avi" + String.fromCharCode(243) + "n", 279 | "The-space": "The space", 280 | "Weird-chars-": "Weird chars!!", 281 | "file-name.zip": "file name.zip", 282 | "my-name.zip": "my.name.zip", 283 | }; 284 | 285 | for (result in testValues) { 286 | expect(sanitizeString(testValues[result])).toBe(result); 287 | } 288 | }); 289 | ``` 290 | 291 | **:)** 292 | 293 | ```js 294 | it("should properly sanitize strings", () => { 295 | expect(sanitizeString("Avi" + String.fromCharCode(243) + "n")).toBe("Avion"); 296 | expect(sanitizeString("The space")).toBe("The-space"); 297 | expect(sanitizeString("Weird chars!!")).toBe("Weird-chars-"); 298 | expect(sanitizeString("file name.zip")).toBe("file-name.zip"); 299 | expect(sanitizeString("my.name.zip")).toBe("my-name.zip"); 300 | }); 301 | ``` 302 | 303 | 更好的做法是:为每种 sanitizeString 方法写一个测试。它将输出所有可能的情况,提高可维护性。 304 | 305 | **:) :)** 306 | 307 | ```js 308 | it("should sanitize a string containing non-ASCII chars", () => { 309 | expect(sanitizeString("Avi" + String.fromCharCode(243) + "n")).toBe("Avion"); 310 | }); 311 | 312 | it("should sanitize a string containing spaces", () => { 313 | expect(sanitizeString("The space")).toBe("The-space"); 314 | }); 315 | 316 | it("should sanitize a string containing exclamation signs", () => { 317 | expect(sanitizeString("Weird chars!!")).toBe("Weird-chars-"); 318 | }); 319 | 320 | it("should sanitize a filename containing spaces", () => { 321 | expect(sanitizeString("file name.zip")).toBe("file-name.zip"); 322 | }); 323 | 324 | it("should sanitize a filename containing more than one dot", () => { 325 | expect(sanitizeString("my.name.zip")).toBe("my-name.zip"); 326 | }); 327 | ``` 328 | 329 | • [返回目录](#-目录) • 330 | 331 | ### 不要写不必要的测试期望 332 | 333 | 请记住,单元测试是某个“行为”应该如何工作的设计规范,而不是代码碰巧要做的所有事情的观察列表。 334 | 335 | **:(** 336 | 337 | ```js 338 | it("should multiply the number passed as parameter and subtract one", () => { 339 | const multiplySpy = spyOn(Calculator, "multiple").and.callThrough(); 340 | const subtractSpy = spyOn(Calculator, "subtract").and.callThrough(); 341 | 342 | const result = Calculator.compute(21.5); 343 | 344 | expect(multiplySpy).toHaveBeenCalledWith(21.5, 2); 345 | expect(subtractSpy).toHaveBeenCalledWith(43, 1); 346 | expect(result).toBe(42); 347 | }); 348 | ``` 349 | 350 | **:)** 351 | 352 | ```js 353 | it("should multiply the number passed as parameter and subtract one", () => { 354 | const result = Calculator.compute(21.5); 355 | expect(result).toBe(42); 356 | }); 357 | ``` 358 | 359 | 这将提高可维护性。您的测试不再与实现细节绑定。 360 | 361 | • [返回目录](#-目录) • 362 | 363 | ### 正确地设置应用于所有相关测试的操作 364 | 365 | **:(** 366 | 367 | ```js 368 | describe("Saving the user profile", () => { 369 | let profileModule; 370 | let notifyUserSpy; 371 | let onCompleteSpy; 372 | 373 | beforeEach(() => { 374 | profileModule = new ProfileModule(); 375 | notifyUserSpy = spyOn(profileModule, "notifyUser"); 376 | onCompleteSpy = jasmine.createSpy(); 377 | }); 378 | 379 | it("should send the updated profile data to the server", () => { 380 | jasmine.Ajax.install(); 381 | 382 | profileModule.save(); 383 | 384 | const request = jasmine.Ajax.requests.mostRecent(); 385 | 386 | expect(request.url).toBe("/profiles/1"); 387 | expect(request.method).toBe("POST"); 388 | expect(request.data()).toEqual({ username: "mawrkus" }); 389 | 390 | jasmine.Ajax.uninstall(); 391 | }); 392 | 393 | it("should notify the user", () => { 394 | jasmine.Ajax.install(); 395 | 396 | profileModule.save(); 397 | 398 | expect(notifyUserSpy).toHaveBeenCalled(); 399 | 400 | jasmine.Ajax.uninstall(); 401 | }); 402 | 403 | it("should properly execute the callback passed as parameter", () => { 404 | jasmine.Ajax.install(); 405 | 406 | profileModule.save(onCompleteSpy); 407 | 408 | jasmine.Ajax.uninstall(); 409 | 410 | expect(onCompleteSpy).toHaveBeenCalled(); 411 | }); 412 | }); 413 | ``` 414 | 415 | 设置代码应该适用于所有的测试: 416 | 417 | **:)** 418 | 419 | ```js 420 | describe("Saving the user profile", () => { 421 | let profileModule; 422 | 423 | beforeEach(() => { 424 | jasmine.Ajax.install(); 425 | profileModule = new ProfileModule(); 426 | }); 427 | 428 | afterEach(() => { 429 | jasmine.Ajax.uninstall(); 430 | }); 431 | 432 | it("should send the updated profile data to the server", () => { 433 | profileModule.save(); 434 | 435 | const request = jasmine.Ajax.requests.mostRecent(); 436 | 437 | expect(request.url).toBe("/profiles/1"); 438 | expect(request.method).toBe("POST"); 439 | }); 440 | 441 | it("should notify the user", () => { 442 | spyOn(profileModule, "notifyUser"); 443 | 444 | profileModule.save(); 445 | 446 | expect(profileModule.notifyUser).toHaveBeenCalled(); 447 | }); 448 | 449 | it("should properly execute the callback passed as parameter", () => { 450 | const onCompleteSpy = jasmine.createSpy(); 451 | 452 | profileModule.save(onCompleteSpy); 453 | 454 | expect(onCompleteSpy).toHaveBeenCalled(); 455 | }); 456 | }); 457 | ``` 458 | 459 | 考虑将设置代码保持在最小以保持可读性和可维护性 460 | 461 | • [返回目录](#-目录) • 462 | 463 | ### 考虑在测试中使用工厂模式 464 | 465 | 工厂模式可以: 466 | 467 | - 帮助减少设置代码,特别是在使用依赖项注入时 468 | - 使每个测试更具可读性,因为创建是一个单独的函数调用,可以在测试本身中而不是在设置中 469 | - 在创建新实例时提供灵活性(例如,设置初始状态) 470 | 471 | 在应用 DRY 原则和可读性之间需要权衡。 472 | 473 | **:(** 474 | 475 | ```js 476 | describe("User profile module", () => { 477 | let profileModule; 478 | let pubSub; 479 | 480 | beforeEach(() => { 481 | const element = document.getElementById("my-profile"); 482 | pubSub = new PubSub({ sync: true }); 483 | 484 | profileModule = new ProfileModule({ 485 | element, 486 | pubSub, 487 | likes: 0, 488 | }); 489 | }); 490 | 491 | it('should publish a topic when a new "like" is given', () => { 492 | spyOn(pubSub, "notify"); 493 | profileModule.incLikes(); 494 | expect(pubSub.notify).toHaveBeenCalledWith("likes:inc", { count: 1 }); 495 | }); 496 | 497 | it("should retrieve the correct number of likes", () => { 498 | profileModule.incLikes(); 499 | profileModule.incLikes(); 500 | expect(profileModule.getLikes()).toBe(2); 501 | }); 502 | }); 503 | ``` 504 | 505 | **:)** 506 | 507 | ```js 508 | describe("User profile module", () => { 509 | function createProfileModule({ 510 | element = document.getElementById("my-profile"), 511 | likes = 0, 512 | pubSub = new PubSub({ sync: true }), 513 | }) { 514 | return new ProfileModule({ element, likes, pubSub }); 515 | } 516 | 517 | it('should publish a topic when a new "like" is given', () => { 518 | const pubSub = jasmine.createSpyObj("pubSub", ["notify"]); 519 | const profileModule = createProfileModule({ pubSub }); 520 | 521 | profileModule.incLikes(); 522 | 523 | expect(pubSub.notify).toHaveBeenCalledWith("likes:inc"); 524 | }); 525 | 526 | it("should retrieve the correct number of likes", () => { 527 | const profileModule = createProfileModule({ likes: 40 }); 528 | 529 | profileModule.incLikes(); 530 | profileModule.incLikes(); 531 | 532 | expect(profileModule.getLikes()).toBe(42); 533 | }); 534 | }); 535 | ``` 536 | 537 | 工厂模式在处理 DOM 时特别有用: 538 | 539 | **:(** 540 | 541 | ```js 542 | describe("The search component", () => { 543 | describe("when the search button is clicked", () => { 544 | let container; 545 | let form; 546 | let searchInput; 547 | let submitInput; 548 | 549 | beforeEach(() => { 550 | fixtures.inject(`
551 |
552 | 553 | 554 |
555 |
`); 556 | 557 | container = document.getElementById("container"); 558 | form = container.getElementsByClassName("js-form")[0]; 559 | searchInput = form.querySelector("input[type=search]"); 560 | submitInput = form.querySelector("input[type=submith]"); 561 | }); 562 | 563 | it("should validate the text entered", () => { 564 | const search = new Search({ container }); 565 | spyOn(search, "validate"); 566 | 567 | search.init(); 568 | 569 | input(searchInput, "peace"); 570 | click(submitInput); 571 | 572 | expect(search.validate).toHaveBeenCalledWith("peace"); 573 | }); 574 | 575 | // ... 576 | }); 577 | }); 578 | ``` 579 | 580 | **:)** 581 | 582 | ```js 583 | function createHTMLFixture() { 584 | fixtures.inject(`
585 |
586 | 587 | 588 |
589 |
`); 590 | 591 | const container = document.getElementById("container"); 592 | const form = container.getElementsByClassName("js-form")[0]; 593 | const searchInput = form.querySelector("input[type=search]"); 594 | const submitInput = form.querySelector("input[type=submith]"); 595 | 596 | return { 597 | container, 598 | form, 599 | searchInput, 600 | submitInput, 601 | }; 602 | } 603 | 604 | describe("The search component", () => { 605 | describe("when the search button is clicked", () => { 606 | it("should validate the text entered", () => { 607 | const { container, form, searchInput, submitInput } = createHTMLFixture(); 608 | const search = new Search({ container }); 609 | spyOn(search, "validate"); 610 | 611 | search.init(); 612 | 613 | input(searchInput, "peace"); 614 | click(submitInput); 615 | 616 | expect(search.validate).toHaveBeenCalledWith("peace"); 617 | }); 618 | 619 | // ... 620 | }); 621 | }); 622 | ``` 623 | 624 | • [返回目录](#-目录) • 625 | 626 | ### 了解你的测试框架 API 627 | 628 | 测试框架/库的 API 文档应该是你的枕边书! 629 | 630 | 对 API 有良好的了解可以帮助您减少测试代码的大小/复杂性,并且通常在开发过程中对您有帮助。一个简单的例子: 631 | 632 | **:(** 633 | 634 | ```js 635 | it("should call a method with the proper arguments", () => { 636 | const foo = { 637 | bar: jasmine.createSpy(), 638 | baz: jasmine.createSpy(), 639 | }; 640 | 641 | foo.bar("qux"); 642 | 643 | expect(foo.bar).toHaveBeenCalled(); 644 | expect(foo.bar.calls.argsFor(0)).toEqual(["qux"]); 645 | }); 646 | 647 | /*it('should do more but not now', () => { 648 | }); 649 | 650 | it('should do much more but not now', () => { 651 | });*/ 652 | ``` 653 | 654 | **:)** 655 | 656 | ```js 657 | fit("should call once a method with the proper arguments", () => { 658 | const foo = jasmine.createSpyObj("foo", ["bar", "baz"]); 659 | 660 | foo.bar("baz"); 661 | 662 | expect(foo.bar).toHaveBeenCalledWith("baz"); 663 | }); 664 | 665 | it("should do something else but not now", () => {}); 666 | 667 | it("should do something else but not now", () => {}); 668 | ``` 669 | 670 | #### 注意 671 | 672 | 上面示例中使用的方便的`fit`函数允许您只执行一个测试,而不必注释掉下面的所有测试。`fdescribe`也适用于测试套件。这有助于在开发时节省大量时间。 673 | 674 | 更多信息,请查看 [Jasmine website](http://jasmine.github.io). 675 | 676 | • [返回目录](#-目录) • 677 | 678 | ### 不要在同一个测试中测试多个功能点 679 | 680 | 如果一个方法有多个最终结果,那么应该分别测试每个结果。当 bug 发生时,它将帮助您定位问题的根源。 681 | 682 | **:(** 683 | 684 | ```js 685 | it("should send the profile data to the server and update the profile view properly", () => { 686 | // expect(...)to(...); 687 | // expect(...)to(...); 688 | }); 689 | ``` 690 | 691 | **:)** 692 | 693 | ```js 694 | it("should send the profile data to the server", () => { 695 | // expect(...)to(...); 696 | }); 697 | 698 | it("should update the profile view properly", () => { 699 | // expect(...)to(...); 700 | }); 701 | ``` 702 | 703 | 注意,当命名您的测试时写“AND”或“OR”,感觉很不好… 704 | 705 | • [返回目录](#-目录) • 706 | 707 | ### 涵盖一般情况和边缘情况 708 | 709 | “奇怪的行为”通常发生在边缘……请记住,测试可以是代码的实时文档。 710 | 711 | **:(** 712 | 713 | ```js 714 | it("should properly calculate a RPN expression", () => { 715 | const result = RPN("5 1 2 + 4 * - 10 /"); 716 | expect(result).toBe(-0.7); 717 | }); 718 | ``` 719 | 720 | **:)** 721 | 722 | ```js 723 | describe("The RPN expression evaluator", () => { 724 | it("should return null when the expression is an empty string", () => { 725 | const result = RPN(""); 726 | expect(result).toBeNull(); 727 | }); 728 | 729 | it("should return the same value when the expression holds a single value", () => { 730 | const result = RPN("42"); 731 | expect(result).toBe(42); 732 | }); 733 | 734 | it("should properly calculate an expression", () => { 735 | const result = RPN("5 1 2 + 4 * - 10 /"); 736 | expect(result).toBe(-0.7); 737 | }); 738 | 739 | it("should throw an error whenever an invalid expression is passed", () => { 740 | const compute = () => RPN("1 + - 1"); 741 | expect(compute).toThrow(); 742 | }); 743 | }); 744 | ``` 745 | 746 | • [返回目录](#-目录) • 747 | 748 | ### 在应用 TDD 时,总是从编写最简单的失败测试开始 749 | 750 | **:(** 751 | 752 | ```js 753 | it("should suppress all chars that appear multiple times", () => { 754 | expect(keepUniqueChars("Hello Fostonic !!")).toBe("HeFstnic"); 755 | }); 756 | ``` 757 | 758 | **:)** 759 | 760 | ```js 761 | it("should return an empty string when passed an empty string", () => { 762 | expect(keepUniqueChars("")).toBe(""); 763 | }); 764 | ``` 765 | 766 | 在此基础上,开始逐步构建功能。 767 | 768 | • [返回目录](#-目录) • 769 | 770 | ### 在应用 TDD 时,总是在每个测试优先的周期中执行一些小步骤 771 | 772 | 构建您的测试套件,从简单的案例到更复杂的案例。记住增量式设计。快速、增量地、短迭代地交付软件。 773 | 774 | **:(** 775 | 776 | ```js 777 | it("should return null when the expression is an empty string", () => { 778 | const result = RPN(""); 779 | expect(result).toBeNull(); 780 | }); 781 | 782 | it("should properly calculate a RPN expression", () => { 783 | const result = RPN("5 1 2 + 4 * - 10 /"); 784 | expect(result).toBe(-0.7); 785 | }); 786 | ``` 787 | 788 | **:)** 789 | 790 | ```js 791 | describe("The RPN expression evaluator", () => { 792 | it("should return null when the expression is an empty string", () => { 793 | const result = RPN(""); 794 | expect(result).toBeNull(); 795 | }); 796 | 797 | it("should return the same value when the expression holds a single value", () => { 798 | const result = RPN("42"); 799 | expect(result).toBe(42); 800 | }); 801 | 802 | describe("Additions-only expressions", () => { 803 | it("should properly calculate a simple addition", () => { 804 | const result = RPN("41 1 +"); 805 | expect(result).toBe(42); 806 | }); 807 | 808 | it("should properly calculate a complex addition", () => { 809 | const result = RPN("2 9 + 15 3 + + 7 6 + +"); 810 | expect(result).toBe(42); 811 | }); 812 | }); 813 | 814 | // ... 815 | 816 | describe("Complex expressions", () => { 817 | it("should properly calculate an expression containing all 4 operators", () => { 818 | const result = RPN("5 1 2 + 4 * - 10 /"); 819 | expect(result).toBe(-0.7); 820 | }); 821 | }); 822 | }); 823 | ``` 824 | 825 | • [返回目录](#-目录) • 826 | 827 | ### 测试行为,而不是内部实现 828 | 829 | **:(** 830 | 831 | ```js 832 | it("should add a user in memory", () => { 833 | userManager.addUser("Dr. Falker", "Joshua"); 834 | 835 | expect(userManager._users[0].name).toBe("Dr. Falker"); 836 | expect(userManager._users[0].password).toBe("Joshua"); 837 | }); 838 | ``` 839 | 840 | 一个更好的方法是在 API 的同一级别进行测试: 841 | 842 | **:)** 843 | 844 | ```js 845 | it("should add a user in memory", () => { 846 | userManager.addUser("Dr. Falker", "Joshua"); 847 | 848 | expect(userManager.loginUser("Dr. Falker", "Joshua")).toBe(true); 849 | }); 850 | ``` 851 | 852 | Pro: 853 | 854 | - 更改类/对象的内部实现并不一定会强制您重构测试 855 | 856 | Con: 857 | 858 | - 如果测试失败,我们可能必须进行调试才能知道需要修复代码的哪一部分 859 | 860 | 在这里,必须找到一个平衡,单元测试的一些关键部分可能是有益的。 861 | 862 | • [返回目录](#-目录) • 863 | 864 | ### 不要模拟所有东西 865 | 866 | **:(** 867 | 868 | ```js 869 | describe("when the user has already visited the page", () => { 870 | // storage.getItem('page-visited', '1') === '1' 871 | describe("when the survey is not disabled", () => { 872 | // storage.getItem('survey-disabled') === null 873 | it("should display the survey", () => { 874 | const storage = jasmine.createSpyObj("storage", ["setItem", "getItem"]); 875 | storage.getItem.and.returnValue("1"); // ouch. 876 | 877 | const surveyManager = new SurveyManager(storage); 878 | spyOn(surveyManager, "display"); 879 | 880 | surveyManager.start(); 881 | 882 | expect(surveyManager.display).toHaveBeenCalled(); 883 | }); 884 | }); 885 | 886 | // ... 887 | }); 888 | ``` 889 | 890 | 此测试失败,因为调查被认为是禁用的。让我们解决这个问题: 891 | 892 | **:)** 893 | 894 | ```js 895 | describe("when the user has already visited the page", () => { 896 | // storage.getItem('page-visited', '1') === '1' 897 | describe("when the survey is not disabled", () => { 898 | // storage.getItem('survey-disabled') === null 899 | it("should display the survey", () => { 900 | const storage = jasmine.createSpyObj("storage", ["setItem", "getItem"]); 901 | storage.getItem.and.callFake((key) => { 902 | switch (key) { 903 | case "page-visited": 904 | return "1"; 905 | 906 | case "survey-disabled": 907 | return null; 908 | } 909 | 910 | return null; 911 | }); // ouch. 912 | 913 | const surveyManager = new SurveyManager(storage); 914 | spyOn(surveyManager, "display"); 915 | 916 | surveyManager.start(); 917 | 918 | expect(surveyManager.display).toHaveBeenCalled(); 919 | }); 920 | }); 921 | 922 | // ... 923 | }); 924 | ``` 925 | 926 | 这样写可以……但是需要大量的代码。让我们尝试一个更简单的方法: 927 | 928 | **:(** 929 | 930 | ```js 931 | describe("when the user has already visited the page", () => { 932 | // storage.getItem('page-visited', '1') === '1' 933 | describe("when the survey is not disabled", () => { 934 | // storage.getItem('survey-disabled') === null 935 | it("should display the survey", () => { 936 | const storage = window.localStorage; // ouch. 937 | storage.setItem("page-visited", "1"); 938 | 939 | const surveyManager = new SurveyManager(); 940 | spyOn(surveyManager, "display"); 941 | 942 | surveyManager.start(); 943 | 944 | expect(surveyManager.display).toHaveBeenCalled(); 945 | }); 946 | }); 947 | 948 | // ... 949 | }); 950 | ``` 951 | 952 | 我们创建了一个永久的数据存储。如果我们不好好清洁,会发生什么? 953 | 我们可能会影响其他的测试。让我们解决这个问题: 954 | 955 | **:) :)** 956 | 957 | ```js 958 | describe("when the user has already visited the page", () => { 959 | // storage.getItem('page-visited', '1') === '1' 960 | describe("when the survey is not disabled", () => { 961 | // storage.getItem('survey-disabled') === null 962 | it("should display the survey", () => { 963 | const storage = new MemoryStorage(); // see https://github.com/tatsuyaoiw/webstorage 964 | storage.setItem("page-visited", "1"); 965 | 966 | const surveyManager = new SurveyManager(storage); 967 | spyOn(surveyManager, "display"); 968 | 969 | surveyManager.start(); 970 | 971 | expect(surveyManager.display).toHaveBeenCalled(); 972 | }); 973 | }); 974 | }); 975 | ``` 976 | 977 | 这里使用的`MemoryStorage` 不持久化数据。又好又简单,没有副作用。 978 | 979 | #### 请注意 980 | 981 | 需要记住的是,依赖项仍然可以是“真实的”对象。不要因为你可以 mock 所有东西而去 mock 一切东西。如果下列情况,请考虑使用对象的“真实”版本: 982 | 983 | - 它带来了一个简单、漂亮和容易的测试设置 984 | - 它不会在测试之间创建共享状态,从而导致意外的副作用 985 | - 正在测试的代码不会发出 AJAX 请求、API 调用或重新加载浏览器页面 986 | - 测试的执行速度保持在您确定的范围内 987 | 988 | • [返回目录](#-目录) • 989 | 990 | ### 为每个缺陷创建新的测试 991 | 992 | 当发现 bug 时,在**接触任何代码之前**创建一个复制问题的测试。然后,您可以像往常一样应用 TDD 来修复它。 993 | 994 | • [返回目录](#-目录) • 995 | 996 | ### 不要为复杂的用户交互编写单元测试 997 | 998 | 复杂用户交互的例子: 999 | 1000 | - 填写表单,拖放一些项目,然后提交表单 1001 | 1002 | - 点击一个选项卡,点击一个图像缩略图,然后从在数据库预加载的图像库中导航 1003 | 1004 | - (...) 1005 | 1006 | 这些交互可能涉及许多工作单元,应该通过**功能测试**在更高的级别上进行处理。他们需要更多的时间来执行。它们可能是不可靠的(假阴性),并且在报告失败时需要进行调试。 1007 | 1008 | 对于功能测试,可以考虑使用测试自动化框架([Selenium](http://docs.seleniumhq.org/)或 QA 手动测试。 1009 | 1010 | • [返回目录](#-目录) • 1011 | 1012 | ### 测试简单的用户操作 1013 | 1014 | 简单用户操作示例: 1015 | 1016 | - 点击切换 DOM 元素可见性的链接 1017 | 1018 | - 提交触发表单验证的表单 1019 | 1020 | - (...) 1021 | 1022 | 这些动作可以通过模拟 DOM 事件**轻松测试**,例如: 1023 | 1024 | ```js 1025 | describe('clicking on the "Preview profile" link', () => { 1026 | it("should show the profile preview if it is hidden", () => { 1027 | const previewLink = document.createElement("a"); 1028 | const profileModule = createProfileModule({ 1029 | previewLink, 1030 | previewIsVisible: false, 1031 | }); 1032 | 1033 | spyOn(profileModule, "showPreview"); 1034 | 1035 | click(previewLink); 1036 | 1037 | expect(profileModule.showPreview).toHaveBeenCalled(); 1038 | }); 1039 | 1040 | it("should hide the profile preview if it is displayed", () => { 1041 | const previewLink = document.createElement("a"); 1042 | const profileModule = createProfileModule({ 1043 | previewLink, 1044 | previewIsVisible: true, 1045 | }); 1046 | 1047 | spyOn(profileModule, "hidePreview"); 1048 | 1049 | click(previewLink); 1050 | 1051 | expect(profileModule.hidePreview).toHaveBeenCalled(); 1052 | }); 1053 | }); 1054 | ``` 1055 | 1056 | 注意测试是多么简单,因为 UI (DOM)层没有与业务逻辑层混合: 1057 | 1058 | - 发生“单击”事件 1059 | - 调用一个公共方法 1060 | 1061 | 下一步可能是测试在“showPreview()”或“hidePreview()”中实现的业务逻辑。 1062 | 1063 | • [返回目录](#-目录) • 1064 | 1065 | ### 首先检查测试代码 1066 | 1067 | 当评审代码时,总是从阅读测试代码开始。测试是可以深入研究的代码的迷你用例。 1068 | 1069 | 它将帮助您非常快速地理解开发人员的意图(可能只是通过查看测试的名称)。 1070 | 1071 | • [返回目录](#-目录) • 1072 | 1073 | ### 练习代码,学习结对编程 1074 | 1075 | 因为经验是唯一的老师。最终,伟大来自实践;一遍又一遍地应用这个理论,利用反馈每次都变得更好。 1076 | 1077 | • [返回目录](#-目录) • 1078 | 1079 | ## 📙 Resources 1080 | 1081 | ### Best practices 1082 | 1083 | - Roy Osherove - "JS Unit Testing Good Practices and Horrible Mistakes": https://www.youtube.com/watch?v=iP0Vl-vU3XM 1084 | - Steven Sanderson - "Writing Great Unit Tests: Best and Worst Practices": http://blog.stevensanderson.com/2009/08/24/writing-great-unit-tests-best-and-worst-practises/ 1085 | - Rebecca Murphy - "Writing Testable JavaScript": http://alistapart.com/article/writing-testable-javascript 1086 | - YUI Team - "Writing Effective JavaScript Unit Tests with YUI Test": http://yuiblog.com/blog/2009/01/05/effective-tests/ 1087 | - Colin Snover - "Testable code best practices": http://www.sitepen.com/blog/2014/07/11/testable-code-best-practices/ 1088 | - Miško Hevery - "The Clean Code Talks -- Unit Testing": https://www.youtube.com/watch?v=wEhu57pih5w 1089 | - José Armesto - "Unit Testing sucks (and it’s our fault)": https://www.youtube.com/watch?v=GZ9iZsMAZFQ 1090 | - TDD - From the Inside Out or the Outside In?: https://8thlight.com/blog/georgina-mcfadyen/2016/06/27/inside-out-tdd-vs-outside-in.html 1091 | 1092 | ### Clean code 1093 | 1094 | - Clean code cheat sheet: http://www.planetgeek.ch/2014/11/18/clean-code-cheat-sheet-v-2-4/ 1095 | - Addy Osmani - "Learning JavaScript Design Patterns": http://addyosmani.com/resources/essentialjsdesignpatterns/book/ 1096 | 1097 | ### BDD 1098 | 1099 | - Enrique Amodeo - "Learning Behavior-driven Development with JavaScript": https://www.packtpub.com/application-development/learning-behavior-driven-development-javascript 1100 | 1101 | ### Events 1102 | 1103 | - Assert(js) Testing Conference 2018: https://www.youtube.com/playlist?list=PLZ66c9_z3umNSrKSb5cmpxdXZcIPNvKGw 1104 | 1105 | ### Libraries 1106 | 1107 | - Jasmine: https://jasmine.github.io/ 1108 | - Jest: https://jestjs.io/ 1109 | - Mocha: https://mochajs.org/ 1110 | - Tape: https://github.com/substack/tape 1111 | 1112 | • [返回目录](#-目录) • 1113 | --------------------------------------------------------------------------------