├── .all-contributorsrc ├── .gitignore ├── .operations └── questions-answers.md ├── LICENSE ├── assets ├── amazon-visual-regression.jpeg ├── bp-1-3-parts.jpeg ├── bp-12-Yoni-Goldberg-Testing.jpeg ├── bp-12-rich-testing.jpeg ├── bp-13-component-test-yoni-goldberg.png ├── bp-14-testing-best-practices-contract-flow.png ├── bp-16-yoni-goldberg-quality.png ├── bp-17-yoni-goldberg-chaos-monkey-nodejs.png ├── bp-18-code-coverage2.jpeg ├── bp-18-yoni-goldberg-code-coverage.png ├── bp-19-coverage-yoni-goldberg-nodejs-consultant.png ├── bp-20-yoni-goldberg-mutation-testing.jpeg ├── bp-21-yoni-goldberg-eslint.jpeg ├── bp-24-yonigoldberg-jest-parallel.png ├── bp-25-nodejs-licsense.png ├── bp-26-npm-audit-snyk.png ├── bp-27-yoni-goldberg-npm.png ├── flat-report.png ├── header-alt.png ├── header-image.png ├── header.pptx ├── headspace.png ├── hierarchical-report.png ├── jest-stars.jpg ├── js-bp-header.jpg ├── js-test-bp-header.png ├── jtbp-header-blue.png ├── jtbp-header-with-checklist.png ├── jtbp-header-with-pipeline.png ├── lighthouse2.png ├── logo-chalkboard.png ├── mind-map.jpg ├── nbptest.jpeg ├── nbtest.jpg ├── readme.md ├── repo-header.png ├── roadmap-brief.png ├── story-book.jpg ├── t.png ├── tbp.png ├── twitter.png ├── uml.png ├── yoni-goldberg.jpg └── zh-CN │ ├── bp-1-3-parts.jpg │ ├── bp-12-rich-testing.jpg │ ├── bp-13-component-test-yoni-goldberg.png │ ├── bp-14-testing-best-practices-contract-flow.png │ ├── bp-20-yoni-goldberg-mutation-testing.jpg │ └── headspace.png ├── index.js ├── package.json ├── readme-es.md ├── readme-fr.md ├── readme-pl.md ├── readme-pr-fr.md ├── readme-pt-br.md ├── readme-ru.md ├── readme-ua.md ├── readme-zh-CN.md ├── readme-zh-TW.md ├── readme.kr.md └── readme.md /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "readme.md" 4 | ], 5 | "imageSize": 100, 6 | "contributorsPerLine": 7, 7 | "badgeTemplate": "[![All Contributors](https://img.shields.io/badge/all_contributors-<%= contributors.length %>-orange.svg?style=flat-square)](#contributors)", 8 | "skipCi": true, 9 | "contributors": [ 10 | { 11 | "login": "stdavis", 12 | "name": "Scott Davis", 13 | "avatar_url": "https://avatars3.githubusercontent.com/u/1326248?v=4", 14 | "profile": "http://geospatialscott.blogspot.com/", 15 | "contributions": [ 16 | "content" 17 | ] 18 | }, 19 | { 20 | "login": "AdrienRedon", 21 | "name": "Adrien REDON", 22 | "avatar_url": "https://avatars2.githubusercontent.com/u/5978436?v=4", 23 | "profile": "https://github.com/AdrienRedon", 24 | "contributions": [ 25 | "content" 26 | ] 27 | }, 28 | { 29 | "login": "NoriSte", 30 | "name": "Stefano Magni", 31 | "avatar_url": "https://avatars0.githubusercontent.com/u/173663?v=4", 32 | "profile": "https://twitter.com/NoriSte", 33 | "contributions": [ 34 | "content" 35 | ] 36 | }, 37 | { 38 | "login": "yjoer", 39 | "name": "Yeoh Joer", 40 | "avatar_url": "https://avatars2.githubusercontent.com/u/47742486?v=4", 41 | "profile": "https://www.joer.im", 42 | "contributions": [ 43 | "content" 44 | ] 45 | }, 46 | { 47 | "login": "jhonnymoreira", 48 | "name": "Jhonny Moreira", 49 | "avatar_url": "https://avatars0.githubusercontent.com/u/2177742?v=4", 50 | "profile": "http://jhonnymoreira.dev", 51 | "contributions": [ 52 | "content" 53 | ] 54 | }, 55 | { 56 | "login": "Germanika", 57 | "name": "Ian Germann", 58 | "avatar_url": "https://avatars2.githubusercontent.com/u/8846678?v=4", 59 | "profile": "https://github.com/Germanika", 60 | "contributions": [ 61 | "content" 62 | ] 63 | }, 64 | { 65 | "login": "AbdelrahmanHafez", 66 | "name": "Hafez", 67 | "avatar_url": "https://avatars3.githubusercontent.com/u/19984935?v=4", 68 | "profile": "https://github.com/AbdelrahmanHafez", 69 | "contributions": [ 70 | "content" 71 | ] 72 | }, 73 | { 74 | "login": "ruxandrafed", 75 | "name": "Ruxandra Fediuc", 76 | "avatar_url": "https://avatars1.githubusercontent.com/u/11021586?v=4", 77 | "profile": "http://www.ruxandrafediuc.com", 78 | "contributions": [ 79 | "content" 80 | ] 81 | }, 82 | { 83 | "login": "jacklee814", 84 | "name": "Jack", 85 | "avatar_url": "https://avatars0.githubusercontent.com/u/9951291?v=4", 86 | "profile": "https://github.com/jacklee814", 87 | "contributions": [ 88 | "content" 89 | ] 90 | }, 91 | { 92 | "login": "aloyr", 93 | "name": "Peter Carrero", 94 | "avatar_url": "https://avatars0.githubusercontent.com/u/231727?v=4", 95 | "profile": "https://www.petercarrero.com", 96 | "contributions": [ 97 | "content" 98 | ] 99 | }, 100 | { 101 | "login": "huhgawz", 102 | "name": "Huhgawz", 103 | "avatar_url": "https://avatars3.githubusercontent.com/u/369338?v=4", 104 | "profile": "https://github.com/huhgawz", 105 | "contributions": [ 106 | "content" 107 | ] 108 | }, 109 | { 110 | "login": "haakonmb", 111 | "name": "Haakon Borch", 112 | "avatar_url": "https://avatars1.githubusercontent.com/u/7099302?v=4", 113 | "profile": "https://github.com/haakonmb", 114 | "contributions": [ 115 | "content" 116 | ] 117 | }, 118 | { 119 | "login": "jaimemendozadev", 120 | "name": "Jaime Mendoza", 121 | "avatar_url": "https://avatars3.githubusercontent.com/u/5395811?v=4", 122 | "profile": "https://jaimemendoza.com/", 123 | "contributions": [ 124 | "content" 125 | ] 126 | }, 127 | { 128 | "login": "camerondunford", 129 | "name": "Cameron Dunford", 130 | "avatar_url": "https://avatars0.githubusercontent.com/u/840612?v=4", 131 | "profile": "https://github.com/camerondunford", 132 | "contributions": [ 133 | "content" 134 | ] 135 | }, 136 | { 137 | "login": "shadowspawn", 138 | "name": "John Gee", 139 | "avatar_url": "https://avatars1.githubusercontent.com/u/15719847?v=4", 140 | "profile": "https://github.com/shadowspawn", 141 | "contributions": [ 142 | "content" 143 | ] 144 | }, 145 | { 146 | "login": "aurelijusrozenas", 147 | "name": "Aurelijus Rožėnas", 148 | "avatar_url": "https://avatars0.githubusercontent.com/u/3273544?v=4", 149 | "profile": "https://github.com/aurelijusrozenas", 150 | "contributions": [ 151 | "content" 152 | ] 153 | }, 154 | { 155 | "login": "aaronshivers", 156 | "name": "Aaron", 157 | "avatar_url": "https://avatars2.githubusercontent.com/u/42848750?v=4", 158 | "profile": "http://aaronshivers.com", 159 | "contributions": [ 160 | "content" 161 | ] 162 | }, 163 | { 164 | "login": "tomanagle", 165 | "name": "Tom Nagle", 166 | "avatar_url": "https://avatars1.githubusercontent.com/u/8683577?v=4", 167 | "profile": "https://tomdoes.tech/", 168 | "contributions": [ 169 | "content" 170 | ] 171 | }, 172 | { 173 | "login": "yvesyao", 174 | "name": "Yves yao", 175 | "avatar_url": "https://avatars0.githubusercontent.com/u/7723729?v=4", 176 | "profile": "https://github.com/yvesyao", 177 | "contributions": [ 178 | "content" 179 | ] 180 | }, 181 | { 182 | "login": "Userbit", 183 | "name": "Userbit", 184 | "avatar_url": "https://avatars1.githubusercontent.com/u/34487074?v=4", 185 | "profile": "https://github.com/Userbit", 186 | "contributions": [ 187 | "content" 188 | ] 189 | }, 190 | { 191 | "login": "glaucia86", 192 | "name": "Glaucia Lemos", 193 | "avatar_url": "https://avatars0.githubusercontent.com/u/1631477?v=4", 194 | "profile": "https://glaucialemos.netlify.com/", 195 | "contributions": [ 196 | "maintenance" 197 | ] 198 | }, 199 | { 200 | "login": "koooge", 201 | "name": "koooge", 202 | "avatar_url": "https://avatars2.githubusercontent.com/u/7419215?v=4", 203 | "profile": "https://twitter.com/koooge", 204 | "contributions": [ 205 | "content" 206 | ] 207 | }, 208 | { 209 | "login": "mbiesiad", 210 | "name": "Michal", 211 | "avatar_url": "https://avatars0.githubusercontent.com/u/18367606?v=4", 212 | "profile": "https://twitter.com/michalbiesiada", 213 | "contributions": [ 214 | "content" 215 | ] 216 | }, 217 | { 218 | "login": "roywalker", 219 | "name": "roywalker", 220 | "avatar_url": "https://avatars0.githubusercontent.com/u/611846?v=4", 221 | "profile": "http://roywalker.me", 222 | "contributions": [ 223 | "content" 224 | ] 225 | }, 226 | { 227 | "login": "dangen-effy", 228 | "name": "dangen", 229 | "avatar_url": "https://avatars3.githubusercontent.com/u/23185799?v=4", 230 | "profile": "https://dangen-effy.github.io/", 231 | "contributions": [ 232 | "content" 233 | ] 234 | }, 235 | { 236 | "login": "biesiadamich", 237 | "name": "biesiadamich", 238 | "avatar_url": "https://avatars1.githubusercontent.com/u/60202305?v=4", 239 | "profile": "https://dev.to/mbiesiad", 240 | "contributions": [ 241 | "content" 242 | ] 243 | }, 244 | { 245 | "login": "cncolder", 246 | "name": "Yanlin Jiang", 247 | "avatar_url": "https://avatars3.githubusercontent.com/u/127009?v=4", 248 | "profile": "https://tarojsx.github.io", 249 | "contributions": [ 250 | "content" 251 | ] 252 | }, 253 | { 254 | "login": "sanguino", 255 | "name": "sanguino", 256 | "avatar_url": "https://avatars2.githubusercontent.com/u/2077168?v=4", 257 | "profile": "https://github.com/sanguino", 258 | "contributions": [ 259 | "content" 260 | ] 261 | }, 262 | { 263 | "login": "MorganGeek", 264 | "name": "Morgan", 265 | "avatar_url": "https://avatars0.githubusercontent.com/u/3721240?v=4", 266 | "profile": "https://github.com/MorganGeek", 267 | "contributions": [ 268 | "content" 269 | ] 270 | }, 271 | { 272 | "login": "lukasbischof", 273 | "name": "Lukas Bischof", 274 | "avatar_url": "https://avatars0.githubusercontent.com/u/8350985?v=4", 275 | "profile": "https://luk4s.dev", 276 | "contributions": [ 277 | "test", 278 | "content" 279 | ] 280 | }, 281 | { 282 | "login": "JuanMaRuiz", 283 | "name": "JuanMa Ruiz", 284 | "avatar_url": "https://avatars2.githubusercontent.com/u/1837650?v=4", 285 | "profile": "https://juanmaruiz.surge.sh", 286 | "contributions": [ 287 | "content" 288 | ] 289 | }, 290 | { 291 | "login": "luisangelorjr", 292 | "name": "Luís Ângelo Rodrigues Jr.", 293 | "avatar_url": "https://avatars3.githubusercontent.com/u/22268900?v=4", 294 | "profile": "https://luisangelorjr.com.br", 295 | "contributions": [ 296 | "content" 297 | ] 298 | }, 299 | { 300 | "login": "jfernandezpe", 301 | "name": "José Fernández", 302 | "avatar_url": "https://avatars0.githubusercontent.com/u/12046620?v=4", 303 | "profile": "https://jfernandezpe.wordpress.com/", 304 | "contributions": [ 305 | "content" 306 | ] 307 | }, 308 | { 309 | "login": "AlejandroGutierrezB", 310 | "name": "Alejandro Gutierrez Barcenilla", 311 | "avatar_url": "https://avatars3.githubusercontent.com/u/56408597?v=4", 312 | "profile": "http://www.linkedin.com/in/AlejandroGutierrezB", 313 | "contributions": [ 314 | "content" 315 | ] 316 | }, 317 | { 318 | "login": "jasonandmonte", 319 | "name": "Jason", 320 | "avatar_url": "https://avatars1.githubusercontent.com/u/30088000?v=4", 321 | "profile": "https://github.com/jasonandmonte", 322 | "contributions": [ 323 | "content" 324 | ] 325 | }, 326 | { 327 | "login": "otavionetoca", 328 | "name": "Otavio Araujo", 329 | "avatar_url": "https://avatars.githubusercontent.com/u/11263232?v=4", 330 | "profile": "https://github.com/otavionetoca", 331 | "contributions": [ 332 | "test", 333 | "content" 334 | ] 335 | }, 336 | { 337 | "login": "contributorpw", 338 | "name": "Alex Ivanov", 339 | "avatar_url": "https://avatars.githubusercontent.com/u/5027939?v=4", 340 | "profile": "https://contributor.pw", 341 | "contributions": [ 342 | "content" 343 | ] 344 | }, 345 | { 346 | "login": "YeeJone", 347 | "name": "Yiqiao Xu", 348 | "avatar_url": "https://avatars.githubusercontent.com/u/20400822?v=4", 349 | "profile": "https://github.com/YeeJone", 350 | "contributions": [ 351 | "content" 352 | ] 353 | }, 354 | { 355 | "login": "yubinTW", 356 | "name": "YuBin, Hsu", 357 | "avatar_url": "https://avatars.githubusercontent.com/u/31545456?v=4", 358 | "profile": "https://github.com/yubinTW", 359 | "contributions": [ 360 | "translation", 361 | "code" 362 | ] 363 | }, 364 | { 365 | "login": "TREER00T", 366 | "name": "Ali Azmoodeh", 367 | "avatar_url": "https://avatars.githubusercontent.com/u/76606342?v=4", 368 | "profile": "https://github.com/TREER00T", 369 | "contributions": [ 370 | "content" 371 | ] 372 | }, 373 | { 374 | "login": "Saimon398", 375 | "name": "Alex Popov", 376 | "avatar_url": "https://avatars.githubusercontent.com/u/71539667?v=4", 377 | "profile": "https://github.com/Saimon398", 378 | "contributions": [ 379 | "content" 380 | ] 381 | }, 382 | { 383 | "login": "Shramkoweb", 384 | "name": "Serhii Shramko", 385 | "avatar_url": "https://avatars.githubusercontent.com/u/42001531?v=4", 386 | "profile": "http://shramko.dev", 387 | "contributions": [ 388 | "content" 389 | ] 390 | }, 391 | { 392 | "login": "yugoccp", 393 | "name": "Yugo Sakamoto", 394 | "avatar_url": "https://avatars.githubusercontent.com/u/1724114?v=4", 395 | "profile": "https://github.com/yugoccp", 396 | "contributions": [ 397 | "content" 398 | ] 399 | }, 400 | { 401 | "login": "Fdawgs", 402 | "name": "Frazer Smith", 403 | "avatar_url": "https://avatars.githubusercontent.com/u/43814140?v=4", 404 | "profile": "https://yeovilhospital.co.uk/", 405 | "contributions": [ 406 | "content" 407 | ] 408 | }, 409 | { 410 | "login": "wralith", 411 | "name": "Wralith", 412 | "avatar_url": "https://avatars.githubusercontent.com/u/75392169?v=4", 413 | "profile": "https://github.com/wralith", 414 | "contributions": [ 415 | "content" 416 | ] 417 | }, 418 | { 419 | "login": "saseungmin", 420 | "name": "Harang", 421 | "avatar_url": "https://avatars.githubusercontent.com/u/60910665?v=4", 422 | "profile": "https://haranglog.tistory.com", 423 | "contributions": [ 424 | "content" 425 | ] 426 | }, 427 | { 428 | "login": "rcanelav", 429 | "name": "rcanelav", 430 | "avatar_url": "https://avatars.githubusercontent.com/u/64812826?v=4", 431 | "profile": "https://github.com/rcanelav", 432 | "contributions": [ 433 | "content" 434 | ] 435 | }, 436 | { 437 | "login": "drewrwilson", 438 | "name": "Drew Wilson", 439 | "avatar_url": "https://avatars.githubusercontent.com/u/4324656?v=4", 440 | "profile": "https://github.com/drewrwilson", 441 | "contributions": [ 442 | "content" 443 | ] 444 | }, 445 | { 446 | "login": "XtLee", 447 | "name": "XtLee", 448 | "avatar_url": "https://avatars.githubusercontent.com/u/30145777?v=4", 449 | "profile": "https://github.com/XtLee", 450 | "contributions": [ 451 | "content" 452 | ] 453 | }, 454 | { 455 | "login": "smonn", 456 | "name": "Simon Ingeson", 457 | "avatar_url": "https://avatars.githubusercontent.com/u/44818?v=4", 458 | "profile": "https://www.smonn.se", 459 | "contributions": [ 460 | "content" 461 | ] 462 | }, 463 | { 464 | "login": "elfacu0", 465 | "name": "elfacu0", 466 | "avatar_url": "https://avatars.githubusercontent.com/u/30785449?v=4", 467 | "profile": "https://github.com/elfacu0", 468 | "contributions": [ 469 | "content" 470 | ] 471 | }, 472 | { 473 | "login": "jorbelca", 474 | "name": "jorbelca", 475 | "avatar_url": "https://avatars.githubusercontent.com/u/76847923?v=4", 476 | "profile": "https://github.com/jorbelca", 477 | "contributions": [ 478 | "content" 479 | ] 480 | } 481 | ], 482 | "projectName": "javascript-testing-best-practices", 483 | "projectOwner": "goldbergyoni", 484 | "repoType": "github", 485 | "repoHost": "https://github.com", 486 | "commitConvention": "angular" 487 | } 488 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /.operations/questions-answers.md: -------------------------------------------------------------------------------- 1 | # Common questions and answers 2 | 3 | ## Q: How do I start a new translation? 4 | 5 | **Answer:** 6 | 7 | Welcome aboard! Having a Brazilian Portuguese translation would be awesome 🔥🌈👌💚 . I'll be glad to collaborate with you on this and help wherever I can 8 | 9 | Before you start with this, I've prepared some basic workflow guidelines: 10 | 11 | **Where to do the translation?** - Fork and work your own copy, create a readme-{language}.md file (e.g. readme-fr.md) and do the translation work over there 12 | 13 | **How to push changes?** - I will create a dedicated branch for you translations-{language}-staging (e.g. translations-fr-staging), whenever you want to save some changes or share with the team - just PR to this branch 14 | 15 | **How & when to publish to master?** - The content can be published once it's 70% translated and 100% language proofed. Kindly run it through spell checker. Whenever you feel that the content stands to these guidelines, just a raise a flag and I'll merge the language branch into the master 16 | 17 | **Will I get credit for the translation work?** - Obviously! Your name will appear nearby the language flag in the main readme.md, added to the repo team, appear boldly at the top of the translation page - 'Translated, adapted and reviewed by {Your name}'. We will also publish a medium article with the translation with your name at the top 18 | 19 | Looking forward and excited to work on this! 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yoni Goldberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/amazon-visual-regression.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/amazon-visual-regression.jpeg -------------------------------------------------------------------------------- /assets/bp-1-3-parts.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-1-3-parts.jpeg -------------------------------------------------------------------------------- /assets/bp-12-Yoni-Goldberg-Testing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-12-Yoni-Goldberg-Testing.jpeg -------------------------------------------------------------------------------- /assets/bp-12-rich-testing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-12-rich-testing.jpeg -------------------------------------------------------------------------------- /assets/bp-13-component-test-yoni-goldberg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-13-component-test-yoni-goldberg.png -------------------------------------------------------------------------------- /assets/bp-14-testing-best-practices-contract-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-14-testing-best-practices-contract-flow.png -------------------------------------------------------------------------------- /assets/bp-16-yoni-goldberg-quality.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-16-yoni-goldberg-quality.png -------------------------------------------------------------------------------- /assets/bp-17-yoni-goldberg-chaos-monkey-nodejs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-17-yoni-goldberg-chaos-monkey-nodejs.png -------------------------------------------------------------------------------- /assets/bp-18-code-coverage2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-18-code-coverage2.jpeg -------------------------------------------------------------------------------- /assets/bp-18-yoni-goldberg-code-coverage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-18-yoni-goldberg-code-coverage.png -------------------------------------------------------------------------------- /assets/bp-19-coverage-yoni-goldberg-nodejs-consultant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-19-coverage-yoni-goldberg-nodejs-consultant.png -------------------------------------------------------------------------------- /assets/bp-20-yoni-goldberg-mutation-testing.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-20-yoni-goldberg-mutation-testing.jpeg -------------------------------------------------------------------------------- /assets/bp-21-yoni-goldberg-eslint.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-21-yoni-goldberg-eslint.jpeg -------------------------------------------------------------------------------- /assets/bp-24-yonigoldberg-jest-parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-24-yonigoldberg-jest-parallel.png -------------------------------------------------------------------------------- /assets/bp-25-nodejs-licsense.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-25-nodejs-licsense.png -------------------------------------------------------------------------------- /assets/bp-26-npm-audit-snyk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-26-npm-audit-snyk.png -------------------------------------------------------------------------------- /assets/bp-27-yoni-goldberg-npm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/bp-27-yoni-goldberg-npm.png -------------------------------------------------------------------------------- /assets/flat-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/flat-report.png -------------------------------------------------------------------------------- /assets/header-alt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/header-alt.png -------------------------------------------------------------------------------- /assets/header-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/header-image.png -------------------------------------------------------------------------------- /assets/header.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/header.pptx -------------------------------------------------------------------------------- /assets/headspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/headspace.png -------------------------------------------------------------------------------- /assets/hierarchical-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/hierarchical-report.png -------------------------------------------------------------------------------- /assets/jest-stars.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/jest-stars.jpg -------------------------------------------------------------------------------- /assets/js-bp-header.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/js-bp-header.jpg -------------------------------------------------------------------------------- /assets/js-test-bp-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/js-test-bp-header.png -------------------------------------------------------------------------------- /assets/jtbp-header-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/jtbp-header-blue.png -------------------------------------------------------------------------------- /assets/jtbp-header-with-checklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/jtbp-header-with-checklist.png -------------------------------------------------------------------------------- /assets/jtbp-header-with-pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/jtbp-header-with-pipeline.png -------------------------------------------------------------------------------- /assets/lighthouse2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/lighthouse2.png -------------------------------------------------------------------------------- /assets/logo-chalkboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/logo-chalkboard.png -------------------------------------------------------------------------------- /assets/mind-map.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/mind-map.jpg -------------------------------------------------------------------------------- /assets/nbptest.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/nbptest.jpeg -------------------------------------------------------------------------------- /assets/nbtest.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/nbtest.jpg -------------------------------------------------------------------------------- /assets/readme.md: -------------------------------------------------------------------------------- 1 | 123 2 | -------------------------------------------------------------------------------- /assets/repo-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/repo-header.png -------------------------------------------------------------------------------- /assets/roadmap-brief.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/roadmap-brief.png -------------------------------------------------------------------------------- /assets/story-book.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/story-book.jpg -------------------------------------------------------------------------------- /assets/t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/t.png -------------------------------------------------------------------------------- /assets/tbp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/tbp.png -------------------------------------------------------------------------------- /assets/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/twitter.png -------------------------------------------------------------------------------- /assets/uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/uml.png -------------------------------------------------------------------------------- /assets/yoni-goldberg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/yoni-goldberg.jpg -------------------------------------------------------------------------------- /assets/zh-CN/bp-1-3-parts.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/zh-CN/bp-1-3-parts.jpg -------------------------------------------------------------------------------- /assets/zh-CN/bp-12-rich-testing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/zh-CN/bp-12-rich-testing.jpg -------------------------------------------------------------------------------- /assets/zh-CN/bp-13-component-test-yoni-goldberg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/zh-CN/bp-13-component-test-yoni-goldberg.png -------------------------------------------------------------------------------- /assets/zh-CN/bp-14-testing-best-practices-contract-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/zh-CN/bp-14-testing-best-practices-contract-flow.png -------------------------------------------------------------------------------- /assets/zh-CN/bp-20-yoni-goldberg-mutation-testing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/zh-CN/bp-20-yoni-goldberg-mutation-testing.jpg -------------------------------------------------------------------------------- /assets/zh-CN/headspace.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goldbergyoni/javascript-testing-best-practices/63a2bb07bb718d0a34a9c1249ef0df9b1266dad8/assets/zh-CN/headspace.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // This is a book for now, but code examples are coming soon 2 | // See https://github.com/testjavascript/nodejs-integration-tests-best-practices to see an example app 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "javascript-testing-best-practices", 3 | "version": "2.0.0", 4 | "description": "📗🌐 🚢 Comprehensive and exhaustive JavaScript & Node.js testing best practices (April 2020) https://testjavascript.com/", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "t" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/i0natan/javascript-testing-best-practices.git" 12 | }, 13 | "keywords": [ 14 | "test", 15 | "tdd", 16 | "javascript", 17 | "mocha", 18 | "jest", 19 | "aaa", 20 | "coverage", 21 | "confidence", 22 | "unit", 23 | "test", 24 | "integration", 25 | "e2e" 26 | ], 27 | "author": "Yoni Goldberg", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/i0natan/javascript-testing-best-practices/issues" 31 | }, 32 | "homepage": "https://github.com/i0natan/javascript-testing-best-practices#readme" 33 | } 34 | -------------------------------------------------------------------------------- /readme-zh-CN.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | # 👇 为什么本指南可以助你将测试能力提升到下一层级 6 | 7 |
8 | 9 | ## 📗 45+ 最佳实践:非常全面彻底 10 | 这篇文章从 A 到 Z 给出了 JavaScript & Node.js 的稳定性指南。它为你整理总结了市面上大量的最佳博客文章、书籍以及工具。 11 | 12 | 13 | ## 🚢 进阶:在基础上前进 10000 公里 14 | 从基础领域跨上前往进阶话题的旅程,包括:在生产环境测试、编译测试、基于属性的测试以及很多策略 & 专业工具。如果你仔细阅读了本指南中的每个字,则你的测试能力将有可能大大超过平均水平。 15 | 16 | 17 | ## 🌐 全栈:前端、后端、CI、任何岗位 18 | 先了解通用的测试实践为其他应用层的打下基础。然后,在你自己的领域深入探索:前端/UI、后端、CI 甚至是他们所有的层面。 19 | 20 |
21 | 22 | ### 作者 Yoni Goldberg 23 | - 一位 JavaScript & Node.js 顾问 24 | - 👨‍🏫 [我的测试网站](https://www.testjavascript.com/) - 在欧洲 & 美国了解 [我的测试网站](https://www.testjavascript.com/) 25 | - [在 Twitter 关注我](https://twitter.com/goldbergyoni/) 26 | - 来 [LA](https://js.la/), [Verona](https://2019.nodejsday.it/), [Kharkiv](https://kharkivjs.org/), [free webinar](https://zoom.us/webinar/register/1015657064375/WN_Lzvnuv4oQJOYey2jXNqX6A)听我的演讲。后续工作待定。 27 | - [我的 JavaScript 质量新闻](https://testjavascript.com/newsletter/) - 战略层面的视野和信息 28 | 29 | 30 |

31 | 32 | ## `内容列表` 33 | 34 | #### [`第 0 章:黄金法则`](#第-0-章黄金法则-1) 35 | 36 | 一条启发所有人的建议(特殊的 1 条) 37 | 38 | #### [`第一章:测试剖析`](#第一章-测试剖析) 39 | 40 | 基础 - 搭建干净的测试(12 条) 41 | 42 | #### [`第二章:后端测试`](#第二章-后端测试) 43 | 44 | 高效地编写后端和微服务的测试(8 条) 45 | 46 | #### [`第三章:前端测试`](#第三章-前端测试) 47 | 48 | 为 UI(包括组件和 E2E 测试)编写测试(11 条) 49 | 50 | #### [`第四章:度量测试效果`](#第四章-度量测试效果) 51 | 52 | 度量测试质量(4 条) 53 | 54 | #### [`第五章:持续集成(CI)`](#第五章持续集成ci-1) 55 | 56 | JS 领域的 CI 指南(9 条) 57 | 58 |

59 | 60 | 61 | # 第 0 章:黄金法则 62 | 63 |
64 | 65 | ## ⚪️ 0 黄金法则:设计瘦测试 66 | 67 | :white_check_mark: **建议:** 测试代码与生产代码不同,要使它变得极其简单、短小、没有抽象、扁平化、使人愉悦、瘦。一段测试代码需要做到让人一眼就能看出其目的。 68 | 69 | 我们的思维空间被主体生产代码充满,因此无法腾出额外的“大脑空间”存放复杂的东西。如果向可怜的大脑中塞进其他复杂代码,将会使得整个部分变慢,而这个部分正是用来解决我们需要测试的问题的。这也是大部分团队放弃测试的原因。 70 | 71 | 另一方面,测试是一个友好的助手,一个你乐于与之合作、投资小回汇报大的助手。科学证明我们有两套大脑系统:系统 1 用于无需努力的活动如在一个空旷的路上开车;系统 2 用于复杂和繁琐的工作如算一道数学表达式。将你的测试为系统 1 设计,当你看一段测试代码时,需要像改 HTML 文档一样简单而不是像计算 2 × (17 × 24)。 72 | 73 | 为了达到这个目的,我们可以通过选择性价比高、投入产出比(ROI)高的技术、工具以及测试对象。仅测试需要的内容,努力保持其灵活性,某些时候甚至值得去舍弃一些测试来换取灵活性和简洁性。 74 | 75 | ![alt text](/assets/zh-CN/headspace.png "We have no head room for additional complexity") 76 | 77 | 下面的大部分建议衍生自这一原则。 78 | 79 | ### 准备好开始了吗? 80 | 81 | 82 |

83 | 84 | # 第一章: 测试剖析 85 | 86 |
87 | 88 | ## ⚪ ️ 1.1 每个测试用例的名称必须包含三个部分 89 | 90 | :white_check_mark: **建议:** 一个测试报告需要让不熟悉代码的人(测试、运维)明确知道新的变更是符合需求。因此测试名称需要从**需求层面**描述,并且包含三个部分: 91 | 92 | (1) 被测的是什么?(比如 ProductsService.addNewProduct 方法) 93 | 94 | (2) 在什么条件和场景下?(比如没有 向该方法传入 price 参数) 95 | 96 | (3) 期望的结果是什么?(比如不允许添加该产品) 97 | 98 |
99 | 100 | 101 | ❌ **否则:** 当一个名为“新增产品”的测试用例挂掉之后,你如何准确找到是哪里出问题了? 102 | 103 |
104 | 105 | **👇 Note:** 每一条后面会有一个 代码示例,有时候还会放一张图片说明。 106 | 107 |
代码示例 108 | 109 |
110 | 111 | ### :clap: 正例: 一个包含三部分的用例名 112 | 113 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Mocha-blue.svg 114 | "Using Mocha to illustrate the idea") 115 | 116 | ```javascript 117 | //1. unit under test 118 | describe('Products Service', function() { 119 | describe('Add new product', function() { 120 | //2. scenario and 3. expectation 121 | it('When no price is specified, then the product status is pending approval', ()=> { 122 | const newProduct = new ProductService().add(...); 123 | expect(newProduct.status).to.equal('pendingApproval'); 124 | }); 125 | }); 126 | }); 127 | 128 | ``` 129 |
130 | 131 | ### :clap: 正例: 一个包含三部分的用例名 132 | ![alt text](/assets/zh-CN/bp-1-3-parts.jpg "A test name that constitutes 3 parts") 133 | 134 |
135 | 136 |

137 | 138 | ## ⚪ ️ 1.2 使用 AAA 模式构造测试内容 139 | 140 | :white_check_mark: **建议:** 将你的测试内容划分为三个部分:布置,执行,断言 —— Arrange, Act & Assert (AAA)。这样读者就无需动用脑细胞理解你的测试内容了: 141 | 142 | 1st A - 准备(Arrange):一些用于提供上下文的代码。可能包含:构造数据、添加 DB 记录、mocking/stubbing 对象,以及其他的准备代码; 143 | 144 | 2nd A - 执行(Act):执行测试单元。通常一行代码。 145 | 146 | 3rd A - 断言(Assert):保证得到的值符合预期。通常一行代码。 147 | 148 | 149 |
150 | 151 | 152 | ❌ **否则:** 你不仅要花大量时间理解这段代码,而且本该是最简单的部分却耗费了你的大量脑细胞。 153 | 154 |
155 | 156 |
代码示例 157 | 158 |
159 | 160 | ### :clap: 正例: 一个使用 AAA 模式构造的测试用例 161 | 162 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg 163 | "Examples with Jest") ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg 164 | "Examples with Jest") 165 | 166 | ```javascript 167 | describe('Customer classifier', () => { 168 | test('When customer spent more than 500$, should be classified as premium', () => { 169 | //Arrange 170 | const customerToClassify = {spent:505, joined: new Date(), id:1} 171 | const DBStub = sinon.stub(dataAccess, "getCustomer") 172 | .reply({id:1, classification: 'regular'}); 173 | 174 | //Act 175 | const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); 176 | 177 | //Assert 178 | expect(receivedClassification).toMatch('premium'); 179 | }); 180 | }); 181 | ``` 182 | 183 |
184 | 185 | ### :thumbsdown: 反例: 没有分隔、一大坨、难以解释 186 | 187 | ```javascript 188 | test('Should be classified as premium', () => { 189 | const customerToClassify = {spent:505, joined: new Date(), id:1} 190 | const DBStub = sinon.stub(dataAccess, "getCustomer") 191 | .reply({id:1, classification: 'regular'}); 192 | const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); 193 | expect(receivedClassification).toMatch('premium'); 194 | }); 195 | ``` 196 | 197 | 198 |
199 | 200 | 201 | 202 |

203 | 204 | 205 | 206 | 207 | ## ⚪ ️1.3 用产品语言描述期望:使用 BDD 形式的断言 208 | 209 | :white_check_mark: **建议:** 使用声明的方式写代码,可以使读者无脑 get 到重点。而如果你的代码使用各种条件逻辑包裹起来,则会增加读者的理解难度。因此,我们应尽量使用类似人类语言的形式描述如 `expect` 或 `should` 而不是自己写代码。如果 Chai 和 Jest 不包含你想要的断言,而且这种断言可被高度复用时,你可以考虑 [扩展 Jest 匹配器 (Jest)](https://jestjs.io/docs/en/expect#expectextendmatchers) 或者写一个 [自定义 Chai 插件](https://www.chaijs.com/guide/plugins/) 210 |
211 | 212 | 213 | ❌ **否则:** 团队的测试代码会越写越少,而且会用 .skip() 把一些讨厌的测试用例注释掉。 214 | 215 |
216 | 217 |
代码示例
218 | 219 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg 220 | "Examples with Mocha & Chai") ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg 221 | "Examples with Jest") 222 | 223 | ### :thumbsdown: 反例: 为了了解该用例的目的,读者必须快速浏览冗长复杂的代码 224 | 225 | ```javascript 226 | test("When asking for an admin, ensure only ordered admins in results" , () => { 227 | //assuming we've added here two admins "admin1", "admin2" and "user1" 228 | const allAdmins = getUsers({adminOnly:true}); 229 | 230 | const admin1Found, adming2Found = false; 231 | 232 | allAdmins.forEach(aSingleUser => { 233 | if(aSingleUser === "user1"){ 234 | assert.notEqual(aSingleUser, "user1", "A user was found and not admin"); 235 | } 236 | if(aSingleUser==="admin1"){ 237 | admin1Found = true; 238 | } 239 | if(aSingleUser==="admin2"){ 240 | admin2Found = true; 241 | } 242 | }); 243 | 244 | if(!admin1Found || !admin2Found ){ 245 | throw new Error("Not all admins were returned"); 246 | } 247 | }); 248 | 249 | ``` 250 |
251 | 252 | ### :clap: 正例: 快速浏览下面的声明式用例很轻松 253 | 254 | 255 | ```javascript 256 | it("When asking for an admin, ensure only ordered admins in results" , () => { 257 | //assuming we've added here two admins 258 | const allAdmins = getUsers({adminOnly:true}); 259 | 260 | expect(allAdmins).to.include.ordered.members(["admin1" , "admin2"]) 261 | .but.not.include.ordered.members(["user1"]); 262 | }); 263 | 264 | ``` 265 | 266 |
267 | 268 | 269 |

270 | 271 | 272 | ## ⚪ ️ 1.4 坚持黑盒测试:只测 public 方法 273 | 274 | :white_check_mark: **建议:** 测试内部逻辑是无意义且浪费时间的。如果你的 代码/API 返回了正确的结果,你真的需要花三个小时时间去测试它内部究竟如何实现的,并且在之后维护这一堆脆弱的测试吗?每当测试一个公共方法时,其私有实现也会被隐式地测试,只有当存在某个问题(例如错误的输出)时测试才会中断。这种方法也称为`行为测试`。另一方面,如果你测试内部方法(白盒方法)—你的关注点将从组件的输出结果转移到具体的细节上,如果某天内部逻辑改变了,即使结果依然正确,你也要花精力去维护之前的测试逻辑,这无形中增加了维护成本。 275 |
276 | 277 | 278 | ❌ **否则:** 你的代码将会像[狼来了](https://en.wikipedia.org/wiki/The_Boy_Who_Cried_Wolf)一样:总是叫唤着“出问题啦”(比如一个因私有变量名改变导致的用例失败)。则人们必然会开始忽略 CI 的通知,直到某天真正的 bug 被忽略…… 279 | 280 |
281 |
代码示例 282 | 283 |
284 | 285 | ### :thumbsdown: 反例: 一个无脑测试内部方法的测试用例 286 | 287 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg 288 | "Examples with Mocha & Chai") 289 | 290 | ```javascript 291 | class ProductService{ 292 | //this method is only used internally 293 | //Change this name will make the tests fail 294 | calculateVAT(priceWithoutVAT){ 295 | return {finalPrice: priceWithoutVAT * 1.2}; 296 | //Change the result format or key name above will make the tests fail 297 | } 298 | //public method 299 | getPrice(productId){ 300 | const desiredProduct= DB.getProduct(productId); 301 | finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice; 302 | } 303 | } 304 | 305 | 306 | it("White-box test: When the internal methods get 0 vat, it return 0 response", async () => { 307 | //There's no requirement to allow users to calculate the VAT, only show the final price. Nevertheless we falsely insist here to test the class internals 308 | expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0); 309 | }); 310 | 311 | ``` 312 | 313 |
314 | 315 | 316 | 317 | 318 |

319 | 320 | ## ⚪ ️ ️1.5 使用正确的测试替身(Test Double):避免总用 stub 和 spy 321 | 322 | :white_check_mark: **建议:** 测试替身是把双刃剑,他们在提供巨大价值的同时,耦合了应用的内部逻辑 ([这里有一篇关于测试替身的文章: mocks vs stubs vs spies](https://martinfowler.com/articles/mocksArentStubs.html)).
在使用测试替身前,问自己一个很简单的问题:我是用它来测试需求文档中定义的可见的功能或者可能可见的功能吗?如果不是,那就可能是白盒测试了。
举例来说,如果你想测试你的应用程序在支付服务宕机时的合理表现,你可以 stub 支付服务并触发一些“无响应”返回,以确保被测试的单元返回正确的值。这可以测试特定场景下的应用程序的行为、响应、输出结果。你也可以使用一个 spy 来断言当服务宕机时发送了一封电子邮件——这又是一个针对可能出现在需求文档中的行为的检查(“如果无法保存付款,请发送电子邮件”)。反过来,如果你 mock 的支付服务,并确保它被正确调用并传入正确的 JavaScript 类型,那么你的测试重点是内部的逻辑,它与应用的功能关系不大,而且可能会经常变化。 323 | 324 |
325 | 326 | 327 | ❌ **否则:** 任何代码重构都要求搜索代码中的所有 mock 并相应地进行更新。测试变成了一种负担,而不是一个帮手。 328 | 329 |
330 | 331 |
代码示例 332 | 333 |
334 | 335 | ### :thumbsdown: 反例: 关注内部实现的 mock 336 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Sinon-blue.svg 337 | "Examples with Mocha & Chai") 338 | ```javascript 339 | it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => { 340 | //Assume we already added a product 341 | const dataAccessMock = sinon.mock(DAL); 342 | //hmmm BAD: testing the internals is actually our main goal here, not just a side-effect 343 | dataAccessMock.expects("deleteProduct").once().withArgs(DBConfig, theProductWeJustAdded, true, false); 344 | new ProductService().deletePrice(theProductWeJustAdded); 345 | dataAccessMock.verify(); 346 | }); 347 | ``` 348 |
349 | 350 | ### :clap:正例: 使用 spy 关注于测试需求本身,而作为副作用不得不接触内部 351 | 352 | ```javascript 353 | it("When a valid product is about to be deleted, ensure an email is sent", async () => { 354 | //Assume we already added here a product 355 | const spy = sinon.spy(Emailer.prototype, "sendEmail"); 356 | new ProductService().deletePrice(theProductWeJustAdded); 357 | //hmmm OK: we deal with internals? Yes, but as a side effect of testing the requirements (sending an email) 358 | }); 359 | ``` 360 | 361 |
362 | 363 | 364 | 365 |

366 | 367 | ## ⚪ ️1.6 不要“foo”,使用真实数据 368 | 369 | :white_check_mark: **建议:** 生产环境中的 bug 通常是在一些特殊或者意外的输入下出现的——所以测试的输入数据越真实,越容易在早期抓住问题。使用现有的一些库(比如 [Faker](https://www.npmjs.com/package/faker))去造“假”真数据来模拟生产环境数据的多样性和形式。比如,这些库可以生成真实的电话号码、用户名、信用卡、公司名等等。你还可以创建一些测试(在单元测试之上,而不是替代)生产随机 fakers 数据来扩展你的测试单元,甚至从生产环境中导入真实的数据。想要进阶的话,请看下一条:基于属性的测试。 370 |
371 | 372 | 373 | ❌ **否则:** 你所有的用例都在 “foo” 之类的输入值下表现正确,结果上线后收到诸如  “[@3e2ddsf ]() . ##’ 1 fdsfds . fds432 AAAA” 之类的输入后挂掉了。 374 | 375 | 376 |
377 | 378 |
代码示例 379 | 380 |
381 | 382 | ### :thumbsdown: 反例: 一个用例因使用非真实数据而通过 383 | 384 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg 385 | "Examples with Jest") 386 | 387 | 388 | ```javascript 389 | const addProduct = (name, price) =>{ 390 | const productNameRegexNoSpace = /^\S*$/;//no white-space allowd 391 | 392 | if(!productNameRegexNoSpace.test(name)) 393 | return false;//this path never reached due to dull input 394 | 395 | //some logic here 396 | return true; 397 | }; 398 | 399 | test("Wrong: When adding new product with valid properties, get successful confirmation", async () => { 400 | //The string "Foo" which is used in all tests never triggers a false result 401 | const addProductResult = addProduct("Foo", 5); 402 | expect(addProductResult).toBe(true); 403 | //Positive-false: the operation succeeded because we never tried with long 404 | //product name including spaces 405 | }); 406 | 407 | ``` 408 |
409 | 410 | ### :clap:正例: 随机生成真实的输入数据 411 | 412 | ```javascript 413 | it("Better: When adding new valid product, get successful confirmation", async () => { 414 | const addProductResult = addProduct(faker.commerce.productName(), faker.random.number()); 415 | //Generated random input: {'Sleek Cotton Computer', 85481} 416 | expect(addProductResult).to.be.true; 417 | //Test failed, the random input triggered some path we never planned for. 418 | //We discovered a bug early! 419 | }); 420 | ``` 421 | 422 |
423 | 424 | 425 | 426 | 427 |

428 | 429 | ## ⚪ ️ 1.7 基于属性的测试:测试输入的多种组合 430 | 431 | :white_check_mark: **建议:** 通常我们只会选择部分的数据样例去测试,即使是使用了上一节讲到的工具去模拟真实数据,我们也只覆盖到了一部分输入的组合(`method(‘’, true, 1), method(“string” , false” , 0)`)。然而在生产环境中,一个拥有 5 个参数的 API,可能会遇到上千种排列组合,而其中的某一种可能会把你的进程搞挂([见 Fuzz Testing](https://en.wikipedia.org/wiki/Fuzzing))。如何自动生成这上千种组合并在它们出问题后 catch 到?基于属性的测试适用于这种需求:向你的测试单元传入所有可能的输入组合,以增加发现 bug 的可能。例如,给定一个方法 —— `addNewProduct(id, name, isDiscount)`,支持属性测试的库将使用一批`(number, string, boolean)`组合调用此方法,比如`(1,“iPhone”,false)`,`(2,“Galaxy”,true)`。您可以使用您最喜欢的测试运行器(Mocha、Jest等),
通常,我们为每个测试选择一些输入样本。即使输入格式类似于现实世界的数据(见子弹“别foo”),我们只涉及几个输入组合(方法(“,真的,1),方法(“字符串”,假”,0)),然而,在生产中,一个API调用与成千上万的5个参数可以调用不同的排列,其中一个可能使我们的流程(见模糊测试)。如果您可以编写一个测试,自动发送1000个不同输入的排列组合,并捕获我们的代码未能返回正确响应的输入,那该怎么办?基于属性的测试就是这样一种技术:通过发送所有可能的输入组合到你的测试单元中,它增加了发现bug的偶然性。例如,给定一个方法—addNewProduct(id, name, isDiscount)—支持库将使用许多(number, string, boolean)组合调用此方法,比如(1,“iPhone”,false),(2,“Galaxy”,true)。您可以使用您最喜欢的测试运行器(Mocha、Jest等):比如 [js-verify](https://github.com/jsverify/jsverify) 或者 [testcheck](https://github.com/leebyron/testcheck-js) (文档比较好)。 更新: Nicolas Dubien 在下面的回复中建议 [了解下 fast-check](https://github.com/dubzzz/fast-check#readme) 它提供了更多的能力,似乎更易维护。 432 |
433 | 434 | 435 | ❌ **否则:** 你无意中选择的输入数据只覆盖了没问题的代码路径。不幸的是,它没有真正发现了 bug。 436 | 437 | 438 |
439 | 440 |
代码示例 441 | 442 |
443 | 444 | ### :clap: 正例: 使用“fast-check”测试输入的组合 445 | 446 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg 447 | "Examples with Jest") 448 | 449 | ```javascript 450 | import fc from "fast-check"; 451 | 452 | describe("Product service", () => { 453 | describe("Adding new", () => { 454 | //this will run 100 times with different random properties 455 | it("Add new product with random yet valid properties, always successful", () => 456 | fc.assert( 457 | fc.property(fc.integer(), fc.string(), (id, name) => { 458 | expect(addNewProduct(id, name).status).toEqual("approved"); 459 | }) 460 | )); 461 | }); 462 | }); 463 | ``` 464 | 465 |
466 | 467 | 468 | 469 | 470 |

471 | 472 | ## ⚪ ️ 1.8 snapshot:如果需要,仅使用短的行内快照 473 | 474 | :white_check_mark: **建议:** 如果你需要 [快照测试](https://jestjs.io/docs/en/snapshot-testing),仅使用端快照(比如 3-7 行),并且把它们作为测试的一部分([内联快照](https://jestjs.io/docs/en/snapshot-testing#inline-snapshots))而不是存放到外部文件中。遵循这条指导原则将确保您的测试保持自解释并且不那么脆弱。 475 | 476 | 另一方面,“经典”快照教程和工具鼓励我们在一些外部介质上存储大文件(如组件的渲染结果,API 的 JSON 结果),并确保每次运行测试时将新结果与保存的版本进行比较。打个比方,这么做有可能隐式地将我们的测试与包含 3000 个数据值的 1000 行内容关联起来,而这些数据值是测试编写者从来没有读过和考虑过的。这么做将会使得你的用例有 1000 个失败的理由 —— 常常改一行代码就会导致快照失效。这个频率有多高?对于每个空格,注释或少量的 CSS/HTML 更改。不仅如此,失败结果不会给出关于失败的任何提示,因为它只是检查 1000 行内容有没有改动,而且测试编写人员不得不将这一大堆他无法自己验证的长文档作为期望的 true。所有这些都是测试目标不明确、测试目标过多的症状。 477 | 478 | 这将会使得我们的测试带上一大堆我们以后可能不会再看的数据。这样做有什么问题?你的测试将有无数种理由失败,因为你放入了太多自己不需要关心的结果数据进去,而你又无法抽出足够的精力从结果的 diff 中判断当前表现是否符合期望。 479 | 480 | 仅在很少的场景下,长外部快照是可以接受的——当测试断言 schema 而不是数据时(提取值并关注其中的字段),或者当快照的内容很少被更改时。 481 | 482 |
483 | 484 | ❌ **否则:** 一个 UI 测试挂掉了。代码看起来 ok,屏幕上正确渲染了每个像素,发生了什么?- 你的测试发现跟之前的快照相比,新的快照 markdown 中多了一个空格…… 485 | 486 |
487 | 488 |
代码示例 489 | 490 |
491 | 492 | ### :thumbsdown: 反例: 为我们的用例耦合看不到的 2000 行代码 493 | 494 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg 495 | "Examples with Jest") 496 | 497 | ```javascript 498 | it('TestJavaScript.com is renderd correctly', () => { 499 | 500 | //Arrange 501 | 502 | //Act 503 | const receivedPage = renderer 504 | .create( Test JavaScript < /DisplayPage>) 505 | .toJSON(); 506 | 507 | //Assert 508 | expect(receivedPage).toMatchSnapshot(); 509 | //We now implicitly maintain a 2000 lines long document 510 | //every additional line break or comment - will break this test 511 | 512 | }); 513 | ``` 514 |
515 | 516 | ### :clap: 正例: 期望是可见且集中的 517 | ```javascript 518 | it('When visiting TestJavaScript.com home page, a menu is displayed', () => { 519 | //Arrange 520 | 521 | //Act 522 | receivedPage tree = renderer 523 | .create( Test JavaScript < /DisplayPage>) 524 | .toJSON(); 525 | 526 | //Assert 527 | 528 | const menu = receivedPage.content.menu; 529 | expect(menu).toMatchInlineSnapshot(` 530 |
    531 |
  • Home
  • 532 |
  • About
  • 533 |
  • Contact
  • 534 |
535 | `); 536 | }); 537 | ``` 538 | 539 |
540 | 541 | 542 |

543 | 544 | ## ⚪ ️1.9 不要写全局的 fixtures 和 seeds,而是放在每个测试中 545 | 546 | :white_check_mark: **建议:** 参照黄金法则,每条测试需要在它自己的 DB 行中运行避免互相污染。现实中,这条规则经常被打破:为了性能提升而在执行测试前全局初始化数据库([也被称为‘test fixture’](https://en.wikipedia.org/wiki/Test_fixture))。尽管性能很重要,但是它可以通过后面讲的「分组件测试」缓和。为了减轻复杂度,我们可以在每个测试中只初始化自己需要的数据。除非性能问题真的非常显著,那么可以做一定的妥协——仅在全局放不会改变的数据(比如 query)。 547 |
548 | 549 | 550 | ❌ **否则:** 一部分测试挂了,我们的团队花费大量宝贵时间后发现,是由于两个测试同时改变了同一个 seed 数据导致的。 551 | 552 | 553 |
554 | 555 |
代码示例 556 | 557 |
558 | 559 | ### :thumbsdown: 反例: 用例之间不独立,而是依赖同一个全局钩子来生成全局 DB 数据 560 | 561 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg 562 | "Examples with Jest") 563 | 564 | ```javascript 565 | before(() => { 566 | //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework 567 | await DB.AddSeedDataFromJson('seed.json'); 568 | }); 569 | it("When updating site name, get successful confirmation", async () => { 570 | //I know that site name "portal" exists - I saw it in the seed files 571 | const siteToUpdate = await SiteService.getSiteByName("Portal"); 572 | const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); 573 | expect(updateNameResult).to.be(true); 574 | }); 575 | it("When querying by site name, get the right site", async () => { 576 | //I know that site name "portal" exists - I saw it in the seed files 577 | const siteToCheck = await SiteService.getSiteByName("Portal"); 578 | expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ 579 | }); 580 | 581 | ``` 582 |
583 | 584 | ### :clap: 正例: 每个用例操作它自己的数据集 585 | 586 | ```javascript 587 | it("When updating site name, get successful confirmation", async () => { 588 | //test is adding a fresh new records and acting on the records only 589 | const siteUnderTest = await SiteService.addSite({ 590 | name: "siteForUpdateTest" 591 | }); 592 | 593 | const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); 594 | 595 | expect(updateNameResult).to.be(true); 596 | }); 597 | 598 | ``` 599 | 600 |
601 | 602 | 603 |
604 | 605 | ## ⚪ ️ 1.10 不要 catch 错误,expect 它们 606 | :white_check_mark: **建议:** 当你测试一些输入是否会触发错误时,使用 try-catch-finally 测试看起来似乎没问题。但结果会比较奇葩(会隐藏测试的意图和期望结果),并且把 tc 复杂化(比如下面的例子)。 607 | 608 | 一个更优雅的替代方法是使用 Chai断言 `expect(method).to.throw` (或者 Jest 的: `expect(method).toThrow()`)。必须保证异常包含一个表示错误类型的属性,否则如果只给出一个通用错误,应用程序没法展示足够的信息。 609 |
610 | 611 | 612 | ❌ **否则:** 从测试报告(如 CI 报告)中查找出错的位置将会很痛苦。 613 | 614 | 615 |
616 | 617 |
代码示例 618 | 619 |
620 | 621 | ### :thumbsdown: 反例: 一个长测试用例,尝试使用 try-catch 断言错误 622 | 623 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg 624 | "Examples with Jest") 625 | 626 | ```javascript 627 | it("When no product name, it throws error 400", async() => { 628 | let errorWeExceptFor = null; 629 | try { 630 | const result = await addNewProduct({name:'nest'});} 631 | catch (error) { 632 | expect(error.code).to.equal('InvalidInput'); 633 | errorWeExceptFor = error; 634 | } 635 | expect(errorWeExceptFor).not.to.be.null; 636 | //if this assertion fails, the tests results/reports will only show 637 | //that some value is null, there won't be a word about a missing Exception 638 | }); 639 | 640 | ``` 641 |
642 | 643 | ### :clap: 正例: 一个人类可读的期望,它很容易被理解,甚至可被 QA 或技术 PM 理解 644 | 645 | ```javascript 646 | it.only("When no product name, it throws error 400", async() => { 647 | expect(addNewProduct)).to.eventually.throw(AppError).with.property('code', "InvalidInput"); 648 | }); 649 | 650 | ``` 651 | 652 |
653 | 654 | 655 | 656 | 657 |

658 | 659 | ## ⚪ ️ 1.11 为你的测试用例打标签 660 | 661 | :white_check_mark: **建议:** 不同的测试需要在不同的场景中执行:快速冒烟、IO 测试、开发者保存或者提交文件后的测试、当一个新的 PR 提交后需要全量执行的端到端测试 等等。你可以用一些 #cold #api #sanity 之类的标签标注测试来达到这个目的,这样你就可以在测试时仅测试想要的子集。如在 mocha 中可以这样唤起用例组 `mocha — grep ‘sanity’` 。 662 |
663 | 664 | 665 | ❌ **否则:** 执行所有的用例,包括执行大量 DB 查询的用例,开发者做的任何小改动都需要等待很长的时间,将会导致开发者不再想运行测试。 666 | 667 | 668 |
669 | 670 |
代码示例 671 | 672 |
673 | 674 | ### :clap: 正例: 将用例标记为‘#cold-test’使得用例执行者可以仅执行快的用例 (Cold===没有 IO 的快速测试,可以在开发人员打字时频繁执行) 675 | 676 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg 677 | "Examples with Jest") 678 | ```javascript 679 | //this test is fast (no DB) and we're tagging it correspondigly 680 | //now the user/CI can run it frequently 681 | describe('Order service', function() { 682 | describe('Add new order #cold-test #sanity', function() { 683 | test('Scenario - no currency was supplied. Expectation - Use the default currency #sanity', function() { 684 | //code logic here 685 | }); 686 | }); 687 | }); 688 | 689 | 690 | ``` 691 | 692 |
693 | 694 | 695 | 696 | 697 |

698 | 699 | ## ⚪ ️1.12 其他常见的优秀测试习惯 700 | 701 | :white_check_mark: **建议:** 本文主要讨论与 Node JS 相关的测试建议,或者至少可以用 Node JS 作为例子。然而,本小节整理了一些众所周知的与 Node 无关的技巧。 702 | 703 | 学习并实践 [TDD 原则](https://www.sm-cloud.com/book-review-test-driven-development-by-example-a-tldr/)——它对许多人来说非常有价值,但是如果它们不适合你的风格,不要害怕,你不是唯一一个。尝试在写代码之前使用 [red-green-refactor 风格](https://blog.cleancoder.com/uncle-bob/2014/12/17/TheCyclesOfTDD.html) 编写测试,确保每个测试只检查一项,当你发现一个 bug 时,在修复前新增一个测试在未来检测到它,让每一个测试在变绿之前至少失败一次,快速编写一个简单的代码模块以满足这个测试,然后逐渐将其重构至生产水平,避免任何依赖环境(路径、操作系统等)。 704 | 705 | ❌ **否则:** 你会错过数十年来智慧的结晶。 706 | 707 |

708 | 709 | 710 | # 第二章: 后端测试 711 | 712 | ## ⚪ ️2.1 丰富您的测试组合:不局限于单元测试和测试金字塔 713 | 714 | :white_check_mark: **建议:** [测试金字塔](https://martinfowler.com/bliki/TestPyramid.html),虽然已经有超过 10 年的历史了,但是它仍是一个很好的相关模型,它提出了三种测试类型,并且影响了大多数开发人员的测试策略。与此同时,大量闪亮的新测试技术出现了,并隐藏在测试金字塔的阴影下。考虑到近 10 年来我们所看到的所有巨变(微服务、云、无服务器),这个非常老的模型是否仍能适用于所有类型的应用?测试界不应该考虑欢迎新的测试技术吗? 715 | 716 | 请不要误解,在 2019 年,测试金字塔、TDD、单测仍然是强大的技术,且对于大多数应用仍是最佳选择。但是像其他模型一样,尽管它有用,但是一定会在[某些时候出问题](https://en.wikipedia.org/wiki/All_models_are_wrong)。例如,我们有一个 IOT 应用,将许多事件注入一个 Kafka/RabbitMQ 这样的消息总线中,然后这些事件流入一些数据仓库并被分析 UI 查询。我们真的需要花费 50% 的测试预算去为这个几乎没有逻辑的集成中心化的应用写单测吗?随着应用类型(机器人、密码、Alexa-skills)的多样性增长,测试金字塔可能将不再是某些场景的最佳选择了。 717 | 718 | 是时候丰富你的测试组合并了解更多的测试类型了(下一节会给你一些小建议),这些类似于测试金字塔的思维模型与你所面临的现实问题更匹配('嘿,我们的API 挂了,试试消费者驱动的合同测试!'),让您的测试多样化,比如建立基于风险分析的检查模型 —— 评估可能出现问题的位置,并提供一些预防措施以减轻这些潜在风险。 719 | 720 | 需要注意的是:软件世界中的 TDD 模型面临两个极端的态度,一些人鼓吹到处使用它,另一些人则认为它是魔鬼。 每个说绝对的人都是错的 :] 721 | 722 |
723 | 724 | 725 | ❌ **否则:** 你将错过一些超高投入产出比的工具,比如 Fuzz、lint、mutation 这些工具只需 10 分钟配置就能贡献价值。 726 | 727 | 728 |
729 | 730 |
代码示例 731 | 732 |
733 | 734 | ### :clap: 正例: Cindy Sridharan 在她的文章“测试微服务——理智的方式”中提出了一个丰富的测试组合 735 | ![alt text](assets/zh-CN/bp-12-rich-testing.jpg "Cindy Sridharan suggests a rich testing portfolio in her amazing post ‘Testing Microservices — the sane way’") 736 | 737 | ☺️Example: [YouTube: “Beyond Unit Tests: 5 Shiny Node.JS Test Types (2018)” (Yoni Goldberg)](https://www.youtube.com/watch?v=-2zP494wdUY&feature=youtu.be) 738 | 739 |
740 | 741 | ![alt text](assets/bp-12-Yoni-Goldberg-Testing.jpeg "A test name that constitutes 3 parts") 742 | 743 | 744 |
745 | 746 | 747 | 748 | 749 |

750 | 751 | ## ⚪ ️2.2 组件化测试可能是最有效的利器 752 | 753 | :white_check_mark: **建议:** 应用的每个单元测试仅能覆盖应用的一小部分,覆盖全部会非常麻烦,而端到端测试可以很轻松地覆盖大量区域,但是比较脆弱而且很慢。何不找一个平衡点:写一些比单测大,但是比端到端测试小的测试。组件测试是测试世界的一颗遗珠——它找到了两个模式的最佳平衡点:不错的性能和使用 TDD 模式的可能性 + 真实且强大的覆盖率。 754 | 755 | 组件测试关注于微服务“单元”,他们反对 API,不 mock 任何属于微服务本身的东西(比如:真实的 DB,甚至是该 DB 的内存版本)但是 stub 所有外部的东西比如调用其他微服务。这么做,我们测试我们部署的部分,由外而内地覆盖应用,节省大量时间。 756 | 757 |
758 | 759 | 760 | ❌ **否则:** 你可能花了好几天写单测,却发现仅得到了 20% 的系统覆盖率。 761 | 762 | 763 |
764 | 765 |
代码示例 766 | 767 |
768 | 769 | ### :clap: 正例: 使用 Supertest 测试 Express API (快速、覆盖很多层) 770 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg 771 | "Examples with Jest") 772 | 773 | ![alt text](assets/zh-CN/bp-13-component-test-yoni-goldberg.png " [Supertest](https://www.npmjs.com/package/supertest) allows approaching Express API in-process (fast and cover many layers)") 774 | 775 |
776 | 777 |

778 | 779 | ## ⚪ ️2.3 保证新的 release 不会破坏 API 的使用 780 | 781 | :white_check_mark: **建议:** 你的微服务有很多的客户,而你为了兼容性运行着该服务的很多版本(keeping everyone happy)。当你改了某个字段后“砰!”,依赖该字段的几个重要的客户炸锅了。服务端满足所有客户的期望是非常难的——另一方面,客户无法发起测试,因为服务端控制着 release。 [Consumer-driven contracts and the framework PACT](https://docs.pact.io/) 诞生了,它以一种破坏性的方式规范了这一流程——不再由服务端定义测试计划,而是客户端决定服务端的测试!PACT 可以记录客户端的期望——“中间人(Broker)”,并放置到共享空间,服务端可以 pull 下来这写期望并利用 PACT 的库在所有的版本中检测是否有被破坏的契约——有客户端的期望没有被满足。通过这种方式,所有客户端-服务端不匹配的 API 将会在 构建/CI 阶段被 catch 到,从而减少你大量的烦恼。 782 | 783 |
784 | 785 | 786 | ❌ **否则:** 所有的变更将带来繁琐的手动测试,导致开发者惧怕发布。 787 | 788 | 789 |
790 | 791 |
代码示例 792 | 793 |
794 | 795 | ### :clap: 正例: 796 | 797 | ![](https://img.shields.io/badge/🔧%20Example%20using%20PACT-blue.svg 798 | "Examples with PACT") 799 | 800 | ![alt text](assets/zh-CN/bp-14-testing-best-practices-contract-flow.png ) 801 | 802 | 803 |
804 | 805 | 806 | 807 |

808 | 809 | 810 | ## ⚪ ️ 2.4 单独测试你的中间件 811 | 812 | :white_check_mark: **建议:** 许多人拒绝测试中间件,是因为它们仅占据系统的一小部分而且依赖真实的 Express server。这两个原因都不正确——中间件虽然小,但是影响全部或者大部分请求,而且可以被简单地作为纯函数测试(参数为 {req,res} JS 对象)。测试中间件函数,你仅需调用它,并且 spy ([比如使用 Sinon](https://www.npmjs.com/package/sinon)) {req,res} 的交互以保证函数执行了正确的行为。[node-mock-http](https://www.npmjs.com/package/node-mocks-http) 库更进一步:它还监听了 {req,res} 对象的行为。例如,它可以断言 res 对象上的 http 状态是否符合预期。(看下面的例子) 813 |
814 | 815 | 816 | ❌ **否则:** Express 中间件上的一个 bug === 所有或者大部分请求的 bug 817 | 818 | 819 |
820 | 821 |
代码示例 822 | 823 |
824 | 825 | ### :clap:正例: 隔离地测试中间件,不发出网络调用或唤醒整个Express机器 826 | 827 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg 828 | "Examples with Jest") 829 | 830 | ```javascript 831 | //the middleware we want to test 832 | const unitUnderTest = require('./middleware') 833 | const httpMocks = require('node-mocks-http'); 834 | //Jest syntax, equivelant to describe() & it() in Mocha 835 | test('A request without authentication header, should return http status 403', () => { 836 | const request = httpMocks.createRequest({ 837 | method: 'GET', 838 | url: '/user/42', 839 | headers: { 840 | authentication: '' 841 | } 842 | }); 843 | const response = httpMocks.createResponse(); 844 | unitUnderTest(request, response); 845 | expect(response.statusCode).toBe(403); 846 | }); 847 | 848 | ``` 849 | 850 |
851 | 852 | 853 | 854 | 855 |

856 | 857 | ## ⚪ ️2.5 使用静态分析工具度量并指导重构 858 | 859 | :white_check_mark: **建议:** 使用静态度量工具可以帮助你客观地提升代码质量并使其可维护。你可以将静态分析工具放在你的 CI 中。除了普通 linting 外,它的主要卖点是结合多文件的上下文来检查质量(例如:发现重复定义)、执行高级分析(例如:代码复杂度)以及跟踪 code issue 的历史和进度。有两个工具供你使用:[Sonarqube](https://www.sonarqube.org/) (2,600+ stars) and [Code Climate](https://codeclimate.com/) (1,500+ stars) 860 | 861 | 贡献:: [Keith Holliday](https://github.com/TheHollidayInn) 862 | 863 |
864 | 865 | 866 | ❌ **否则:** 由于代码质量差,再新的库和 feature 也无法拯救你的 bug 和性能。 867 | 868 | 869 |
870 | 871 |
代码示例 872 | 873 |
874 | 875 | ### :clap: 正例:  CodeClimate —— 一个用于发现复杂方法的商业工具 876 | 877 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Code%20Climate-blue.svg 878 | "Examples with CodeClimate") 879 | 880 | ![alt text](assets/bp-16-yoni-goldberg-quality.png " CodeClimat, a commercial tool that can identify complex methods:") 881 | 882 |
883 | 884 | 885 | 886 | 887 |

888 | 889 | ## ⚪ ️ 2.6 你是否准备好迎接 Node 相关的噪声 890 | 891 | :white_check_mark: **建议:** 怪异的是,大部分软件测试仅关注逻辑和数据,但是最糟糕(而且很难减轻)的往往是基础设施问题。例如,你测试过当你的进程存储过载、服务器/进程挂掉时的表现吗?或者你的监控系统会检测到 API 减慢 50% 了吗?为了测试及减轻类似问题,Netflix 设立了 [噪声工程](https://principlesofchaos.org/)。它的目的是为我们的系统在故障问题下的健壮性提供意识、框架及工具。比如,著名的工具之一 [噪声猴子](https://github.com/Netflix/chaosmonkey),随机地杀掉服务以保证我们的服务仍服务于用户,而不是仅依赖一个单独的服务器(Kubernetes 也有一个版本 [kube-monkey](https://github.com/asobti/kube-monkey) 用于杀掉 pods)。这些工具都是作用于服务器/平台层面,但如果你想测试及生产纯粹的 Node 噪声比如检查你的 Node 进程如何处理未知错误、未知的 promise rejection、v8 内存超过 1.7GB 的限制以及当事件循环经常卡住后你的 UX 是否仍正常运行?为了解决上面提到的这些问题, [node-chaos](https://github.com/i0natan/node-chaos-monkey)(alpha)提供了各种 Node 相关的噪声。 892 |
893 | 894 | 895 | ❌ **否则:** 墨菲定律一定会无情地砸中你的产品,跑不掉的。 896 | 897 | 898 |
899 | 900 |
代码示例 901 | 902 |
903 | 904 | ### :clap: 正例: Node-chaos 可以生成所有类型的 Node.js 问题,因此您可以测试您的应用程序对混乱的适应能力 905 | ![alt text](assets/bp-17-yoni-goldberg-chaos-monkey-nodejs.png "Node-chaos can generate all sort of Node.js pranks so you can test how resilience is your app to chaos") 906 | 907 |
908 | 909 |
910 | 911 | ## ⚪ ️2.7 不要写全局的 fixtures 和 seeds,而是放在每个测试中 912 | 913 | :white_check_mark: **建议:** 参照黄金法则,每条测试需要在它自己的 DB 行中运行避免互相污染。现实中,这条规则经常被打破:为了性能提升而在执行测试前全局初始化数据库([也被称为‘test fixture’](https://en.wikipedia.org/wiki/Test_fixture))。尽管性能很重要,但是它可以通过后面讲的「分组件测试」缓和。为了减轻复杂度,我们可以在每个测试中只初始化自己需要的数据。除非性能问题真的非常显著,那么可以做一定的妥协——仅在全局放不会改变的数据(比如 query)。 914 |
915 | 916 | 917 | ❌ **否则:** 一部分测试挂了,我们的团队花费大量宝贵时间后发现,是由于两个测试同时改变了同一个 seed 数据导致的。 918 | 919 | 920 |
921 | 922 |
代码示例 923 | 924 |
925 | 926 | ### :thumbsdown: 反例: 用例之间不独立,而是依赖同一个全局钩子来生成全局 DB 数据 927 | 928 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg 929 | "Examples with Jest") 930 | 931 | ```javascript 932 | before(() => { 933 | //adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework 934 | await DB.AddSeedDataFromJson('seed.json'); 935 | }); 936 | it("When updating site name, get successful confirmation", async () => { 937 | //I know that site name "portal" exists - I saw it in the seed files 938 | const siteToUpdate = await SiteService.getSiteByName("Portal"); 939 | const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); 940 | expect(updateNameResult).to.be(true); 941 | }); 942 | it("When querying by site name, get the right site", async () => { 943 | //I know that site name "portal" exists - I saw it in the seed files 944 | const siteToCheck = await SiteService.getSiteByName("Portal"); 945 | expect(siteToCheck.name).to.be.equal("Portal"); //Failure! The previous test change the name :[ 946 | }); 947 | 948 | ``` 949 |
950 | 951 | ### :clap: 正例: 每个用例操作它自己的数据集 952 | 953 | ```javascript 954 | it("When updating site name, get successful confirmation", async () => { 955 | //test is adding a fresh new records and acting on the records only 956 | const siteUnderTest = await SiteService.addSite({ 957 | name: "siteForUpdateTest" 958 | }); 959 | 960 | const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); 961 | 962 | expect(updateNameResult).to.be(true); 963 | }); 964 | 965 | ``` 966 | 967 |
968 | 969 |

970 | 971 | # 第三章: 前端测试 972 | 973 | ## ⚪ ️ 3.1 将 UI 与功能分离 974 | 975 | :white_check_mark: **建议:** 当专注于测试组件逻辑时,UI 细节就变成了应该剔除的噪音,这样您的测试就可以集中在纯数据上。实际上,通过抽象从代码中提取所需的数据将降低与图形实现的耦合,仅对纯数据 (vs HTML/CSS 图形细节) 断言,并禁用会拖慢速度的动画。您可能会试图避免渲染,仅测试 UI 后面的部分(例如,服务、操作、存储),但这将导致测试与实际情况不太相符,「正确的数据根本无法到达 UI」这种问题就无法发现。 976 | 977 | 978 |
979 | 980 | ❌ **否则:** 您的测试的纯计算数据可能在 10ms 内就准备好了,但是由于一些花哨和无关的动画,整个测试将持续500ms (100个测试 = 1分钟) 981 | 982 | 983 |
984 | 985 |
代码示例 986 | 987 |
988 | 989 | ### :clap: 正例: 分离 UI 细节 990 | 991 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React-blue.svg 992 | "Examples with React") ![](https://img.shields.io/badge/🔧%20Example%20using%20React%20Testing%20Library-blue.svg 993 | "Examples with react-testing-library") 994 | 995 | ```javascript 996 | test('When users-list is flagged to show only VIP, should display only VIP members', () => { 997 | // Arrange 998 | const allUsers = [ 999 | { id: 1, name: 'Yoni Goldberg', vip: false }, 1000 | { id: 2, name: 'John Doe', vip: true } 1001 | ]; 1002 | 1003 | // Act 1004 | const { getAllByTestId } = render(); 1005 | 1006 | // Assert - Extract the data from the UI first 1007 | const allRenderedUsers = getAllByTestId('user').map(uiElement => uiElement.textContent); 1008 | const allRealVIPUsers = allUsers.filter((user) => user.vip).map((user) => user.name); 1009 | expect(allRenderedUsers).toEqual(allRealVIPUsers); //compare data with data, no UI here 1010 | }); 1011 | 1012 | ``` 1013 | 1014 |
1015 | 1016 | ### :thumbsdown: 反例: 混杂了 UI 细节和数据的断言 1017 | ```javascript 1018 | test('When flagging to show only VIP, should display only VIP members', () => { 1019 | // Arrange 1020 | const allUsers = [ 1021 | {id: 1, name: 'Yoni Goldberg', vip: false }, 1022 | {id: 2, name: 'John Doe', vip: true } 1023 | ]; 1024 | 1025 | // Act 1026 | const { getAllByTestId } = render(); 1027 | 1028 | // Assert - Mix UI & data in assertion 1029 | expect(getAllByTestId('user')).toEqual('[
  • John Doe
  • ]'); 1030 | }); 1031 | 1032 | ``` 1033 | 1034 |
    1035 | 1036 | 1037 | 1038 | 1039 |

    1040 | 1041 | 1042 | ## ⚪ ️ 3.2 使用不太容易改变的属性去查询 HTML 元素 1043 | 1044 | :white_check_mark: **建议:**通过不太同意受图形变更印象的属性查询 HTML 元素(例如 form label,而不是 CSS selector)。如果指定的元素没有这样的属性,则创建一个专用的测试属性,如“test-id-submit-button”。这样做不仅可以确保您的功能/逻辑测试不会因为外观变化而中断,而且整个团队可以清楚地看到,测试使用了这个元素和属性,不应该删除它。 1045 | 1046 |
    1047 | 1048 | ❌ **否则:** 你想要测试一个跨越许多组件、逻辑和服务的登录功能,一切都设置得很完美——stub、spy、Ajax 调用都是隔离的。所有似乎是完美的。然后测试失败,因为开发者将 div CSS 类从 'thick-border' 更改为 'thin-border'。 1049 | 1050 |
    1051 | 1052 |
    代码示例 1053 | 1054 |
    1055 | 1056 | ### :clap: 正例: 使用专用的 attrbiute 查询元素进行测试 1057 | 1058 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React-blue.svg 1059 | "Examples with React") 1060 | 1061 | ```html 1062 | // the markup code (part of React component) 1063 |

    1064 | 1065 | {value} 1066 | 1067 |

    1068 | ``` 1069 | 1070 | ```javascript 1071 | // this example is using react-testing-library 1072 | test('Whenever no data is passed to metric, show 0 as default', () => { 1073 | // Arrange 1074 | const metricValue = undefined; 1075 | 1076 | // Act 1077 | const { getByTestId } = render(); 1078 | 1079 | expect(getByTestId('errorsLabel')).text()).toBe("0"); 1080 | }); 1081 | 1082 | ``` 1083 | 1084 |
    1085 | 1086 | ### :thumbsdown: 反例: 依赖 css attribute 1087 | ```html 1088 | 1089 | {value} 1090 | ``` 1091 | 1092 | ```javascript 1093 | // this exammple is using enzyme 1094 | test('Whenever no data is passed, error metric shows zero', () => { 1095 | // ... 1096 | 1097 | expect(wrapper.find("[className='d-flex-column']").text()).toBe("0"); 1098 | }); 1099 | ``` 1100 | 1101 | 1102 |
    1103 | 1104 | 1105 | 1106 | 1107 |
    1108 | 1109 | ## ⚪ ️ 3.3 只要有可能,使用真实且完全渲染的组件进行测试 1110 | 1111 | :white_check_mark: **建议:** 只要大小合适,就像用户那样从外部测试组件,完全渲染 UI,对其进行操作,并断言呈现的 UI 的行为符合预期。避免各种 mock、部分和 shallow render——这么做可能会由于缺乏细节导致未捕获的 bug,并且由于测试与内部的混在一起将增加维护成本(参见小结“多用黑盒测试”)。如果其中一个子组件明显拖慢测试(如动画)或使很难配置,可以考虑主动用伪组件替换它。 1112 | 1113 | 综上所述,需要注意的是: 这种技术适用于封装一定数量子组件的中小型组件。如果一个组件包含太多的子组件,那么将很难对失败测试进行定位(分析根本原因),并且可能会变得过于缓慢。在这种情况下,只需针对胖父组件编写少量测试,而针对其子组件编写更多测试。 1114 | 1115 |
    1116 | 1117 | ❌ **否则:** 之前通过调用组件的私有方法来测试组件的内部状态。后续重构组件时你必须重构所有测试。你真的有能力进行这种程度的维护吗? 1118 | 1119 | 1120 |
    1121 | 1122 |
    代码示例 1123 | 1124 |
    1125 | 1126 | ### :clap: 正例: 操作一个充分渲染的真实组件 1127 | 1128 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React-blue.svg 1129 | "Examples with React") ![](https://img.shields.io/badge/🔧%20Example%20using%20Enzyme-blue.svg 1130 | "Examples with Enzyme") 1131 | 1132 | ```javascript 1133 | class Calendar extends React.Component { 1134 | static defaultProps = {showFilters: false} 1135 | 1136 | render() { 1137 | return ( 1138 |
    1139 | A filters panel with a button to hide/show filters 1140 | 1141 |
    1142 | ) 1143 | } 1144 | } 1145 | 1146 | //Examples use React & Enzyme 1147 | test('Realistic approach: When clicked to show filters, filters are displayed', () => { 1148 | // Arrange 1149 | const wrapper = mount() 1150 | 1151 | // Act 1152 | wrapper.find('button').simulate('click'); 1153 | 1154 | // Assert 1155 | expect(wrapper.text().includes('Choose Filter')); 1156 | // This is how the user will approach this element: by text 1157 | }) 1158 | 1159 | 1160 | ``` 1161 | 1162 | ### :thumbsdown: 反例: 通过 shallow render 测试伪组件 1163 | ```javascript 1164 | 1165 | test('Shallow/mocked approach: When clicked to show filters, filters are displayed', () => { 1166 | // Arrange 1167 | const wrapper = shallow() 1168 | 1169 | // Act 1170 | wrapper.find('filtersPanel').instance().showFilters(); 1171 | // Tap into the internals, bypass the UI and invoke a method. White-box approach 1172 | 1173 | // Assert 1174 | expect(wrapper.find('Filter').props()).toEqual({title: 'Choose Filter'}); 1175 | // what if we change the prop name or don't pass anything relevant? 1176 | }) 1177 | 1178 | ``` 1179 | 1180 |
    1181 | 1182 |
    1183 | 1184 | 1185 | ## ⚪ ️ 3.4 不要 sleep,使用框架内置的对 async 事件的支持。并且尝试提效。 1186 | 1187 | :white_check_mark: **建议:** 在许多情况下,被测试单元的完成时间是未知的 (例如,animation 挂起了元素表现 )——在这种情况下,不要 sleep (例如setTimeout),并是使用大多数框架提供的更靠谱的方法。一些库允许等待操作 (例如 [Cypress .request('url')](https://docs.cypress.io/guides/references/best-practices.html#Unnecessary-Waiting)),另一些库提供用于等待的 API,如 [@testing-library/dom 方法 wait(expect(element))](https://testing-library.com/docs/guide-disappearance)。有时一种更优雅的方法是 stub 慢的资源,比如API,然后一旦响应时间变得确定,组件就可以显式地重新渲染。当依赖一些 sleep 的外部组件时,[加快时钟](https://jestjs.io/docs/en/timer-mocks)可能会提供帮助。sleep 是一种需要避免的模式,因为它会迫使您的测试变得缓慢或有风险(当等待的时间太短时)。当 sleep 和轮询不可避免且测试框架原生不支持时,一些npm库 (如 [wait-for-expect](https://www.npmjs.com/package/wait-for-expect)) 可以帮助解决半确定性问题。 1188 |
    1189 | 1190 | ❌ **否则:** 当 sleep 时间长时,测试速度会慢一个数量级。当尝试缩短 sleep 时间时,如果被测试的单元没有及时响应,则测试将失败。这时你不得不在脆弱的测试和糟糕的性能之间进行权衡。 1191 | 1192 | 1193 |
    1194 | 1195 |
    代码示例 1196 | 1197 |
    1198 | 1199 | ### :clap: 正例: E2E API 仅在异步完成后 resolve (Cypress) 1200 | 1201 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React-blue.svg 1202 | "Examples with React") ![](https://img.shields.io/badge/🔧%20Example%20using%20React%20Testing%20Library-blue.svg 1203 | "Examples with react-testing-library") 1204 | 1205 | ```javascript 1206 | // using Cypress 1207 | cy.get('#show-products').click()// navigate 1208 | cy.wait('@products')// wait for route to appear 1209 | // this line will get executed only when the route is ready 1210 | 1211 | ``` 1212 | 1213 | ### :clap: 正例: 测试库等待 DOM 元素 1214 | 1215 | ```javascript 1216 | // @testing-library/dom 1217 | test('movie title appears', async () => { 1218 | // element is initially not present... 1219 | 1220 | // wait for appearance 1221 | await wait(() => { 1222 | expect(getByText('the lion king')).toBeInTheDocument() 1223 | }) 1224 | 1225 | // wait for appearance and return the element 1226 | const movie = await waitForElement(() => getByText('the lion king')) 1227 | }) 1228 | 1229 | ``` 1230 | 1231 | ### :thumbsdown: 反例: 自己写 sleep 代码 1232 | ```javascript 1233 | 1234 | test('movie title appears', async () => { 1235 | // element is initially not present... 1236 | 1237 | // custom wait logic (caution: simplistic, no timeout) 1238 | const interval = setInterval(() => { 1239 | const found = getByText('the lion king'); 1240 | if(found){ 1241 | clearInterval(interval); 1242 | expect(getByText('the lion king')).toBeInTheDocument(); 1243 | } 1244 | 1245 | }, 100); 1246 | 1247 | // wait for appearance and return the element 1248 | const movie = await waitForElement(() => getByText('the lion king')) 1249 | }) 1250 | 1251 | ``` 1252 | 1253 |
    1254 | 1255 | 1256 |
    1257 | 1258 | ## ⚪ ️ 3.5 观察内容是如何通过网络提供的 1259 | 1260 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Google%20LightHouse-blue.svg 1261 | "Examples with Lighthouse") 1262 | 1263 | ✅ **建议:** 使用一些活动监视器,以确保在真实网络下的页面负载是最优的——这包括了一些用户体验问题:如缓慢的页面负载或未压缩的包。检查工具市场很丰富:像 [pingdom](https://www.pingdom.com/)、AWS CloudWatch、[gcp StackDriver](https://cloud.google.com/monitoring/uptime-checks/) 这样的基础工具可以很容易地配置来监视服务器是否处于活动状态,并在合理的 SLA 下响应。不过这只解决了表面上的问题,因此最好选择专门用于前端的工具 (如 [lighthouse](https://developers.google.com/web/tools/lighthouse/)、[pagespeed](https://developers.google.com/speed/pagespeed/insights/)) 以进行更全面的分析。注意力应该放在症状和直接影响用户体验的指标上,比如页面加载时间、[有意义的绘制](https://scotch.io/courses/10-web-performance-audit-tips-for-your-next-billion-users-in-2018/fmp-first-meaningful-paint)、[页面可交互(TTI) 时间](https://calibreapp.com/blog/time-to-interactive/)。最重要的是,你还可以关注技术原因,比如确保内容被压缩、第一个字节的时间、优化图像、确保合理的 DOM 大小、SSL 和许多其他方面。建议在开发期间使用这些丰富的监视器,作为 CI 的一部分,最重要的是在生产服务器/CDN上 24x7 使用它们。 1264 | 1265 |
    1266 | 1267 | ❌ **否则:** 在精心设计了一个UI、通过了100%的功能测试并进行了复杂的打包之后,用户体验却因为 CDN 的错误配置变得糟糕而缓慢。 1268 | 1269 |
    1270 | 1271 |
    代码示例 1272 | 1273 | ### :clap: 正例: Lighthouse 页面加载检查报告 1274 | 1275 | ![](/assets/lighthouse2.png "Lighthouse page load inspection report") 1276 | 1277 | 1278 |
    1279 | 1280 | 1281 |
    1282 | 1283 | ## ⚪ ️ 3.6 stub 古怪或缓慢的资源如后端 API 1284 | 1285 | :white_check_mark: **建议:** 当编写你的主流测试 (不是 E2E 测试) 时,避免接触任何超出你职责和控制范围的资源,如后端 API,而是使用 stub(即测试替身)。使用一些测试替身库 (如[Sinon](https://sinonjs.org/)、[test double](https://www.npmjs.com/package/testdouble) 等) 来 stub API 响应,而不是真正的对API的网络调用。最大的好处是防止出现故障——测试或预发环境下 api 的定义不是很稳定,尽管组件的表现正确(生产环境不适合测试,它通常会限制请求),但有时会请求失败。通过 stub 允许模拟各种 API 行为,比如当没有找到数据或 API 抛出错误时测试组件行为。最后但并非最不重要的原因是,网络调用将大大降低测试速度。 1286 | 1287 |
    1288 | 1289 | ❌ **否则:** 测试的平均时长不在是几毫秒,一个经典的 API 调用花费 100ms+,这使得每个用例变慢 ~20x。 1290 | 1291 | 1292 |
    1293 | 1294 |
    代码示例 1295 | 1296 |
    1297 | 1298 | ### :clap: 正例: stub 或拦截 API 调用 1299 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React-blue.svg 1300 | "Examples with React") ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg 1301 | "Examples with react-testing-library") 1302 | 1303 | ```javascript 1304 | 1305 | // unit under test 1306 | export default function ProductsList() { 1307 | const [products, setProducts] = useState(false) 1308 | 1309 | const fetchProducts = async() => { 1310 | const products = await axios.get('api/products') 1311 | setProducts(products); 1312 | } 1313 | 1314 | useEffect(() => { 1315 | fetchProducts(); 1316 | }, []); 1317 | 1318 | return products ?
    {products}
    :
    No products
    1319 | } 1320 | 1321 | // test 1322 | test('When no products exist, show the appropriate message', () => { 1323 | // Arrange 1324 | nock("api") 1325 | .get(`/products`) 1326 | .reply(404); 1327 | 1328 | // Act 1329 | const {getByTestId} = render(); 1330 | 1331 | // Assert 1332 | expect(getByTestId('no-products-message')).toBeTruthy(); 1333 | }); 1334 | 1335 | ``` 1336 | 1337 |
    1338 | 1339 |
    1340 | 1341 | ## ⚪ ️ 3.7 写几个跨越整个系统的端到端测试 1342 | 1343 | :white_check_mark: **建议:** 虽然 E2E (端到端) 通常表示在真实浏览器中进行 UI 测试(见 3.6),但某些情况下,它们表示覆盖整个系统的测试,包括真正的后端。后一种测试非常有价值,因为它们涵盖了前端和后端之间的集成 bug,这些 bug 可能是由于沟通 schema 时产生误会导致的。它们也是一种有效的方法来发现 backend-to-backend 集成问题 (例如微服务 A 将错误的信息发送给微服务 B) 甚至检测部署失败,目前后端没有像 [Cypress](https://www.cypress.io/) 和 [Puppeteer](https://github.com/GoogleChrome/puppeteer) 友好的 UI 框架一样友好且成熟的 E2E 框架。这种测试的缺点是,配置一个包含如此多组件的环境的成本很高,而且大多数组件都很脆弱——假设有 50 个微服务,即使其中一个失败,整个 E2E 也会失败。出于这个原因,我们应该少用这种技术,大概1-10个就够了。也就是说,即使是少量的 E2E 测试也很有可能捕获它们所针对的问题——部署和集成故障。建议在与生产环境相似的预发运行它们。 1344 | 1345 |
    1346 | 1347 | ❌ **否则:** UI 可能在测试它的功能上投入了大量的精力,但最后才意识到后端返回的有效负载 (UI 必须使用的数据模式) 与预期有很大的不同。 1348 | 1349 |
    1350 | 1351 | ## ⚪ ️ 3.8 通过复用登录凭证提速 E2E 测试 1352 | 1353 | :white_check_mark: **建议:** 在涉及真实的后端并依赖有效的用户 token 进行 API 调用的 E2E 测试中,我们没有必要将测试按照「创建用户并在每个请求中登录」的级别隔离。相反,在测试执行开始之前只登录一次 (即 before-all hook),将 token 保存在一些本地存储中,并在请求之间复用它。这似乎违反了核心测试原则之一——保持测试的自治,不要耦合资源。虽然这是一个合理的担忧,但在 E2E 测试中,性能是一个关键问题,在执行每个用例之前创建 1-3 个 API 请求可能会大大增加执行时间。复用凭证并不意味着测试必须基于相同的用户记录——如果依赖于用户记录 (例如测试用户付款历史记录),那么要确保生成这些记录作为测试的一部分,并避免与其他测试共享它们。还要记住后端是可以 fake 的——如果你想重点测试前端,那么最好隔离它,然后 stub 后端 API (见 3.6 节)。 1354 | 1355 |
    1356 | 1357 | ❌ **否则:** 给定 200 个测试用例,假设登录耗时 100ms,则需要花费 20s 仅仅用于一遍遍登录。 1358 | 1359 |
    1360 | 1361 |
    代码示例 1362 | 1363 |
    1364 | 1365 | ### :clap: 正例: 在 before-all 而不是 before-each 中登录 1366 | 1367 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Cypress-blue.svg 1368 | "Using Cypress to illustrate the idea") 1369 | 1370 | ```javascript 1371 | let authenticationToken; 1372 | 1373 | // happens before ALL tests run 1374 | before(() => { 1375 | cy.request('POST', 'http://localhost:3000/login', { 1376 | username: Cypress.env('username'), 1377 | password: Cypress.env('password'), 1378 | }) 1379 | .its('body') 1380 | .then((responseFromLogin) => { 1381 | authenticationToken = responseFromLogin.token; 1382 | }) 1383 | }) 1384 | 1385 | // happens before EACH test 1386 | beforeEach(setUser => () { 1387 | cy.visit('/home', { 1388 | onBeforeLoad (win) { 1389 | win.localStorage.setItem('token', JSON.stringify(authenticationToken)) 1390 | }, 1391 | }) 1392 | }) 1393 | 1394 | ``` 1395 | 1396 |
    1397 | 1398 | 1399 | 1400 | 1401 |
    1402 | 1403 | ## ⚪ ️ 3.9 创建一个 E2E 冒烟测试,仅仅走一遍网站地图 1404 | 1405 | :white_check_mark: **建议:** 为了监控生产环境以及开发时的完整性检查,运行一个 E2E 测试,该测试访问所有或大部分站点页面并确保没有被中断。这种测试投资回报率极高,因为它非常容易编写和维护,但可以检测任何类型的故障,包括功能、网络和部署问题。其他类型的冒烟和完备性检查并没有那么可靠和详尽——一些 ops 团队只是 ping 主页 (生产),或者开发人员运行一些集成测试无法发现打包和浏览器问题。毫无疑问,烟雾测试不会取代功能测试,而只是作为一个快速的烟雾探测器。 1406 | 1407 |
    1408 | 1409 | ❌ **否则:** 一切似乎都很完美,所有的测试都通过了,生产环境健康检查也是 OK 的,但是支付组件有一些打包问题,只有 `/Payment` 路径没有渲染。 1410 | 1411 | 1412 |
    1413 | 1414 |
    代码示例 1415 | 1416 |
    1417 | 1418 | ### :clap: 正例: 一个跑一遍所有页面的冒烟测试 1419 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Cypress-blue.svg 1420 | "Using Cypress to illustrate the idea") 1421 | ```javascript 1422 | it('When doing smoke testing over all page, should load them all successfully', () => { 1423 | // exemplified using Cypress but can be implemented easily 1424 | // using any E2E suite 1425 | cy.visit('https://mysite.com/home'); 1426 | cy.contains('Home'); 1427 | cy.contains('https://mysite.com/Login'); 1428 | cy.contains('Login'); 1429 | cy.contains('https://mysite.com/About'); 1430 | cy.contains('About'); 1431 | }) 1432 | ``` 1433 | 1434 |
    1435 | 1436 | 1437 |
    1438 | 1439 | ## ⚪ ️ 3.10 将测试以实时协作文档的形式公开 1440 | 1441 | :white_check_mark: **建议:** 除了提高应用程序的可靠性,测试还带来了另一个极具吸引力的场景——作为实时应用文档。由于测试本质上使用的是一种技术含量较低的产品 / UX 语言,因此使用正确的工具可以将他们作为一个沟通媒介,便捷地协调了所有的同事——开发人员和他们的客户。例如,一些框架允许使用人类可读的语言来表达流程和期望 (即测试计划),这样任何相关人员,包括产品经理,都可以阅读、批准和协作测试,这时测试就成为了实时的需求文档。这种技术也被称为“验收测试”,因为它允许客户用简单的语言定义他的验收标准。这是最纯粹的 [BDD (行为驱动测试)](https://en.wikipedia.org/wiki/Behavior-driven_development)。支持此功能的流行框架之一是 [Cucumber](https://github.com/cucumber/cucumber-js),它具有 JavaScript 风格,参见下面的示例。另一个相似但不同的场景是 [StoryBook](https://storybook.js.org/),它可以将 UI 组件公开为一个图形化的目录,用户可以浏览每个组件的各种状态(如一个栅格组件的 w/o filter,使其渲染多行或者 0 行,等等),查看它的展示形式,以及如何触发状态——这也可以提供给产品人原,但主要是作为实时文档提供给消费这些组件的开发人员。 1442 | 1443 | ❌ **否则:** 你在测试上耗费了大量的资源,如果不利用这项投资来获取更大的价值,是很可惜的。 1444 | 1445 | 1446 |
    1447 | 1448 |
    代码示例 1449 | 1450 |
    1451 | 1452 | ### :clap: 正例: 使用 cucumber-js 以人类语言描述测试 1453 | 1454 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Cocumber-blue.svg "Examples using Cucumber") 1455 | ```javascript 1456 | // this is how one can describe tests using cucumber: plain language that allows anyone to understand and collaborate 1457 | 1458 | Feature: Twitter new tweet 1459 | 1460 | I want to tweet something in Twitter 1461 | 1462 | @focus 1463 | Scenario: Tweeting from the home page 1464 | Given I open Twitter home 1465 | Given I click on "New tweet" button 1466 | Given I type "Hello followers!" in the textbox 1467 | Given I click on "Submit" button 1468 | Then I see message "Tweet saved" 1469 | 1470 | ``` 1471 | 1472 | ### :clap: 正例: 使用 Storybook 展示我们的组件及其各种状态和输入 1473 | ![](https://img.shields.io/badge/🔨%20Example%20using%20StoryBook-blue.svg "Using StoryBook") 1474 | 1475 | 1476 |
    1477 | 1478 | 1479 | 1480 | 1481 | ## ⚪ ️ 3.11 使用自动化工具检测可视化问题 1482 | 1483 | 1484 | :white_check_mark: **建议:** 设置自动化工具来抓取 UI 截屏,并在变更后检测内容重叠或中断等可视化问题。这样不仅可以确保数据的正确性,而且用户可以方便地看到它。这种技术没有被广泛采用,我们的测试思维更倾向于功能测试,但它代表了真实的用户体验,而且可以轻易地发现跨多设备类型的 UI bug。目前部分免费工具可以提供一些基础功能——生成和保存屏幕截图以供肉眼检查。虽然这种方法对于小应用来说可能已经足够了,但是它的缺陷与任何其他手动测试一样
    :任何变更后都需要耗费人力来处理。另一方面,由于缺乏清晰的定义,自动检测 UI 问题非常具有挑战性——这就是“视觉回归”领域解决这个难题的切入点:对比旧 UI 与最新的更改并检测差异。一些开源/免费的工具可以提供这个能力 (例如: [wraith](https://github.com/BBC-News/wraith)、PhantomCSS) 但可能安装耗时比较久。一些商业工具 (如  [Applitools](https://applitools.com/)、[Percy.io](https://percy.io/)) 则更进一步,它们简化了安装过程,并封装了高级特性,如管理 UI、告警、通过去除“视觉噪音”(如广告、动画) 进行智能捕获,甚至可以分析引发问题的 DOM/css 变化的根本原因。 1485 | 1486 |
    1487 | 1488 | ❌ **否则:** 如何评判这样的页面好不好:内容显示正确 (100%测试通过)、加载迅速但有一半内容区域隐藏? 1489 | 1490 | 1491 |
    1492 | 1493 |
    代码示例 1494 | 1495 |
    1496 | 1497 | ### :thumbsdown: 反例: 一个典型的视觉回归 —— 右侧内容展示异常 1498 | 1499 | ![alt text](assets/amazon-visual-regression.jpeg "Amazon page breaks") 1500 | 1501 |
    1502 | 1503 | 1504 | ### :clap: 正例: 配置 wraith 来捕获并比对 UI 快照 1505 | 1506 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Wraith-blue.svg 1507 | "Using Cypress to illustrate the idea") 1508 | 1509 | ``` 1510 | ​# Add as many domains as necessary. Key will act as a label​ 1511 | 1512 | domains: 1513 | english: "http://www.mysite.com"​ 1514 | 1515 | ​# Type screen widths below, here are a couple of examples​ 1516 | 1517 | screen_widths: 1518 | 1519 | - 600​ 1520 | - 768​ 1521 | - 1024​ 1522 | - 1280​ 1523 | 1524 | 1525 | ​# Type page URL paths below, here are a couple of examples​ 1526 | paths: 1527 | about: 1528 | path: /about 1529 | selector: '.about'​ 1530 | subscribe: 1531 | selector: '.subscribe'​ 1532 | path: /subscribe 1533 | ``` 1534 | 1535 | ### :clap: 正例: 使用 Applitools 获取快照比对以及进阶特性 1536 | 1537 | ![](https://img.shields.io/badge/🔨%20Example%20using%20AppliTools-blue.svg 1538 | "Using Cypress to illustrate the idea") ![](https://img.shields.io/badge/🔨%20Example%20using%20Cypress-blue.svg 1539 | "Using Cypress to illustrate the idea") 1540 | 1541 | ```javascript 1542 | import * as todoPage from '../page-objects/todo-page'; 1543 | 1544 | describe('visual validation', () => { 1545 | before(() => todoPage.navigate()); 1546 | beforeEach(() => cy.eyesOpen({ appName: 'TAU TodoMVC' })); 1547 | afterEach(() => cy.eyesClose()); 1548 | 1549 | it('should look good', () => { 1550 | cy.eyesCheckWindow('empty todo list'); 1551 | todoPage.addTodo('Clean room'); 1552 | todoPage.addTodo('Learn javascript'); 1553 | cy.eyesCheckWindow('two todos'); 1554 | todoPage.toggleTodo(0); 1555 | cy.eyesCheckWindow('mark as completed'); 1556 | }); 1557 | }); 1558 | ``` 1559 | 1560 | 1561 | 1562 | 1563 |
    1564 | 1565 | 1566 | 1567 |

    1568 | 1569 | 1570 | # 第四章: 度量测试效果 1571 | 1572 |

    1573 | 1574 | ## ⚪ ️ 4.1 通过足够的覆盖率获取自信,~80% 看起来是个幸运数字 1575 | 1576 | :white_check_mark: **建议:** 测试的目的是为了获取足够的自信去快速迭代,显然,越多代码被测试到,则我们团队越自信。覆盖率用于度量多少代码行(以及分支、语句等)被测试执行到。所以多少够了?10-30% 明显无法证明项目的正确性,而 100% 则非常耗时并且可能会使得你关注太多细枝末节的代码。我们的答案是取决于应用的类型——如果你正在建造 A380 的下一代,那么 100% 是必须的;而对于一个漫画网站,50% 可能太多了。尽管大部分测试拥趸们强调覆盖率门槛是依赖所处环境的,但是他们大部分提到 80% 是一个不错的规则([Fowler: “in the upper 80s or 90s”](https://martinfowler.com/bliki/TestCoverage.html))大概可以满足大部分应用。 1577 | 1578 | 实现建议:你可能想在你的 CI 中设置覆盖率门槛,并阻止不满足要求的构建(也可以为每一个组件设置门槛,见下面的例子)。另外,我们可以监测构建的覆盖率下降(当新提交的代码的覆盖率较低时)——这将推动开发者提升或者至少保持被测试的代码数。说了这么多,覆盖率仅仅是一个可量化的度量值,它并不能确切地证明你的测试的健壮性,你也可能被它骗到(见下一节内容)。 1579 | 1580 |
    1581 | 1582 | 1583 | ❌ **否则:** 信心和数字是相辅相成的,如果无法确保你的测试已经覆盖了了大部分的系统,那你将会害怕,害怕会让你慢下来。 1584 | 1585 | 1586 |
    1587 | 1588 |
    代码示例 1589 | 1590 |
    1591 | 1592 | ### :clap: 正例: 一个经典的覆盖率报告 1593 | ![alt text](assets/bp-18-yoni-goldberg-code-coverage.png "A typical coverage report") 1594 | 1595 |
    1596 | 1597 | ### :clap: 正例: 为每个组件设置覆盖率 (使用 Jest) 1598 | 1599 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Jest-blue.svg 1600 | "Using Cypress to illustrate the idea") 1601 | 1602 | ![alt text](assets/bp-18-code-coverage2.jpeg "Setting up coverage per component (using Jest)") 1603 | 1604 |
    1605 | 1606 | 1607 | 1608 |

    1609 | 1610 | ## ⚪ ️ 4.2 检查覆盖率报告,以发现未覆盖的区域和其他奇怪的地方 1611 | 1612 | :white_check_mark: **建议:** 有些问题隐藏在雷达之下,而使用传统工具很难发现它们。它们通常不是真正的 bug,大多数情况下是应用的怪异表现,而这种表现可能造成严重影响。例如,一些代码区域几乎不会或很少被调用——你以为“PricingCalculator”类只会设置产品价格,结果他几乎不会被调用,即使我们的数据库中有 10000 件商品以及很多交易……代码覆盖率报告可以帮助你发现应用是否按照你的期望执行。初次之外,它高亮了那些类型的代码没有被测试到——80% 的代码被测试并不能说明你的关键部分被覆盖到。生成报告很简单——只需在构造或测试覆盖率时跑你的应用,然后看看花花绿绿的报告来告诉你每一片代码区域被多频繁地调到。如果你花一点时间看看这些数据——你可能会发现一些问题。 1613 | 1614 |
    1615 | 1616 | 1617 | ❌ **否则:** 如果你不知道你的代码中有哪些部分没有被测试到,则你没法准确定位问题的来源。 1618 | 1619 | 1620 |
    1621 | 1622 |
    代码示例 1623 | 1624 |
    1625 | 1626 | ### :thumbsdown: 反例: 这份覆盖率报告有什么问题?基于一个真实的场景,我们跟踪了 QA 中的应用程序使用情况,并发现了一些有趣的登录模式(提示:登录失败的数量是不成比例的,有些地方显然有问题。最终表现为一些前端的 bug 不断触发后端登录API) 1627 | 1628 | ![alt text](assets/bp-19-coverage-yoni-goldberg-nodejs-consultant.png "What’s wrong with this coverage report? based on a real-world scenario where we tracked our application usage in QA and find out interesting login patterns (Hint: the amount of login failures is non-proportional, something is clearly wrong. Finally it turned out that some frontend bug keeps hitting the backend login API") 1629 | 1630 |
    1631 | 1632 | 1633 |

    1634 | 1635 | ## ⚪ ️ 4.3 使用「变异测试」度量逻辑覆盖率 1636 | 1637 | :white_check_mark: **建议:** 传统覆盖率通常是骗人的:它可能显示了 100% 的代码覆盖率,但是你的所有的函数都没有返回正确的结果。怎么回事?它只是简单地度量你的测试代码访问过哪些代码行,而不会检查 tc 是否真正地测试了什么——断言了正确的返回。
    基于变更的测试适用于这个需求。它度量了真正被**测试过**的代码而不是仅仅被**访问过**的。[Stryker](https://stryker-mutator.io/) 是一个用于变异测试的 JavaScript 库,而它的实现很巧妙: 1638 | 1639 | (1) 它有意地改变代码并「植入 bug」。例如代码 newOrder.price===0 会被改成 newOrder.price!=0,这个 “bug”即成为变异。 1640 | 1641 | (2) 它跑一遍用例,如果所有都成功了则说明有问题——这些用例没有真正实现他们发现 bug 的目的,这些变异即所谓的“存活”了。如果用例失败了,那么很棒,变异被杀掉了。 1642 | 1643 | 相对于传统覆盖率,得知所有或者大部分变异被杀掉会给予你更高的信心,而两者花费的时间差不多。 1644 |
    1645 | 1646 | 1647 | ❌ **否则:** 你会误以为 85% 的覆盖率代表你的测试会发现你代码中的 85% 的 bug. 1648 | 1649 |
    1650 | 1651 |
    代码示例 1652 | 1653 |
    1654 | 1655 | ### :thumbsdown: 反例: 100% 覆盖率, 0% 被测试到 1656 | 1657 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Stryker-blue.svg 1658 | "Using Cypress to illustrate the idea") 1659 | ```javascript 1660 | function addNewOrder(newOrder) { 1661 | logger.log(`Adding new order ${newOrder}`); 1662 | DB.save(newOrder); 1663 | Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`); 1664 | 1665 | return {approved: true}; 1666 | } 1667 | 1668 | it("Test addNewOrder, don't use such test names", () => { 1669 | addNewOrder({asignee: "John@mailer.com",price: 120}); 1670 | });//Triggers 100% code coverage, but it doesn't check anything 1671 | 1672 | ``` 1673 |
    1674 | 1675 | ### :clap: 正例: Stryker 报告,一个编译测试工具,发现并统计没有被测试到的代码(变异) 1676 | 1677 | ![alt text](assets/zh-CN/bp-20-yoni-goldberg-mutation-testing.jpg "Stryker reports, a tool for mutation testing, detects and counts the amount of code that is not tested (Mutations)") 1678 | 1679 |
    1680 | 1681 | 1682 | 1683 |

    1684 | 1685 | ## ⚪ ️4.4 使用 Test linter 防止测试代码问题 1686 | 1687 | :white_check_mark: **建议:** 有一系列 ESLint 插件用于检查测试代码的风格并发现问题。比如 [eslint-plugin-mocha](https://www.npmjs.com/package/eslint-plugin-mocha) 会警告一个写在 global 层的用例(不是 describe() 语句的子级),或者当测试被 [skip](https://mochajs.org/#inclusive-tests) 时会发出警告,这可能会导致你错误地认为所有测试都通过了。类似的,[eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) 可以在一个用例没有任何断言(不覆盖任何内容)时给出警告。 1688 | 1689 |
    1690 | 1691 | 1692 | ❌ **否则:** 当你满足于 90% 的代码覆盖率和 100% 的绿色用例时,发现很多测试啥都没断言,很多测试直接被 skip 掉了。但愿你没有基于这个错误认知做过额外的构建。 1693 | 1694 | 1695 |
    1696 |
    代码示例 1697 | 1698 |
    1699 | 1700 | ### :thumbsdown: 反例: 一个充满错误的测试用例,幸运的是所有都被 linter 捕获了 1701 | 1702 | ```javascript 1703 | describe("Too short description", () => { 1704 | const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead 1705 | it("Some description", () => {});//* error: valid-test-description. Must include the word "Should" + at least 5 words 1706 | }); 1707 | 1708 | it.skip("Test name", () => {// *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite 1709 | expect("somevalue"); // error:no-assert 1710 | }); 1711 | 1712 | it("Test name", () => {*//error:no-identical-title. Assign unique titles to tests 1713 | }); 1714 | ``` 1715 | 1716 |
    1717 | 1718 |

    1719 | 1720 | 1721 | # 第五章:持续集成(CI)以及其他质量度量手段 1722 | 1723 |

    1724 | 1725 | ## ⚪ ️ 5.1 丰富你的 linter 并丢弃有 lint 问题的构建 1726 | 1727 | :white_check_mark: **建议:** 只需五分钟配置,即可免费获取自动保护代码的工具来捕获代码中的显著问题。Lint 不再只是样式工具,现在的 linter 可以捕获很多严重的问题比如 error 没有被正确抛出以及信息丢失。在基础 rule(如 [ESLint standard](https://www.npmjs.com/package/eslint-plugin-standard) 或 [Airbnb style](https://www.npmjs.com/package/eslint-config-airbnb))之上,我们可以考虑加入一些特殊的 linter,例如 [eslint-plugin-chai-expect](https://www.npmjs.com/package/eslint-plugin-chai-expect) 可以发现用例没有写断言,[eslint-plugin-promise](https://www.npmjs.com/package/eslint-plugin-promise?activeTab=readme) 可以发现 promise 没有 resolve,[eslint-plugin-security](https://www.npmjs.com/package/eslint-plugin-security?activeTab=readme) 可以发现可能被 DOS 攻击的正则表达式,以及 [eslint-plugin-you-dont-need-lodash-underscore](https://www.npmjs.com/package/eslint-plugin-you-dont-need-lodash-underscore) 擅长在代码使用 V8 核心方法后给出告警,如 Lodash._map(…)。 1728 | 1729 |
    1730 | 1731 | ❌ **否则:** 在某个下雨天,你的代码一直 crash 而日志没有显示错误堆栈信息。到底发生了什么?你的代码错误地抛了一个非 error 的对象,而堆栈 trace 丢失了,真让人头秃……只需要五分钟配置一个 linter 来发现这个书写错误即可节省你大量的时间。 1732 | 1733 |
    1734 | 1735 |
    代码示例 1736 | 1737 |
    1738 | 1739 | ### :thumbsdown: 反例: 出错的对象被错误地抛出,没有显示这个错误的堆栈信息。好在 ESLint 捕获到了后面的生产错误 1740 | ![alt text](assets/bp-21-yoni-goldberg-eslint.jpeg "The wrong Error object is thrown mistakenly, no stack-trace will appear for this error. Luckily, ESLint catches the next production bug") 1741 | 1742 |
    1743 | 1744 | 1745 | 1746 | 1747 |

    1748 | 1749 | # ⚪ ️ 5.2 通过本地的开发 CI 来缩短反馈循环 1750 | 1751 | :white_check_mark: **建议:** 在本地使用一个包含测试、Lint、稳定性检查等功能的 CI 可以帮助开发者迅速得到反馈并缩短[反馈循环](https://www.gocd.org/2016/03/15/are-you-ready-for-continuous-delivery-part-2-feedback-loops/)。因为一个有效的测试流程包含很多迭代循环 (1) 尝试 -> (2) 反馈 -> (3) 重构。所以反馈越快,开发者可以在每个模块中可以执行的迭代就越多,并且可以得到更好的结果。反过来,如果反馈来得很慢,则一天只能执行很少的迭代,则团队可能会因急需执行下一个主题/任务/模块 而不再提炼当前模块。 1752 | 1753 | 目前已有一些 CI 供应商 (如: [CircleCI load CLI](https://circleci.com/docs/2.0/local-cli/)) 支持在本地执行 CI。一些商业工具如 [wallaby](https://wallabyjs.com/) 为开发原型提供了非常有用的测试能力。或者你可以仅仅在 package.json 中添加 npm 脚本来跑一些质量命令——使用工具如 [concurrently](https://www.npmjs.com/package/concurrently) 来并行执行,并在任何工具失败后抛出非 0 exit code。则开发者只需执行一个命令(如 `npm run quality` )来快速获取反馈。可以用 githook 来取消没有通过质量检查的提交([husky](https://github.com/typicode/husky) 可以帮到你)。 1754 |
    1755 | 1756 | 1757 | ❌ **否则:** 当质量检查结果在提交后第二天才收到反馈,则测试不再是开发的一部分了。 1758 | 1759 | 1760 |
    1761 | 1762 |
    代码示例 1763 | 1764 |
    1765 | 1766 | ### :clap: 正例: 用于执行代码质量检查的 npm 脚本,在主动触发或用户尝试提交新代码时并行执行。 1767 | 1768 | ```javascript 1769 | "scripts": { 1770 | "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"", 1771 | "inspect:lint": "eslint .", 1772 | "inspect:vulnerabilities": "npm audit", 1773 | "inspect:license": "license-checker --failOn GPLv2", 1774 | "inspect:complexity": "plato .", 1775 | 1776 | "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\"" 1777 | }, 1778 | "husky": { 1779 | "hooks": { 1780 | "precommit": "npm run inspect:all", 1781 | "prepush": "npm run inspect:all" 1782 | } 1783 | } 1784 | 1785 | ``` 1786 | 1787 |
    1788 | 1789 | 1790 | 1791 | 1792 |

    1793 | 1794 | # ⚪ ️5.3 在真实的生产环境镜像中执行端到端测试 1795 | 1796 | :white_check_mark: **建议:** 端到端测试是每个 CI 的主要挑战——实时创建一个生产环境镜像并带上所有相关的云服务是很费时费力的。你需要找到最佳的折中:[Docker-compose](https://serverless.com/) 通过一个文本文件将独立的 docker 环境放置到相同的容器中,但是背后的技术(如网络、构建模型)与真实世界有所差别。你可以将其与[‘AWS Local’](https://github.com/localstack/localstack)结合在真实的 AWS 服务中使用。如果你使用了 [serverless](https://serverless.com/) 框架, [AWS SAM](https://docs.aws.amazon.com/lambda/latest/dg/serverless_app.html)允许本地调用 FaaS 代码。 1797 | 1798 | Kubernetes 强大的生态系统还没有形成一个易用的标准工具用于本地和 CI 镜像,虽然经常推出许多新的工具。一种方法是使用像 [Minikube](https://kubernetes.io/docs/setup/minikube/) 和 [MicroK8s](https://microk8s.io/) 这样的工具来运行一个“最小化的 kubernetes”,这些工具更接近实际,但是开销更少。另一种方法是在远程的 “真实 Kubernetes” 上进行测试,一些 CI 提供商(例如 [Codefresh](https://codefresh.io/))与 Kubernetes 环境进行了本地集成,使得在真实环境中运行 CI 管道变得很容易,其他的则允许针对远程 Kubernetes 进行自定义脚本。 1799 | 1800 |
    1801 | 1802 | 1803 | ❌ **否则:** 生产和测试环境使用不同的技术,需要维护两个部署模型,并将开发人员和 ops 团队分隔开来。 1804 | 1805 | 1806 |
    1807 | 1808 |
    代码示例 1809 | 1810 |
    1811 | 1812 | ### :clap: 正例: 动态生成 Kubernetes 集群的 CI 管道 (贡献: Dynamic-environments Kubernetes](https://container-solutions.com/dynamic-environments-kubernetes/)) 1813 | 1814 | ```yaml 1815 | deploy: 1816 | stage: deploy 1817 | image: registry.gitlab.com/gitlab-examples/kubernetes-deploy 1818 | script: 1819 | - ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN 1820 | - kubectl create ns $NAMESPACE 1821 | - kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL" 1822 | - mkdir .generated 1823 | - echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF" 1824 | - sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml" 1825 | - kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml 1826 | - kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml 1827 | environment: 1828 | name: test-for-ci 1829 | ``` 1830 | 1831 |
    1832 | 1833 | 1834 | 1835 | 1836 | 1837 |

    1838 | 1839 | ## ⚪ ️5.4 并行测试工作 1840 | :white_check_mark: **建议:** 只要操作合理,测试是你 7x24 小时的朋友,为你提供非常及时的反馈。实际上,在单个线程上执行 500 个单元测试可能需要很长时间。幸运的是,现代测试运行器和 CI 平台(如 [Jest](https://github.com/facebook/jest), [AVA](https://github.com/avajs/ava) 和 [Mocha extensions](https://github.com/yandex/mocha-parallel-tests))可以将测试并行化为多个进程,以显著缩短反馈时间。 一些CI供应商也支持跨容器并行化测试,这进一步缩短了反馈循环。 无论是在本地多个进程,还是在使用多台机器的某些云 CLI 上 - 并行化需要保证测试用例的独立性,因为每个用例可能在不同的进程上运行。 1841 | 1842 | 1843 | ❌ **否则:** 在推送新代码 1 小时后获得测试结果,而你已经在写下一个 feature 了。 1844 | 1845 | 1846 |
    1847 | 1848 |
    代码示例 1849 | 1850 |
    1851 | 1852 | ### :clap: 正例: Mocha parallel & Jest 轻松地加速了传统的 Mocha,感谢并行测试([贡献:JavaScript测试运行基准](https://medium.com/dailyjs/javascript-test-runners-benchmark-3a78d4117b4)) 1853 | ![alt text](assets/bp-24-yonigoldberg-jest-parallel.png "Mocha parallel & Jest easily outrun the traditional Mocha thanks to testing parallelization (Credit: JavaScript Test-Runners Benchmark)") 1854 | 1855 |
    1856 | 1857 | 1858 | 1859 | 1860 |

    1861 | 1862 | ## ⚪ ️5.5 使用许可证和抄袭检查避免法务问题 1863 | :white_check_mark: **建议:** 许可和抄袭问题可能不是您现在主要关注的问题,但为什么不在10分钟内加上这个能力呢? 许多 npm 包,如 [license check](https://www.npmjs.com/package/license-checker) 和 [plagiarism check](https://www.npmjs.com/package/plagiarism-checker)(商业的,但是有免费选项)可以很容易地加入您的 CI 管道,并检查一些坑:依赖限制性许可证或从Stackoverflow复制粘贴的代码,或者很明显地侵犯了某些版权。 1864 | 1865 | ❌ **否则:** 无意中,开发人员可能会使用包含不适当许可证的软件包或复制粘贴商业代码并遇到法务问题。 1866 | 1867 | 1868 |
    1869 | 1870 |
    代码示例 1871 | 1872 |
    1873 | 1874 | ### :clap: 正例: 1875 | ```javascript 1876 | //install license-checker in your CI environment or also locally 1877 | npm install -g license-checker 1878 | 1879 | //ask it to scan all licenses and fail with exit code other than 0 if it found unauthorized license. The CI system should catch this failure and stop the build 1880 | license-checker --summary --failOn BSD 1881 | 1882 | ``` 1883 | 1884 |
    1885 | 1886 | ![alt text](assets/bp-25-nodejs-licsense.png) 1887 | 1888 | 1889 |
    1890 | 1891 | 1892 | 1893 |

    1894 | 1895 | ## ⚪ ️5.6 持续检查有漏洞的依赖 1896 | :white_check_mark: **建议:** 即使是最知名的依赖(如 Express)也存在已知的漏洞。 这可以通过使用社区工具(如 [npm audit](https://docs.npmjs.com/getting-started/running-a-security-audit))或商业工具(如 [snyk](https://snyk.io/))(也提供免费的社区版本)轻松解决。 可以在每次构建时都可以从 CI 调用他俩。 1897 | 1898 | ❌ **否则:** 在没有专用工具的帮助下保持代码远离漏洞,将需要不断关注有关新威胁的发布信息。 这相当乏味。 1899 | 1900 | 1901 |
    1902 | 1903 |
    代码示例 1904 | 1905 |
    1906 | 1907 | ### :clap: 正例: NPM Audit 结果 1908 | ![alt text](assets/bp-26-npm-audit-snyk.png "NPM Audit result") 1909 | 1910 |
    1911 | 1912 | 1913 | 1914 | 1915 |

    1916 | 1917 | ## ⚪ ️5.7 自动升级依赖 1918 | 1919 | :white_check_mark: **建议:** Yarn 和 npm 最新推出的 package-lock.json 引入了一个严峻的问题(本意是好的,但却通往地狱) - 默认情况下,包将不再得到更新。即使团队使用 'npm install' 和 'npm update' 也不会获得任何更新。这理想情况下会导致依赖了不太好的包版本,或者最坏的情况引入易受攻击的代码。现在,团队依靠开发人员的善意和记忆来手动更新 package.json 或手动使用像 [ncu](https://www.npmjs.com/package/npm-check-updates) 这样的工具。而更靠谱的方式是自动获取最可靠的依赖版本,虽然没有最优解决方案,但有目前两种可能的自动化方式: 1920 | 1921 | (1)CI 可以使 具有过时依赖 的构建失败 - 使用 '[npm outdated](https://docs.npmjs.com/cli/outdated)' 或 'npm-check-updates(ncu)' 等工具。这样做将强制开发人员更新依赖项。 1922 | 1923 | (2)使用商业工具,他们可以扫描代码并自动发送更新依赖的 PR。剩下的一个有趣的问题是依赖更新策略—— 每个补丁的更新都会产生太多的开销,而大版本发布时更新可能会指向一个不稳定的版本(许多软件包在发布后的几天内被爆出漏洞,请[参阅](https://nodesource.com/blog/a-high-level-post-mortem-of-the-eslint-scope-security-incident/) eslint-scope 事件)。 1924 | 1925 | 有效的更新策略可能允许一些“归属期”——让代码滞后 @latest 一段时间和版本,再将本地副本视为过时(例如本地版本为1.3.1且存储库版本为1.3.8)。 1926 | 1927 |
    1928 | 1929 | 1930 | ❌ **否则:** 您的生产环境运行的包已被其作者明确标记为有风险。 1931 | 1932 | 1933 |
    1934 | 1935 |
    代码示例 1936 | 1937 |
    1938 | 1939 | ### :clap: 正例: 可以手动或在 CI 管道中使用 [ncu](https://www.npmjs.com/package/npm-checkupdates) 来检测代码在最新版本之后的滞后程度 1940 | 1941 | ![alt text](assets/bp-27-yoni-goldberg-npm.png "Nncu can be used manually or within a CI pipeline to detect to which extent the code lag behind the latest versions") 1942 | 1943 | 1944 |
    1945 | 1946 | 1947 |

    1948 | 1949 | ## ⚪ ️ 5.8 其他的,与 Node 无关的,CI 小建议 1950 | 1951 | :white_check_mark: **建议:** 本文的重点是多少与 Node JS 有点关系的测试建议。但是,本节整理了一些众所周知的与 Node 无关的技巧: 1952 | 1953 | 1. 使用声明性语法。这是大多数工具的唯一选择,但旧版本的 Jenkins 允许使用代码或 UI。 1954 | 1. 选择具有本地 Docker 支持的工具。 1955 | 1. 尽快失败,先运行最快的测试。设立一个“冒烟测试” 阶段/里程碑,对多个快速检查工具(如 linting,单元测试)进行分组,为代码提交者提供快速反馈。 1956 | 1. 设法方便地浏览构建的所有产出,包括测试报告,覆盖率报告,变异报告,日志等。 1957 | 1. 为每个事件创建多个管道/作业,提取他们的相同工作。例如,为功能分支的提交配置一个作业,为 master PR配置另一个。(大多数工具提供了一些代码重用的机制) 1958 | 1. 永远不要在工作声明中加入机密信息,从机密库或工作的配置中获取它。 1959 | 1. 在发布构建中明确目标版本号 1960 | 1. 仅构建一次并对整个构建执行所有检查(例如Docker镜像) 1961 | 1. 在一个临时的环境中进行测试,这个环境不会在不同构建之间产生状态漂移。缓存 node_modules 可能是惟一的例外。 1962 | 1963 |
    1964 | 1965 | 1966 | ❌ **否则:** 你会错过多年来智慧的结晶 1967 | 1968 |

    1969 | 1970 | ## ⚪ ️ 5.9 构建模型(Matrix):使用多个 Node 版本执行同一个 CI 流程 1971 | 1972 | :white_check_mark: **建议:** 质量检查是用于发现意外,你覆盖的部分越多,你就越可能尽早地发现问题。 在开发包或运行具有各种配置和 Node 版本的多客户生产环境时,CI 必须在所有配置的组合上运行测试管道。 例如,假设我们的某些客户使用 MySQL,另一批客户使用 Postgres。一些 CI 工具支持一种称为“Matrix”的功能,该功能可以针对 MySQL、Postgres 和多个 Node 版本(如8、9、10)的所有组合执行测试。 只要配置即可完成而无需任何额外工作。 其他不支持 Matrix 的 CI 可能可以通过扩展或一定调整来实现这个功能。 1973 |
    1974 | 1975 | 1976 | ❌ **否则:** 在辛辛苦苦写完所有用例编写之后,怎么可以因为配置问题而让漏洞溜进来? 1977 | 1978 | 1979 |
    1980 | 1981 |
    代码示例 1982 | 1983 |
    1984 | 1985 | ### :clap: 正例: 使用 Travis (CI 提供商) 构建配置,在多个 Node 版本上运行相同的测试 1986 | 1987 | ```yaml 1988 | language: node_js 1989 | node_js: 1990 | - "7" 1991 | - "6" 1992 | - "5" 1993 | - "4" 1994 | install: 1995 | - npm install 1996 | script: 1997 | - npm run test 1998 | ``` 1999 | 2000 |
    2001 | 2002 |

    2003 | 2004 | # Team 2005 | 2006 | 2007 | 2008 | ## Yoni Goldberg 2009 | 2010 |
    2011 | 2012 |
    2013 | 2014 | **Role:** 作者 2015 | 2016 | **About:** 我是一名独立顾问,与 500 强企业和创业公司合作,完善他们的 JS 和 Node.js 应用。与其他任何话题相比,我更感兴趣的是掌握测试的艺术。我也是[Node.js 最佳实践](https://github.com/goldbergyoni/nodebestpractices)的作者。 2017 | 2018 |
    2019 | 2020 | **Workshop:** 👨‍🏫 是否想在您自己的办公室中(欧洲和美国)学习所有这些实践和技术? [在此处注册我的测试工作室](https://testjavascript.com/) 2021 |
    2022 | 2023 | **关注:** 2024 | 2025 | * [🐦 Twitter](https://twitter.com/goldbergyoni/) 2026 | * [📞 Contact](https://testjavascript.com/contact-2/) 2027 | * [✉️ Newsletter](https://testjavascript.com/newsletter//) 2028 | 2029 |
    2030 |
    2031 |
    2032 | 2033 | 2034 | ## [Bruno Scheufler](https://github.com/BrunoScheufler) 2035 | 2036 | **角色:** 技术评审人和顾问 2037 | 2038 | 致力于修改、完善、备注及优化所有文字。 2039 | 2040 | **关于我:** 全栈 Web 工程师,Node.js 和 GraphQL 爱好者 2041 |
    2042 |
    2043 | 2044 | ## [Ido Richter](https://github.com/idori) 2045 | 2046 | **Role:** 概念,设计以及提供好的建议 2047 | 2048 | **About:** 优秀的前端开发者,CSS 专家,emoji 怪 2049 | 2050 | ## [Kyle Martin](https://github.com/js-kyle) 2051 | 2052 | **Role:** 帮助保持本项目的运行,并审查与安全性有关的实践 2053 | 2054 | **About:** 喜欢从事 Node.js 项目和 Web 应用安全性的工作。 2055 | -------------------------------------------------------------------------------- /readme-zh-TW.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |
    4 | 5 | # 👇 為什麼本指南可以幫助你將測試能力提升到下一個等級 6 | 7 |
    8 | 9 | ## 📗 46+ 個最佳實踐:非常全面且徹底 10 | 11 | 這是從 A 到 Z 的 JavaScript 及 Node.js 可靠的指南。它為你總結及規劃了市場上大量的部落格文章、書籍及工具。 12 | 13 | ## 🚢 進階:從基礎向前邁進 10,000 英里 14 | 15 | 從基礎往前邁進的旅程,包括:在生產(production)環境中測試、變異測試(mutation testing)、以屬性為基礎(property-based)的測試以及許多策略和專業工具。如果你認真閱讀本指南書,你的測試技能可能會高於平均水準。 16 | 17 | ## 🌐 全端:前端、後端、CI、所有部分 18 | 19 | 首先了解任何應用程式都通用的測試實踐。然後再深入研究你選擇的領域:前端/UI、後端、CI 或者全部。 20 | 21 | 22 |
    23 | 24 | ### 作者 Yoni Goldberg 25 | 26 | - A JavaScript & Node.js consultant 27 | - 📗 [Testing Node.js & JavaScript From A To Z](https://www.testjavascript.com) - My comprehensive online course with more than [10 hours of video](https://www.testjavascript.com), 14 test types and more than 40 best practices 28 | - [Follow me on Twitter](https://twitter.com/goldbergyoni/) 29 | 30 |
    31 | 32 | ### 翻譯 - 以你的語言來閱讀本文 33 | 34 | - 🇹🇼[Traditional Chinese](readme-zh-TW.md) - Courtesy of [Yubin Hsu](https://github.com/yubinTW) 35 | - 🇨🇳[Chinese](readme-zh-CN.md) - Courtesy of [Yves yao](https://github.com/yvesyao) 36 | - 🇰🇷[Korean](readme.kr.md) - Courtesy of [Rain Byun](https://github.com/ragubyun) 37 | - 🇵🇱[Polish](readme-pl.md) - Courtesy of [Michal Biesiada](https://github.com/mbiesiad) 38 | - 🇪🇸[Spanish](readme-es.md) - Courtesy of [Miguel G. Sanguino](https://github.com/sanguino) 39 | - 🇧🇷[Portuguese-BR](readme-pt-br.md) - Courtesy of [Iago Angelim Costa Cavalcante](https://github.com/iagocavalcante) , [Douglas Mariano Valero](https://github.com/DouglasMV) and [koooge](https://github.com/koooge) 40 | - 🇫🇷[French](readme-fr.md) - Courtesy of [Mathilde El Mouktafi](https://github.com/mel-mouk) 41 | - 🇺🇦[Ukrainian](readme-ua.md) - Courtesy of [Serhii Shramko](https://github.com/Shramkoweb) 42 | - Want to translate to your own language? please open an issue 💜 43 | 44 |

    45 | 46 | ## `目錄` 47 | 48 | #### [`第 0 章:黃金原則`](#第-0-章黃金原則-1) 49 | 50 | 一個激發所有人的建議 (1個特殊項目) 51 | 52 | #### [`第 1 章:測試剖析`](#第-1-章測試剖析-1) 53 | 54 | 基礎 - 建立乾淨的測試 (12項) 55 | 56 | #### [`第 2 章:後端`](#第-2-章後端測試) 57 | 58 | 有效率地撰寫後端及微服務的測試 (8項) 59 | 60 | #### [`第 3 章:前端`](#第-3-章前端測試) 61 | 62 | 為網頁 UI (包括組件及E2E) 撰寫測試 (11項) 63 | 64 | #### [`第 4 章:測量測試的有效程度`](#第-4-章測量測試效果) 65 | 66 | 測量測試的品質 (4項) 67 | 68 | #### [`第 5 章:持續整合 (Continuous Integration)`](#第-5-章持續整合-ci-或其他提高品質的手段) 69 | 70 | JavaScript 世界的 CI 指南 (9項) 71 | 72 |

    73 | 74 | # 第 0 章:黃金原則 75 | 76 |
    77 | 78 | ## ⚪️ 0 黃金原則:Design for lean testing 79 | 80 | :white_check_mark: **建議:** 81 | 測試程式與主要生產環境的程式不同,要把他設計的極其簡單、簡短、具體、扁平、使人愉悅的去使用及學習。一段測試程式應該要可以讓人一眼就看懂其目的。 82 | 83 | 我們的思考空間被主要的程式邏輯所占滿,並沒有額外的腦容量去處理複雜的東西。如果把其他複雜的程式塞進我們可憐的大腦,將會使得整個團隊的運作變慢,而這些複雜的程式正是用來解決我們需要測試的問題。這也是許多團隊放棄測試的原因。 84 | 85 | 另一方面,測試是一個友好的助手,一個讓你樂意與他合作、投資小且回報大的助手。科學證明我們有兩套大腦系統:系統 1 用於無須努力的活動,如在空曠的路上開車;系統 2 用於複雜和繁瑣的工作,如計算一道數學式。把你的測試程式設計成如系統 1 一般,當你看著你的測試,要像修改 HTML 文件一樣的簡單,而不是像計算 2 x (17 x 24)。 86 | 87 | 為了達到這個目標,我們可以選擇具有成本效益和高投資報酬率的的技術、工具和測試目標。只測試需要的內容,努力保持他的靈活性,某些時候甚至得捨棄一些測試來換取靈活性和簡潔性。 88 | 89 | ![alt text](/assets/headspace.png "We have no head room for additional complexity") 90 | 91 | 以下大部分的建議衍生自這一原則。 92 | 93 | ### 準備好了嗎? 94 | 95 |

    96 | 97 | # 第 1 章:測試剖析 98 | 99 |
    100 | 101 | ## ⚪ ️ 1.1 每個測試的名稱要包含的三個部分 102 | 103 | :white_check_mark: **建議:** 一份測試報告應該告訴那些不一定熟悉程式的人,目前應用程式的修訂版本是否符合他們的要求,包括:測試人員、DevOps 工程師和兩年後的你。如果測試能包含這三個需求面的描述,就能很好的實現這一點: 104 | 105 | (1) 測試的對象是什麼? 例如,ProductsService.addNewProduct 這個方法。 106 | 107 | (2) 在什麼情況或場景下? 例如,價格沒有傳給該方法。 108 | 109 | (3) 預期的結果是什麼? 例如,新的產品沒有被批准。 110 | 111 |
    112 | 113 | ❌ **否則:** 一個名叫"新增產品"的測試失敗了。這有確切地告訴你到底是什麼地方出問題嗎? 114 | 115 |
    116 | 117 | **👇 Note:** 每個項目都會有一個程式範例,有時候還會搭配圖片。 118 |
    119 | 120 |
    程式範例 121 | 122 |
    123 | 124 | ### :clap: 正例:一個包含這三部分的測試名稱 125 | 126 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Mocha-blue.svg "Using Mocha to illustrate the idea") 127 | 128 | ```javascript 129 | // 1. unit under test 130 | describe('Products Service', function() { 131 | describe('Add new product', function() { 132 | // 2. scenario and 3. expectation 133 | it('When no price is specified, then the product status is pending approval', ()=> { 134 | const newProduct = new ProductService().add(...); 135 | expect(newProduct.status).to.equal('pendingApproval'); 136 | }); 137 | }); 138 | }); 139 | ``` 140 | 141 |
    142 | 143 | ### :clap: 正例:一個包含這三部分的測試名稱 144 | 145 | ![alt text](/assets/bp-1-3-parts.jpeg "A test name that constitutes 3 parts") 146 | 147 |
    148 | 149 |
    150 |
    © Credits & read-more 151 | 1. Roy Osherove - Naming standards for unit tests 152 |
    153 | 154 |

    155 | 156 | ## ⚪ ️ 1.2 以 AAA 模式來建構測試 157 | 158 | :white_check_mark: **建議:** 用三個部分來組織你的測試:Arrange 安排、Act 執行、Assert 斷言 (AAA)。依照這個結構,可以確保讀者不用花費腦力去理解你的測試。 159 | 160 | 第一個 A - Arrange 安排:所有使系統達到測試所要模擬的情境的程式。這可能包含實體化某個待測單元的建構子、新增 DB 的資料、mocking/stubbing 物件和其他準備程式。 161 | 162 | 第二個 A - Act 執行:執行測試單元。通常為一行程式。 163 | 164 | 第三個 A - Assert 斷言:確保得到的值符合期待。通常為一行程式。 165 | 166 |
    167 | 168 | ❌ **否則:** 你不僅需要花很多時間去理解主要程式,而且本應是最簡單的部分 - 測試,也會讓你腦力耗盡。 169 | 170 |
    171 | 172 |
    程式範例 173 | 174 |
    175 | 176 | ### :clap: 正例:以 AAA 模式來建構測試 177 | 178 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg "Examples with Jest") ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg "Examples with Mocha") 179 | 180 | ```javascript 181 | describe("Customer classifier", () => { 182 | test("When customer spent more than 500$, should be classified as premium", () => { 183 | // Arrange 184 | const customerToClassify = { spent: 505, joined: new Date(), id: 1 }; 185 | const DBStub = sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" }); 186 | 187 | // Act 188 | const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); 189 | 190 | // Assert 191 | expect(receivedClassification).toMatch("premium"); 192 | }); 193 | }); 194 | ``` 195 | 196 |
    197 | 198 | ### :thumbsdown: 反例:沒有分隔、一大坨、難以理解 199 | 200 | ```javascript 201 | test("Should be classified as premium", () => { 202 | const customerToClassify = { spent: 505, joined: new Date(), id: 1 }; 203 | const DBStub = sinon.stub(dataAccess, "getCustomer").reply({ id: 1, classification: "regular" }); 204 | const receivedClassification = customerClassifier.classifyCustomer(customerToClassify); 205 | expect(receivedClassification).toMatch("premium"); 206 | }); 207 | ``` 208 | 209 |
    210 | 211 |

    212 | 213 | ## ⚪ ️1.3 用產品語言來描述預期:使用 BDD 風格的斷言 214 | 215 | :white_check_mark: **建議:** 使用聲明的方式撰寫測試,可以使讀者無腦的 get 到重點。如果你的程式使用各種條件邏輯包起來,會增加讀者的理解難度。因此,我們應該盡量使用類似人類語言的描述與言如 ```expect``` 或 ```should``` 而不是自己寫程式。如果 Chai 或 Jest 沒有你想要用的斷言,且這個斷言可以被頻繁的重複利用的話,可以考慮 [擴充 Jest 的匹配器 (Jest)](https://jestjs.io/docs/en/expect#expectextendmatchers) 或是寫一個 [客製化的 Chai 插件](https://www.chaijs.com/guide/plugins/)。 216 | 217 |
    218 | 219 | ❌ **否則:** 團隊的測試會越寫越少,且會用 .skip() 把討厭的測試略過。 220 | 221 |
    222 | 223 |
    程式範例
    224 | 225 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg "Examples with Mocha & Chai") ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg "Examples with Jest") 226 | 227 | ### :thumbsdown: 反例:讀者必須快速的看完冗長且複雜的程式碼,才能理解該測試的目的 228 | 229 | ```javascript 230 | test("When asking for an admin, ensure only ordered admins in results", () => { 231 | // assuming we've added here two admins "admin1", "admin2" and "user1" 232 | const allAdmins = getUsers({ adminOnly: true }); 233 | 234 | let admin1Found, 235 | adming2Found = false; 236 | 237 | allAdmins.forEach(aSingleUser => { 238 | if (aSingleUser === "user1") { 239 | assert.notEqual(aSingleUser, "user1", "A user was found and not admin"); 240 | } 241 | if (aSingleUser === "admin1") { 242 | admin1Found = true; 243 | } 244 | if (aSingleUser === "admin2") { 245 | admin2Found = true; 246 | } 247 | }); 248 | 249 | if (!admin1Found || !admin2Found) { 250 | throw new Error("Not all admins were returned"); 251 | } 252 | }); 253 | ``` 254 | 255 |
    256 | 257 | ### :clap: 正例:快速瀏覽以下的聲明式測試非常輕鬆 258 | 259 | ```javascript 260 | it("When asking for an admin, ensure only ordered admins in results", () => { 261 | // assuming we've added here two admins 262 | const allAdmins = getUsers({ adminOnly: true }); 263 | 264 | expect(allAdmins) 265 | .to.include.ordered.members(["admin1", "admin2"]) 266 | .but.not.include.ordered.members(["user1"]); 267 | }); 268 | ``` 269 | 270 |
    271 | 272 |

    273 | 274 | ## ⚪ ️ 1.4 堅持黑箱測試:只測試公開方法 275 | 276 | :white_check_mark: **建議:** 測試內部邏輯是無意義且浪費時間的。如果你的程式/API 回傳了正確的結果,你真的需要花三個小時的時間去測試它內部究竟如何實現的,並且在之後維護這一堆脆弱的測試嗎?每當測試一個公開方法時,其私有方法的實作也會被隱性地測試,只有當存在某個問題(例如錯誤的輸出)時測試才會中斷。這種方法也稱為 ```行為測試```。另一方面,如果你測試內部方法 (白箱方法) — 你的關注點將從組件的輸出結果轉移到具體的實作細節上,如果某天內部邏輯改變了,即使結果依然正確,你也要花精力去維護之前的測試邏輯,這無形中增加了維護成本。 277 |
    278 | 279 | ❌ **否則:** 你的測試會像[狼來了](https://en.wikipedia.org/wiki/The_Boy_Who_Cried_Wolf)一樣,總是叫喚著出問題了 (例如一個因為內部變數名稱改變而導致的測試失敗)。不出所料,人們很快就會開始忽視 CI 的通知,直到某天,一個真正的 bug 被忽視... 280 | 281 |
    282 |
    程式範例 283 | 284 |
    285 | 286 | ### :thumbsdown: 反例:一個無腦測試內部方法的測試 287 | 288 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg "Examples with Mocha & Chai") 289 | 290 | ```javascript 291 | class ProductService { 292 | // this method is only used internally 293 | // Change this name will make the tests fail 294 | calculateVATAdd(priceWithoutVAT) { 295 | return { finalPrice: priceWithoutVAT * 1.2 }; 296 | // Change the result format or key name above will make the tests fail 297 | } 298 | // public method 299 | getPrice(productId) { 300 | const desiredProduct = DB.getProduct(productId); 301 | finalPrice = this.calculateVATAdd(desiredProduct.price).finalPrice; 302 | return finalPrice; 303 | } 304 | } 305 | 306 | it("White-box test: When the internal methods get 0 vat, it return 0 response", async () => { 307 | // There's no requirement to allow users to calculate the VAT, only show the final price. Nevertheless we falsely insist here to test the class internals 308 | expect(new ProductService().calculateVATAdd(0).finalPrice).to.equal(0); 309 | }); 310 | ``` 311 | 312 |
    313 | 314 |

    315 | 316 | ## ⚪ ️ ️1.5 使用正確的測試替身 (Test Double):避免總是使用 stub 和 spy 317 | 318 | :white_check_mark: **建議:** 測試替身是把雙刃劍,他們在提供巨大價值的同時,耦合了應用的內部邏輯 ([一篇關於測試替身的文章: mocks vs stubs vs spies](https://martinfowler.com/articles/mocksArentStubs.html)) 在使用測試替身前,問自己一個很簡單的問題:我是用它來測試需求文件中定義的可見的功能或者可能可見的功能嗎?如果不是,那就可能是白盒測試了。 319 | 320 | 舉例來說,如果你想測試你的應用程式在支付服務當機時的預期行為,你可以 stub 支付服務並觸發一些"沒有回應"的回傳行為,以確保被測試的單元回傳正確的值。這可以測試特定場景下應用程式的行為、回應及輸出結果。你也可以使用一個 spy 來斷言當服務當機時是否有發送電子郵件 - 這又是一個針對可能出現在需求文件中行為的檢查 ("如果無法儲存付款資訊,發送電子郵件")。反過來說,如果你 mock 的支付服務,能確保它被正確呼叫並傳入正確的 JavaScript 型別,那麼你的測試重點是內部的邏輯,它與應用程式的功能關係不大,而且可能會經常變化。 321 | 322 |
    323 | 324 | ❌ **否則:** 任何程式的重構都會需要程式中所有的 mock 進行相對應的更新。測試變成了一種負擔,而不是一個助力。 325 | 326 |
    327 | 328 |
    程式範例 329 | 330 |
    331 | 332 | ### :thumbsdown: 反例:關注內部實作的 mock 333 | 334 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Sinon-blue.svg "Examples with Sinon") 335 | 336 | ```javascript 337 | it("When a valid product is about to be deleted, ensure data access DAL was called once, with the right product and right config", async () => { 338 | // Assume we already added a product 339 | const dataAccessMock = sinon.mock(DAL); 340 | // hmmm BAD: testing the internals is actually our main goal here, not just a side-effect 341 | dataAccessMock 342 | .expects("deleteProduct") 343 | .once() 344 | .withArgs(DBConfig, theProductWeJustAdded, true, false); 345 | new ProductService().deletePrice(theProductWeJustAdded); 346 | dataAccessMock.verify(); 347 | }); 348 | ``` 349 | 350 |
    351 | 352 | ### :clap: 正例:Spy 專注於測試需求,但身為一個 side effect,無可避免地會接觸到內部程式結構 353 | 354 | ```javascript 355 | it("When a valid product is about to be deleted, ensure an email is sent", async () => { 356 | // Assume we already added here a product 357 | const spy = sinon.spy(Emailer.prototype, "sendEmail"); 358 | new ProductService().deletePrice(theProductWeJustAdded); 359 | // hmmm OK: we deal with internals? Yes, but as a side effect of testing the requirements (sending an email) 360 | expect(spy.calledOnce).to.be.true; 361 | }); 362 | ``` 363 | 364 |
    365 | 366 |

    367 | 368 | ## 📗 想要透過影片來學習這些做法嗎? 369 | 370 | ### 歡迎來我的線上課程網站 [Testing Node.js & JavaScript From A To Z](https://www.testjavascript.com) 371 | 372 |

    373 | 374 | ## ⚪ ️1.6 不要 "foo", 使用真實的資料 375 | 376 | :white_check_mark: **建議:** 生產環境中的 bug 通常是在一些特殊或者意外的輸入下出現的 — 所以測試的輸入資料越真實,越容易在早期抓住問題。使用現有的一些函式庫(比如 [Faker](https://www.npmjs.com/package/faker))去造"假"真實數據來模擬生產環境數據的多樣性和形式。比如,這些函示庫可以產生真實的電話號碼、用戶名稱、信用卡、公司名稱等等。你還可以創建一些測試(在單元測試之上,而不是替代)生產隨機 fakers 數據來擴充你的測試單元,甚至從生產環境中導入真實的資料。如果想要更進階的話,請看下一個項目:基於屬性的測試 (property-based testing)。 377 |
    378 | 379 | ❌ **否則:** 你要部屬的程式都在 "foo" 之類的輸入值中正確的通過測試,結果上線之後收到像是 ```@3e2ddsf . ##’ 1 fdsfds . fds432 AAAA``` 之類的輸入值後掛掉了。 380 | 381 |
    382 | 383 |
    程式範例 384 | 385 |
    386 | 387 | ### :thumbsdown: 反例: 一個測試案例使用非真實資料去通過測試 388 | 389 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg "Examples with Jest") 390 | 391 | ```javascript 392 | const addProduct = (name, price) => { 393 | const productNameRegexNoSpace = /^\S*$/; // no white-space allowed 394 | 395 | if (!productNameRegexNoSpace.test(name)) return false; // this path never reached due to dull input 396 | 397 | // some logic here 398 | return true; 399 | }; 400 | 401 | test("Wrong: When adding new product with valid properties, get successful confirmation", async () => { 402 | // The string "Foo" which is used in all tests never triggers a false result 403 | const addProductResult = addProduct("Foo", 5); 404 | expect(addProductResult).toBe(true); 405 | // Positive-false: the operation succeeded because we never tried with long 406 | // product name including spaces 407 | }); 408 | ``` 409 | 410 |
    411 | 412 | ### :clap:正例:使用隨機產生的真實資料來輸入 413 | 414 | ```javascript 415 | it("Better: When adding new valid product, get successful confirmation", async () => { 416 | const addProductResult = addProduct(faker.commerce.productName(), faker.random.number()); 417 | // Generated random input: {'Sleek Cotton Computer', 85481} 418 | expect(addProductResult).to.be.true; 419 | // Test failed, the random input triggered some path we never planned for. 420 | // We discovered a bug early! 421 | }); 422 | ``` 423 | 424 |
    425 | 426 |

    427 | 428 | ## ⚪ ️ 1.7 Property-based testing 基於屬性的測試:測試輸入的多種組合 429 | 430 | :white_check_mark: **建議:** 通常我們只會選擇少部分的輸入樣本去做測試。 即使是使用了上一項提到的工具去模擬真實數據,我們也只覆蓋到了一部分輸入的組合 (```method('', true, 1)```, ```method('string', false , 0)```)。然而在生產環境中,一個擁有 5 個參數的 API,可能會遇到上千種排列組合的輸入,而其中的某一種可能會把你的程式搞掛(可參考 [Fuzz Testing](https://en.wikipedia.org/wiki/Fuzzing))。 431 | 432 | 如何撰寫一個測試,可以自動發送 1000 種不同輸入的排列組合,並捕捉到使我們的程式不能正確回傳的輸入?基於屬性的測試 (Property-based testing) 就是這樣一種技術:透過發送所有可能的輸入組合到你的測試單元中,它增加了發現 bug 的可能性。 433 | 434 | 例如,給定一個方法 — ```addNewProduct(id, name, isDiscount)``` — 函示庫將使用許多 ```(number, string, boolean)``` 的組合來呼叫這個方法,比如 ```(1, 'iPhone', false)```,```(2, 'Galaxy', true)```。您可以使用您喜歡的測試運行器(Mocha、Jest等),使用 [js-verify](https://github.com/jsverify/jsverify) 或者 [testcheck](https://github.com/leebyron/testcheck-js) (文件寫得比較好) 來執行基於屬性的測試。 435 | 436 | 更新:Nicolas Dubien 在下面的回復中建議使用 [fast-check](https://github.com/dubzzz/fast-check#readme),它似乎提供了更多的功能,且有被積極維護。 437 | 438 |
    439 | 440 | ❌ **否則:** 你無意中選擇的測試輸入只涵蓋到運作正常的程式片段。不幸的是,他沒有發現真正的錯誤,這也降低了把測試當作發現錯誤的工具的成效。 441 | 442 |
    443 | 444 |
    程式範例 445 | 446 |
    447 | 448 | ### :clap: 正例: 使用 fast-check 來測試許多的輸入組合 449 | 450 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg "Examples with Jest") 451 | 452 | ```javascript 453 | import fc from "fast-check"; 454 | 455 | describe("Product service", () => { 456 | describe("Adding new", () => { 457 | // this will run 100 times with different random properties 458 | it("Add new product with random yet valid properties, always successful", () => 459 | fc.assert( 460 | fc.property(fc.integer(), fc.string(), (id, name) => { 461 | expect(addNewProduct(id, name).status).toEqual("approved"); 462 | }) 463 | )); 464 | }); 465 | }); 466 | ``` 467 | 468 |
    469 | 470 |

    471 | 472 | ## ⚪ ️ 1.8 如果需要,只使用簡短的行內快照 (inline snapshots) 473 | 474 | :white_check_mark: **建議:** 如果你需要進行 快照測試 ([snapshot testing](https://jestjs.io/docs/en/snapshot-testing)),只使用短而集中的快照 (如3~7行),該快照是測試程式的一部份,而不是在外部文件中。保持好這一原則,將會確保你的測試的自我解釋性且不會那麼脆弱。 475 | 476 | 另一方面,"classic snapshots"的教學和工具鼓勵將大文件 (如組件的渲染結果、API 的 JSON 結果) 存儲在一些外部媒介上,並確保每次測試運行時,將收到的結果與保存的版本進行比較。舉個例子,這將會隱性地將我們的測試與包含3000個數值的1000行內容耦合在一起,而測試者從未閱讀和推理過這些數據。為什麼這樣是不對的? 這樣做,將會有1000個原因讓你的測試失敗 - 只要有一行改變,快照比對就會 fail,而這可能會經常發生。多頻繁?當有每一個空格、註解或一點 CSS/HTML 的變化。不僅如此,測試名稱也不會提供關於失敗的線索,因為它只是檢查這1000行是否有變化,而且它還鼓勵測試者去接受一個他無法檢查和驗證的大文件作為期望的結果。所有這些都是測試目標不明確、測試目標過多的症狀。 477 | 478 | 值得注意的是,在少數情況下,大型的外部快照是可以接受的 - 當斷言的對象是 schema 而不是所有內容時 (提取出要的值並專注在某個欄位上),或者當收到的文件內容幾乎不會改變時。 479 | 480 |
    481 | 482 | ❌ **否則:** 一個 UI 的測試失敗了。程式看起來是對的,畫面上也完美渲染了每個像素,但怎麼了? 你的測試程式發現收到的內容與期望的不同,或許只是多了一個空格... 483 | 484 |
    485 | 486 |
    程式範例 487 | 488 |
    489 | 490 | ### :thumbsdown: 反例: 將看不到的 2000 行程式耦合進我們的測試案例中 491 | 492 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg "Examples with Jest") 493 | 494 | ```javascript 495 | it("TestJavaScript.com is renderd correctly", () => { 496 | // Arrange 497 | 498 | // Act 499 | const receivedPage = renderer 500 | .create( Test JavaScript ) 501 | .toJSON(); 502 | 503 | // Assert 504 | expect(receivedPage).toMatchSnapshot(); 505 | // We now implicitly maintain a 2000 lines long document 506 | // every additional line break or comment - will break this test 507 | }); 508 | ``` 509 | 510 |
    511 | 512 | ### :clap: 正例:期望是可見且集中的 513 | 514 | ```javascript 515 | it("When visiting TestJavaScript.com home page, a menu is displayed", () => { 516 | // Arrange 517 | 518 | // Act 519 | const receivedPage = renderer 520 | .create( Test JavaScript ) 521 | .toJSON(); 522 | 523 | // Assert 524 | 525 | const menu = receivedPage.content.menu; 526 | expect(menu).toMatchInlineSnapshot(` 527 | 532 | `); 533 | }); 534 | ``` 535 | 536 |
    537 | 538 |

    539 | 540 | ## ⚪ ️1.9 避免使用全域的 test fixtures 或 seeds,而是放進每個測試中 541 | 542 | :white_check_mark: **建議:** 參照黃金原則,每個測試需要在它自己的 DB 中進行操作避免互相污染。但現實中,這條規則經常被打破:為了性能的提升而在執行測試前初始化全域資料庫 (也被稱為"[test fixture](https://en.wikipedia.org/wiki/Test_fixture)")。儘管性能很重要,但是它可以通過後面講的「組件測試」來做取捨。為了減輕複雜度,我們可以在每個測試中只初始化自己需要的數據。除非性能問題真的非常嚴重,那還是可以做一定程度的妥協 - 僅在全域放不會改變的數據 (比如 query)。 543 | 544 |
    545 | 546 | ❌ **否則:** 有一些測試 fail 了,團隊花了許多時間後發現,只是因為兩個測試同時改變了同一個 seed。 547 | 548 |
    549 | 550 |
    程式範例 551 | 552 |
    553 | 554 | ### :thumbsdown: 反例:測試案例之間不是獨立的。而是相依於全域的 DB 資料 555 | 556 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg "Examples with Mocha") 557 | 558 | ```javascript 559 | before(async () => { 560 | // adding sites and admins data to our DB. Where is the data? outside. At some external json or migration framework 561 | await DB.AddSeedDataFromJson('seed.json'); 562 | }); 563 | it("When updating site name, get successful confirmation", async () => { 564 | // I know that site name "portal" exists - I saw it in the seed files 565 | const siteToUpdate = await SiteService.getSiteByName("Portal"); 566 | const updateNameResult = await SiteService.changeName(siteToUpdate, "newName"); 567 | expect(updateNameResult).to.be(true); 568 | }); 569 | it("When querying by site name, get the right site", async () => { 570 | // I know that site name "portal" exists - I saw it in the seed files 571 | const siteToCheck = await SiteService.getSiteByName("Portal"); 572 | expect(siteToCheck.name).to.be.equal("Portal"); // Failure! The previous test change the name :[ 573 | }); 574 | 575 | ``` 576 | 577 |
    578 | 579 | ### :clap: 正例:每個測試案例只操作他自己的資料 580 | 581 | ```javascript 582 | it("When updating site name, get successful confirmation", async () => { 583 | // test is adding a fresh new records and acting on the records only 584 | const siteUnderTest = await SiteService.addSite({ 585 | name: "siteForUpdateTest" 586 | }); 587 | 588 | const updateNameResult = await SiteService.changeName(siteUnderTest, "newName"); 589 | 590 | expect(updateNameResult).to.be(true); 591 | }); 592 | ``` 593 | 594 |
    595 | 596 |
    597 | 598 | ## ⚪ ️ 1.10 不要 catch 錯誤,expect 他們 599 | 600 | :white_check_mark: **建議:** 當你要測試一些輸入是否有觸發錯誤時,使用 ```try-catch-finally``` 來檢查他是否會進入到 catch 區塊,看起來沒什麼問題。但會變成一個笨拙且冗長的測試案例 (如下面程式範例),他會隱藏簡單的測試意圖和預期的結果。 601 | 602 | 一個更為優雅的作法是使用專用的單行斷言:如 Chai 中的 ```expect(method).to.throw``` 或是 Jest 中的 ```expect(method).toThrow()```。必須要確保這個 expection 包含某個預期的 error type,如果只得到一個通用的錯誤型態,那應用程式將無法表明更多訊息給使用者。 603 | 604 |
    605 | 606 | ❌ **否則:** 從測試報告 (如 CI 報告) 中要看出哪裡有錯會非常困難。 607 | 608 |
    609 | 610 |
    程式範例 611 | 612 |
    613 | 614 | ### :thumbsdown: 反例:一個很長的測試案例,嘗試使用 ```try-catch``` 來斷言錯誤 615 | 616 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg "Examples with Mocha") 617 | 618 | ```javascript 619 | it("When no product name, it throws error 400", async () => { 620 | let errorWeExceptFor = null; 621 | try { 622 | const result = await addNewProduct({}); 623 | } catch (error) { 624 | expect(error.code).to.equal("InvalidInput"); 625 | errorWeExceptFor = error; 626 | } 627 | expect(errorWeExceptFor).not.to.be.null; 628 | // if this assertion fails, the tests results/reports will only show 629 | // that some value is null, there won't be a word about a missing Exception 630 | }); 631 | ``` 632 | 633 |
    634 | 635 | ### :clap: 正例:一個容易閱讀及被了解的 expection,甚至能被 QA 或 PM 理解 636 | 637 | ```javascript 638 | it("When no product name, it throws error 400", async () => { 639 | await expect(addNewProduct({})) 640 | .to.eventually.throw(AppError) 641 | .with.property("code", "InvalidInput"); 642 | }); 643 | ``` 644 | 645 |
    646 | 647 |

    648 | 649 | ## ⚪ ️ 1.11 為測試案例打上標籤 650 | 651 | :white_check_mark: **建議:** 不同的測試需要在不同的情境下執行:快速冒煙測試、無 IO 的測試、開發者儲存或提交檔案的測試、送出一個 PR 後的 end-to-end 測試等等。 可以用一些 ```#cold``` ```#api``` ```#sanity``` 之類的標籤來標註這些測試,這樣就可以在測試時只執行特定的子集合。例如在 Mocha 中可以這樣來執行:```mocha -- grep 'sanity'```。 652 |
    653 | 654 | ❌ **否則:** 執行所有測試案例,包括執行大量查詢 DB 的測試,開發者做的任何微小的變更都需要花很長的時間去跑完所有的測試,將會導致開發者不想再執行測試。 655 | 656 |
    657 | 658 |
    程式範例: 659 | 660 |
    661 | 662 | ### :clap: 正例:將測試案例標記為 '#cold-test' 讓執行測試的人可以只執行速度快的測試案例 (cold 指的是沒有 IO 的快速測試,甚至可以在開發人員打字時頻繁地執行) 663 | 664 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg "Examples with Jest") 665 | 666 | ```javascript 667 | // this test is fast (no DB) and we're tagging it correspondigly 668 | // now the user/CI can run it frequently 669 | describe("Order service", function() { 670 | describe("Add new order #cold-test #sanity", function() { 671 | test("Scenario - no currency was supplied. Expectation - Use the default currency #sanity", function() { 672 | // code logic here 673 | }); 674 | }); 675 | }); 676 | ``` 677 | 678 |
    679 | 680 |

    681 | 682 | ## ⚪ ️ 1.12 把測試案例進行至少兩個層次的分類 683 | 684 | :white_check_mark: **建議:** 對測試案例套用一些結構,讓每個看到這個測試案例的人都可以很容易得理解需求 (測試是最好的文件) 和正在測試的各種情境。一個常見的方法是在測試上方寫至少兩個用來"描述"的區塊:第一個是測試單元的名稱,第二個是額外的分類名稱,如情境或自定義的類別 (參考下面的程式範例和畫面輸出)。這樣的做法也會大幅的改善測試報告的呈現。讀者將會很容易的推斷出測試的類別,讀懂該測試的內容並與失敗的測試關聯起來。此外,對開發者來說,瀏覽這一連串的測試也變得更加容易。有許多額外的結構也是可以考慮使用的,像是 [given-when-then](https://github.com/searls/jasmine-given) 或 [RITE](https://github.com/ericelliott/riteway)。 685 | 686 |
    687 | 688 | ❌ **否則:** 當看到一份毫無結構且數量眾多的測試報告時,讀者只能透過粗略地閱讀整份報告來總結,並將失敗的錯誤案例關聯起來。思考一個情況,當100個測試案例中有7個失敗時,看一個分層結構良好的測試報告與看一個扁平的測試結果清單相比,那些錯誤的測試案例很有可能都在同一個流程或分類底下,讀者將可以很快的推斷出錯誤的地方或看出哪部分是他們失敗的原因。 689 | 690 |
    691 | 692 |
    程式範例 693 | 694 |
    695 | 696 | ### :clap: 正例:利用測試案例的名稱和情境來組織,可以產生良好的測試報告,如下所示 697 | 698 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg "Examples with Jest") 699 | 700 | ```javascript 701 | // Unit under test 702 | describe("Transfer service", () => { 703 | // Scenario 704 | describe("When no credit", () => { 705 | // Expectation 706 | test("Then the response status should decline", () => {}); 707 | 708 | // Expectation 709 | test("Then it should send email to admin", () => {}); 710 | }); 711 | }); 712 | ``` 713 | 714 | ![alt text](assets/hierarchical-report.png) 715 | 716 |
    717 | 718 | ### :thumbsdown: 反例:扁平的測試列表會使讀者很難去看懂 user story 和失敗的測試之間的關係 719 | 720 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg "Examples with Mocha") 721 | 722 | ```javascript 723 | test("Then the response status should decline", () => {}); 724 | 725 | test("Then it should send email", () => {}); 726 | 727 | test("Then there should not be a new transfer record", () => {}); 728 | ``` 729 | 730 | ![alt text](assets/flat-report.png) 731 | 732 |
    733 | 734 |
    735 | 736 |

    737 | 738 | ## ⚪ ️1.13 其他通用且良好的測試習慣 739 | 740 | :white_check_mark: **建議:** 本篇文章的重點是與 NodeJS 相關的測試建議或至少可以用 NodeJS 來舉例說明的內容。然而,這裡有幾個與 NodeJS 無關的建議,且是眾所皆知的。 741 | 742 | 學習並實現 [TDD原則](https://www.sm-cloud.com/book-review-test-driven-development-by-example-a-tldr/) - 他對許多人來說非常有價值,但如果他不適合你的風格,不要被嚇到,不是只有你這樣。試著在寫程式之前使用 [red-green-refactor](https://blog.cleancoder.com/uncle-bob/2014/12/17/TheCyclesOfTDD.html) 的風格來撰寫測試,並確保每個測試案例只檢查一個測試目標。當你發現一個 bug 時,在修復它之前先新增一個可以檢測到它的測試案例,讓每個測試案例在變綠之前至少失敗一次,接著快速撰寫簡單的程式讓這個測試通過 - 然後逐步重構這些程式到可以上 production 的水準,避免對環境 (如路徑或作業系統等) 有任何相依性。 743 | 744 |
    745 | 746 | ❌ **否則:** 你會錯過這數十年來的智慧結晶 747 | 748 |

    749 | 750 | # 第 2 章:後端測試 751 | 752 | ## ⚪ ️2.1 豐富您的測試組合:不局限於單元測試和測試金字塔 753 | 754 | :white_check_mark: **建議:** 雖然 [測試金字塔](https://martinfowler.com/bliki/TestPyramid.html) 已經有超過十年的歷史了,但他仍然是個很好的模型,他提出了三種測試類型,並影響了大多數開發者的測試策略。與此同時,大量閃亮的新測試技術出現了,並隱藏在測試金字塔的陰影下。考慮到近十年來我們所看到的所有巨變 (Microservices, cloud, serverless),這個非常老的模型是否仍能適用於所有類型的應用?測試界不應該考慮新的測試技術嗎? 755 | 756 | 不要誤會,在 2019 年,測試金字塔、TDD、單元測試仍然是強大的技術,且對於大多數應用仍是最佳選擇。但是像其他模型一樣,儘管它有用,但是一定[會在某些時候出問題](https://en.wikipedia.org/wiki/All_models_are_wrong)。例如,我們有一個 IoT 的應用程式,將許多事件傳入一個 Kafka/RabbitMQ 這樣的 message-bus 中,然後這些事件流入資料庫並被經由 UI 來做查詢。我們真的需要花費 50% 的測試預算去為這個幾乎沒有邏輯的中心化的整合應用程式寫單元測試嗎?隨著應用類型 (bots, crypto, Alexa-skills) 的多樣增長,測試金字塔可能將不再是某些場景的最佳選擇了。 757 | 758 | 是時候豐富你的測試組合並了解更多的測試類型了(下一節會給你一些小建議),這些類似於測試金字塔的思維模型與你所面臨的現實問題會更加匹配("嘿,我們的 API 掛了,我們來寫 consumer-driven contract testing 吧!")。讓您的測試多樣化,比如建立基於風險分析的檢查模型 — 評估可能出現問題的地方,並提供一些預防措施以減輕這些潛在風險。 759 | 760 | 需要注意的是:軟體世界中的 TDD 模型面臨兩個極端的態度,一些人鼓吹到處使用它,另一些人則認為它是魔鬼。每個說絕對的人都是錯的 :] 761 | 762 |
    763 | 764 | ❌ **否則:** 你將錯過一些超高 CP 值的工具,比如 Fuzz、lint、mutation,這些工具只需 10 分鐘設定就能為你提供許多好處。 765 | 766 |
    767 | 768 |
    程式範例 769 | 770 |
    771 | 772 | ### :clap: 正例:Cindy Sridharan 在她的文章 "Testing Microservices — the sane way" 中提出了一個豐富的測試組合 773 | 774 | ![alt text](assets/bp-12-rich-testing.jpeg "Cindy Sridharan suggests a rich testing portfolio in her amazing post ‘Testing Microservices — the sane way’") 775 | 776 | ☺️Example: [YouTube: “Beyond Unit Tests: 5 Shiny Node.JS Test Types (2018)” (Yoni Goldberg)](https://www.youtube.com/watch?v=-2zP494wdUY&feature=youtu.be) 777 | 778 |
    779 | 780 | ![alt text](assets/bp-12-Yoni-Goldberg-Testing.jpeg "A test name that constitutes 3 parts") 781 | 782 |
    783 | 784 |

    785 | 786 | ## ⚪ ️2.2 組件化測試可能是最有效的利器 787 | 788 | :white_check_mark: **建議:** 應用程式中的每個單元測試僅能覆蓋整個程式的一小部分,要覆蓋全部會非常麻煩,而端到端測試可以很輕鬆地覆蓋大量區域,但是比較脆弱而且很慢。何不找一個平衡點:寫一些比單元測試大,但是比端到端測試小的測試。組件測試是測試世界的一顆遺珠 — 它找到了兩個模式的最佳平衡點:不錯的性能和使用 TDD 模式的可能性與真實且強大的覆蓋率。 789 | 790 | 組件測試關注於微服務"單元",他們針對 API 來做事,不 mock 任何屬於微服務本身的東西(像是真實的 DB,甚至是該 DB 的 in-memory 版本)但是 stub 所有外部的東西,像是呼叫其他的微服務。藉由這種方式,我們可以測試我們部署的部分,由外而內地覆蓋應用程式,可以節省大量時間並獲得信心。 791 | 792 |
    793 | 794 | ❌ **否則:** 你可能花了好幾天來寫單元測試,卻發現只得到了 20% 的覆蓋率。 795 | 796 |
    797 | 798 |
    程式範例 799 | 800 |
    801 | 802 | ### :clap: 正例:使用 Supertest 來測試 Express API (快速且覆蓋多個層次) 803 | 804 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Mocha-blue.svg "Examples with Mocha") 805 | 806 | ![alt text](assets/bp-13-component-test-yoni-goldberg.png " [Supertest](https://www.npmjs.com/package/supertest) allows approaching Express API in-process (fast and cover many layers)") 807 | 808 |
    809 | 810 |

    811 | 812 | ## ⚪ ️2.3 利用 contract tests 來確保新的 release 不會破壞 API 的使用 813 | 814 | :white_check_mark: **建議:** 你的微服務有許多客戶,而你為了兼容性而運行著很多種版本 (keeping everyone happy)。當你改了某些程式後 "砰!",某些使用該服務的重要客戶生氣了。伺服端要滿足所有客戶的期望是非常困難的 - 另一方面,客戶端無法執行任何測試,因為 release 的日期是伺服端決定的。 815 | 816 | [Consumer-driven contracts and the framework PACT](https://docs.pact.io/) 誕生了,它以一種破壞性的方式規範了這一流程 — 不再由伺服端定義測試計劃,而是客戶端決定伺服端的測試! PACT 可以記錄客戶端的期望並存放在一個共享的位置 — 中間人(Broker),伺服端可以 pull 下這些期望並利用 PACT 的函示庫在所有版本中檢測是否有被破壞的契約,也就是客戶端的期望沒有被滿足。通過這種方式,所有 伺服端-用戶端 沒對好的 API 將會在 build/CI 階段被發現,從而減少你的煩惱。 817 |
    818 | 819 | ❌ **否則:** 所有的變更都將會造成繁瑣的人工測試,導致開發者害怕部屬 820 | 821 |
    822 | 823 |
    程式範例 824 | 825 |
    826 | 827 | ### :clap: 正例: 828 | 829 | ![](https://img.shields.io/badge/🔧%20Example%20using%20PACT-blue.svg "Examples with PACT") 830 | 831 | ![alt text](assets/bp-14-testing-best-practices-contract-flow.png) 832 | 833 |
    834 | 835 |

    836 | 837 | ## ⚪ ️2.4 單獨測試你的 middlewares 838 | 839 | :white_check_mark: **建議:** 許多人拒絕測試 middleware,因為它們只佔系統的一小部分而且相依於真實的 Express server。這兩個原因都不正確 — middleware 雖然小,但是影響著所有或至少大部分請求,而且可以被簡單地作為純函數測試 (參數為 ```{req,res}``` 的 JavaScript 物件)。要測試 middleware 函數,只需要呼叫它,並且監看 ([如使用 Sinon](https://www.npmjs.com/package/sinon)) 與 ```{req,res}``` 的互動來確保函數有執行正確的行為。 [node-mock-http](https://www.npmjs.com/package/node-mocks-http) 函示庫則更進一步:它還監聽了 ```{req,res}``` 物件的行為。例如,它可以斷言 res 物件上的 http 狀態是否符合預期。(看下面的程式範例) 840 |
    841 | 842 | ❌ **否則:** Express middlewares 的 bug === 所有或大部分 request 的 bug 843 | 844 |
    845 | 846 |
    程式範例 847 | 848 |
    849 | 850 | ### :clap: 正例:單獨測試 middleware,不發出網路請求或啟動整個 Express 伺服器 851 | 852 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Jest-blue.svg "Examples with Jest") 853 | 854 | ```javascript 855 | // the middleware we want to test 856 | const unitUnderTest = require("./middleware"); 857 | const httpMocks = require("node-mocks-http"); 858 | // Jest syntax, equivelant to describe() & it() in Mocha 859 | test("A request without authentication header, should return http status 403", () => { 860 | const request = httpMocks.createRequest({ 861 | method: "GET", 862 | url: "/user/42", 863 | headers: { 864 | authentication: "" 865 | } 866 | }); 867 | const response = httpMocks.createResponse(); 868 | unitUnderTest(request, response); 869 | expect(response.statusCode).toBe(403); 870 | }); 871 | ``` 872 | 873 |
    874 | 875 |

    876 | 877 | ## ⚪ ️2.5 使用靜態分析工具來測量與重構 878 | 879 | :white_check_mark: **建議:** 使用靜態分析工具可以幫助你客觀地提升程式品質並保持可維護性。你可以將靜態分析工具放在你的 CI 中。除了普通的 linting 外,它的主要賣點是查看多個檔案的上下文來檢查程式碼品質 (例如:發現程式有沒有重複定義的地方)、執行進階的分析 (例如:程式複雜度) 以及追蹤 code issue 的歷史和進度。有兩個工具供你使用:[SonarQube](https://www.sonarqube.org/) (6,300+ [stars](https://github.com/SonarSource/sonarqube)) 和 [Code Climate](https://codeclimate.com/) (2,300+ [stars](https://github.com/codeclimate/codeclimate))。 880 | 881 | Credit: [Keith Holliday](https://github.com/TheHollidayInn) 882 | 883 |
    884 | 885 | ❌ **否則;** 程式碼的品質過差,再新的函式庫或功能都無法拯救你的 bug 和性能 886 | 887 |
    888 | 889 |
    程式範例 890 | 891 |
    892 | 893 | ### :clap: 正例:CodeClimate,一個可以發現複雜方法的商業工具 894 | 895 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Code%20Climate-blue.svg "Examples with CodeClimate") 896 | 897 | ![alt text](assets/bp-16-yoni-goldberg-quality.png "CodeClimate, a commercial tool that can identify complex methods:") 898 | 899 |
    900 | 901 |

    902 | 903 | ## ⚪ ️ 2.6 檢查你對 Node 相關渾沌的準備工作 904 | 905 | :white_check_mark: **建議:** 奇怪的是,大部分的軟體測試都只專注在邏輯和資料層面,但最重要且很難被緩解的,是那些基礎設施的問題。例如,你有測試過當你的程序記憶體過載、伺服器或程序死掉時的表現嗎?或者你的監控系統可以檢測到 API 的回應時間慢了 50% 嗎?為了測試與減輕類似的問題,Netflix 設立了混沌工程 [Chaos engineering](https://principlesofchaos.org/)。它的目的是提供意識、框架及工具來測試我們的應用程式對於混沌問題的彈性。比如,最著名的工具之一,渾沌猴子 [the chaos monkey](https://github.com/Netflix/chaosmonkey),他會隨機地殺掉服務以確保我們的服務仍然可以提供服務給客戶,而不是僅依賴一個單獨的伺服器 (Kubernetes 也有一個 [kube-monkey](https://github.com/asobti/kube-monkey) 用來殺掉 pods)。這些工具都是作用在伺服器/平台面,但如果你想測試及產生單純的 Node 渾沌,比如檢查你的 Node 程序如何處理未知錯誤、未知的 promise rejection、v8 使用的記憶體超過 1.7GB 的限制以及當 event loop 卡住後你的 UX 是否仍然可以正常運行?為了解決上面提到的問題, [node-chaos](https://github.com/i0natan/node-chaos-monkey) 提供了各種 Node 相關的渾沌。 906 | 907 |
    908 | 909 | ❌ **否則:** 莫非定律一定會擊中你的產品,無可避免的 910 | 911 |
    912 | 913 |
    程式範例 914 | 915 |
    916 | 917 | ### :clap: 正例:Node-chaos 可以產生所有類型的 Node.js 問題,因此您可以測試您的應用程序對於渾沌的適應能力 918 | 919 | ![alt text](assets/bp-17-yoni-goldberg-chaos-monkey-nodejs.png "Node-chaos can generate all sort of Node.js pranks so you can test how resilience is your app to chaos") 920 | 921 |
    922 | 923 |

    924 | 925 | # 第 3 章:前端測試 926 | 927 | ## ⚪ ️ 3.1 將 UI 與功能分離 928 | 929 | :white_check_mark: **建議:** 當專注於測試組件邏輯時,UI 的細節就變成了應該被剔除的雜音,這樣您的測試目標就可以集中在資料面上。實際上,提取出程式中所需的資料,將降低與畫面的耦合,僅對單純的資料 (與 HTML/CSS 等圖形細節相比) 進行斷言,並停用會拖慢速度的動畫。您應該要試著避免畫面的渲染,僅測試 UI 後面的部分 (例如,服務、動作、存儲),但這將導致測試與實際情況不相符,「正確的資料根本無法呈現在 UI 上」這種問題就無法發現。 930 | 931 |
    932 | 933 | ❌ **否則:** 你的測試可能花了 10ms 就準備好資料,但因為一些無關緊要的花俏動畫,讓整個測試案例持續了 500ms。(100個測試 = 1分鐘) 934 | 935 |
    936 | 937 |
    程式範例: 938 | 939 |
    940 | 941 | ### :clap: 正例;分離 UI 的細節 942 | 943 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React-blue.svg "Examples with React") ![](https://img.shields.io/badge/🔧%20Example%20using%20React%20Testing%20Library-blue.svg "Examples with react-testing-library") 944 | 945 | ```javascript 946 | test("When users-list is flagged to show only VIP, should display only VIP members", () => { 947 | // Arrange 948 | const allUsers = [{ id: 1, name: "Yoni Goldberg", vip: false }, { id: 2, name: "John Doe", vip: true }]; 949 | 950 | // Act 951 | const { getAllByTestId } = render(); 952 | 953 | // Assert - Extract the data from the UI first 954 | const allRenderedUsers = getAllByTestId("user").map(uiElement => uiElement.textContent); 955 | const allRealVIPUsers = allUsers.filter(user => user.vip).map(user => user.name); 956 | expect(allRenderedUsers).toEqual(allRealVIPUsers); // compare data with data, no UI here 957 | }); 958 | ``` 959 | 960 |
    961 | 962 | ### :thumbsdown: 反例:混雜了 UI 與資料的斷言 963 | 964 | ```javascript 965 | test("When flagging to show only VIP, should display only VIP members", () => { 966 | // Arrange 967 | const allUsers = [{ id: 1, name: "Yoni Goldberg", vip: false }, { id: 2, name: "John Doe", vip: true }]; 968 | 969 | // Act 970 | const { getAllByTestId } = render(); 971 | 972 | // Assert - Mix UI & data in assertion 973 | expect(getAllByTestId("user")).toEqual('[
  • John Doe
  • ]'); 974 | }); 975 | ``` 976 | 977 |
    978 | 979 |

    980 | 981 | ## ⚪ ️ 3.2 使用不易改變的屬性來查询 HTML 元素 982 | 983 | :white_check_mark: **建議:** 使用不太容易受畫面變更而影響的屬性來查詢 HTML 元素 (例如 form label,而不是 CSS selector)。如果指定的元素沒有這樣的屬性,則創建一個專用的測試屬性,如 `test-id-submit-button`。這樣做不僅可以確保您的功能/邏輯測試不會因為外觀變化而中斷,而且整個團隊可以清楚地看到,測試案例使用了這個元素和屬性,不應該刪除它。 984 | 985 |
    986 | 987 | ❌ **否則:** 假設你想要測試一個跨越許多組件、邏輯和服務的登入功能,一切都設置得很完美 - stub、spy、Ajax 的呼叫都是隔離的。看似一切都很完美,但卻發現測試失敗了,因為開發者將 div 的 class 從 `thick-border` 改為 `thin-border`。 988 | 989 |
    990 | 991 |
    程式範例 992 | 993 |
    994 | 995 | ### :clap: 正例: 使用專用的 attribute 來查詢元素來進行測試 996 | 997 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React-blue.svg "Examples with React") 998 | 999 | ```html 1000 | // the markup code (part of React component) 1001 |

    1002 | 1003 | {value} 1004 | 1005 | 1006 |

    1007 | ``` 1008 | 1009 | ```javascript 1010 | // this example is using react-testing-library 1011 | test("Whenever no data is passed to metric, show 0 as default", () => { 1012 | // Arrange 1013 | const metricValue = undefined; 1014 | 1015 | // Act 1016 | const { getByTestId } = render(); 1017 | 1018 | expect(getByTestId("errorsLabel").text()).toBe("0"); 1019 | }); 1020 | ``` 1021 | 1022 |
    1023 | 1024 | ### :thumbsdown: 反例: 依靠於 CSS attributes 1025 | 1026 | ```html 1027 | 1028 | {value} 1029 | 1030 | ``` 1031 | 1032 | ```javascript 1033 | // this exammple is using enzyme 1034 | test("Whenever no data is passed, error metric shows zero", () => { 1035 | // ... 1036 | 1037 | expect(wrapper.find("[className='d-flex-column']").text()).toBe("0"); 1038 | }); 1039 | ``` 1040 | 1041 |
    1042 | 1043 |
    1044 | 1045 | ## ⚪ ️ 3.3 如果可以,使用真實且完全渲染的組件來進行測試 1046 | 1047 | :white_check_mark: **建議:** 只要尺寸合適,像使用者那樣從外部測試你的組件,完全渲染 UI,對其進行操作,並斷言對那些 UI 的行為是否符合預期。避免各種 mock、partial 和 shallow rendering - 這樣做可能會因為缺乏細節而導致有未捕捉到的 bug,而且由於測試會擾亂內部的結構而使得維護變得更加困難 (參考 堅持黑箱測試)。如果其中一個子組件明顯拖慢速度 (如 動畫) 或很難去設定,可以考慮使用假的組件去替換它。 1048 | 1049 | 綜上所說,需要注意的是:這種技術適用於包含合理大小子組件的中小型組件。完全渲染一個有太多子組件的組件會讓他很難被看出失敗的原因 (root cause analysis),而且可能會非常慢。在這種情況下,可以對那些很肥的父組件撰寫少量的測試,並對其子組件多寫幾個測試。 1050 | 1051 |
    1052 | 1053 | ❌ **否則:** 呼叫組件的私有方法來測試組件的內部狀態。後續重構組件時你必須重構所有測試。你真的有能力進行這種程度的維護嗎? 1054 | 1055 |
    1056 | 1057 |
    程式範例 1058 | 1059 |
    1060 | 1061 | ### :clap: 正例: 操作一個充分渲染的真實組件 1062 | 1063 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React-blue.svg "Examples with React") ![](https://img.shields.io/badge/🔧%20Example%20using%20Enzyme-blue.svg "Examples with Enzyme") 1064 | 1065 | ```javascript 1066 | class Calendar extends React.Component { 1067 | static defaultProps = { showFilters: false }; 1068 | 1069 | render() { 1070 | return ( 1071 |
    1072 | A filters panel with a button to hide/show filters 1073 | 1074 |
    1075 | ); 1076 | } 1077 | } 1078 | 1079 | // Examples use React & Enzyme 1080 | test("Realistic approach: When clicked to show filters, filters are displayed", () => { 1081 | // Arrange 1082 | const wrapper = mount(); 1083 | 1084 | // Act 1085 | wrapper.find("button").simulate("click"); 1086 | 1087 | // Assert 1088 | expect(wrapper.text().includes("Choose Filter")); 1089 | // This is how the user will approach this element: by text 1090 | }); 1091 | ``` 1092 | 1093 | ### :thumbsdown: 反例: 使用 shallow rendering 來測試 1094 | 1095 | ```javascript 1096 | test("Shallow/mocked approach: When clicked to show filters, filters are displayed", () => { 1097 | // Arrange 1098 | const wrapper = shallow(); 1099 | 1100 | // Act 1101 | wrapper 1102 | .find("filtersPanel") 1103 | .instance() 1104 | .showFilters(); 1105 | // Tap into the internals, bypass the UI and invoke a method. White-box approach 1106 | 1107 | // Assert 1108 | expect(wrapper.find("Filter").props()).toEqual({ title: "Choose Filter" }); 1109 | // what if we change the prop name or don't pass anything relevant? 1110 | }); 1111 | ``` 1112 | 1113 |
    1114 | 1115 |
    1116 | 1117 | ## ⚪ ️ 3.4 不要 sleep,善用框架內建對非同步事件的支援,並試著加速他 1118 | 1119 | :white_check_mark: **建議:** 在許多情況下,被測試單元的完成時間是未知的 (例如,因為動畫而延遲了元件的出現) — 在這種情況下,不要 sleep (例如使用 setTimeout),而是使用大多數框架提供的更靠譜的方法。一些函示庫允許等待操作 (例如 [Cypress .request('url')](https://docs.cypress.io/guides/references/best-practices.html#Unnecessary-Waiting)),另一些函示庫提供用於等待的 API,如 [@testing-library/dom 的方法 wait(expect(element))](https://testing-library.com/docs/guide-disappearance)。有時後,更優雅的方法是 stub 那些比較慢的資源,像是 API,然後一旦響應時間變得確定,組件就可以顯式地重新渲染。當依賴一些 sleep 的外部組件時,[加快時鐘的速度](https://jestjs.io/docs/en/timer-mocks)或許能提供幫助。 sleep 是一種需要避免的模式,因為它會導致你的測試變得緩慢或有風險(如果等待的時間太短)。當 sleep 和輪詢不可避免且測試框架原生不支持時,一些 npm 的函示庫 (如 [wait-for-expect](https://www.npmjs.com/package/wait-for-expect)) 可以幫助解決半確定性問題。 1120 | 1121 |
    1122 | 1123 | ❌ **否則:** 當 sleep 的時間太長時,測試速度會慢上一個數量級。當嘗試縮短 sleep 時間時,如果被測試的單元沒有及時響應,測試將會失敗。這時你不得不在脆弱的測試和糟糕的性能之間進行權衡。 1124 | 1125 |
    1126 | 1127 |
    程式範例 1128 | 1129 |
    1130 | 1131 | ### :clap: 正例: E2E API 在非同步的處理完後 resolves (Cypress) 1132 | 1133 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Cypress-blue.svg "Using Cypress to illustrate the idea") 1134 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React%20Testing%20Library-blue.svg "Examples with react-testing-library") 1135 | 1136 | ```javascript 1137 | // using Cypress 1138 | cy.get("#show-products").click(); // navigate 1139 | cy.wait("@products"); // wait for route to appear 1140 | // this line will get executed only when the route is ready 1141 | ``` 1142 | 1143 | ### :clap: 正例:測試的函示庫等待 DOM 元素 1144 | 1145 | ```javascript 1146 | // @testing-library/dom 1147 | test("movie title appears", async () => { 1148 | // element is initially not present... 1149 | 1150 | // wait for appearance 1151 | await wait(() => { 1152 | expect(getByText("the lion king")).toBeInTheDocument(); 1153 | }); 1154 | 1155 | // wait for appearance and return the element 1156 | const movie = await waitForElement(() => getByText("the lion king")); 1157 | }); 1158 | ``` 1159 | 1160 | ### :thumbsdown: 反例: 自製的 sleep 程式 1161 | 1162 | ```javascript 1163 | test("movie title appears", async () => { 1164 | // element is initially not present... 1165 | 1166 | // custom wait logic (caution: simplistic, no timeout) 1167 | const interval = setInterval(() => { 1168 | const found = getByText("the lion king"); 1169 | if (found) { 1170 | clearInterval(interval); 1171 | expect(getByText("the lion king")).toBeInTheDocument(); 1172 | } 1173 | }, 100); 1174 | 1175 | // wait for appearance and return the element 1176 | const movie = await waitForElement(() => getByText("the lion king")); 1177 | }); 1178 | ``` 1179 | 1180 |
    1181 | 1182 |
    1183 | 1184 | ## ⚪ ️ 3.5 觀察資源經由網路被提供的情況 1185 | 1186 | ![](https://img.shields.io/badge/🔧%20Example%20using%20Google%20LightHouse-blue.svg "Examples with Lighthouse") 1187 | 1188 | ✅ **建議:** 使用一些活動監視器,以確保在真實網路下的頁面載入情況是最佳的 — 這包含一些使用者體驗的問題:像是緩慢的頁面載入時間或未經壓縮的資源。市面上有很豐富的檢查工具:像 [pingdom](https://www.pingdom.com/)、AWS CloudWatch、[GCP StackDriver](https://cloud.google.com/monitoring/uptime-checks/) 這些工具可以很容易地監視伺服器是否正常運作著,是否有在合理的 SLA 下回應。不過這只解決了表面上的問題,最好選擇前端專用的工具 (如 [lighthouse](https://developers.google.com/web/tools/lighthouse/)、[pagespeed](https://developers.google.com/speed/pagespeed/insights/)) 來進行更全面的分析。並聚焦在那些直接影響使用者體驗的指標上,像是頁面載入時間、[有意義的繪製](https://scotch.io/courses/10-web-performance-audit-tips-for-your-next-billion-users-in-2018/fmp-first-meaningful-paint)、[頁面可互動時間(TTI)](https://calibreapp.com/blog/time-to-interactive/)。更重要的是,還可以關注其他原因,像是確保內容有被壓縮、[第一個 byte 的時間](https://developer.mozilla.org/en-US/docs/Glossary/time_to_first_byte)、圖片的最佳化、並確保合理的 DOM 尺寸、SSL 或其他。建議在開發期間將這些監視器納入 CI 的一部分,以及更重要的,在 24x7 的 production 伺服器/ CDN 上使用它們。 1189 | 1190 |
    1191 | 1192 | ❌ **否則:** 設計了一個精美的 UI、且通過了 100% 的功能測試與精心的包裝,使用者體驗卻因為 CDN 的錯誤設定而變得糟糕及緩慢。 1193 | 1194 |
    1195 | 1196 |
    程式範例 1197 | 1198 | ### :clap: 正例:Lighthouse 的頁面載入檢測報告 1199 | 1200 | ![](/assets/lighthouse2.png "Lighthouse page load inspection report") 1201 | 1202 |
    1203 | 1204 |
    1205 | 1206 | ## ⚪ ️ 3.6 stub 那些不穩定或緩慢的資源如後端 API 1207 | 1208 | :white_check_mark: **建議:** 當撰寫你主要的測試 (不是 E2E 測試) 時,避免接觸任何超出你職責和控制範圍的資源,如後端 API,而是使用 stub (測試替身)。使用一些測試替身的函式庫 (如 [Sinon](https://sinonjs.org/)、[Test doubles](https://www.npmjs.com/package/testdouble) 等) 來 stub API 的回應,而不是真正的對 API 進行呼叫。最大的好處是防止出現故障 — 測試或 API 的定義常常在變動的時候,儘管組件的表現正確 (生產環境不適合進行測試,它通常對 API 的呼叫進行限制),但有時會呼叫失敗。通過 stub 來模擬各種 API 行為,比如當沒有找到資料或 API 拋出錯誤時測試組件行為。最後但並非最不重要的原因是,經過網絡的呼叫將會大大降低執行測試的速度。 1209 | 1210 |
    1211 | 1212 | ❌ **否則:** 平均執行測試的時間不再只是幾毫秒而已,一個普通的 API 呼叫至少需要 100 毫秒,這會讓你的測試慢 20 倍以上。 1213 | 1214 |
    1215 | 1216 |
    程式範例 1217 | 1218 |
    1219 | 1220 | ### :clap: 正例: Stub 或攔截 API 的呼叫 1221 | 1222 | ![](https://img.shields.io/badge/🔧%20Example%20using%20React-blue.svg "Examples with React") ![](https://img.shields.io/badge/🔧%20Example%20using%20React%20Testing%20Library-blue.svg "Examples with react-testing-library") 1223 | 1224 | ```javascript 1225 | // unit under test 1226 | export default function ProductsList() { 1227 | const [products, setProducts] = useState(false); 1228 | 1229 | const fetchProducts = async () => { 1230 | const products = await axios.get("api/products"); 1231 | setProducts(products); 1232 | }; 1233 | 1234 | useEffect(() => { 1235 | fetchProducts(); 1236 | }, []); 1237 | 1238 | return products ?
    {products}
    :
    No products
    ; 1239 | } 1240 | 1241 | // test 1242 | test("When no products exist, show the appropriate message", () => { 1243 | // Arrange 1244 | nock("api") 1245 | .get(`/products`) 1246 | .reply(404); 1247 | 1248 | // Act 1249 | const { getByTestId } = render(); 1250 | 1251 | // Assert 1252 | expect(getByTestId("no-products-message")).toBeTruthy(); 1253 | }); 1254 | ``` 1255 | 1256 |
    1257 | 1258 |
    1259 | 1260 | ## ⚪ ️ 3.7 寫幾個跨越整個系統的 E2E 測試 1261 | 1262 | :white_check_mark: **建議:** 雖然 E2E (end to end,端到端) 通常代表在真實瀏覽器中進行 UI 測試 (參考 3.6 節),某些情況下,它們表示覆蓋整個系統的測試,包括連接真正的後端。後者的測試非常有價值,因為它們涵蓋那些前端和後端之間整合的問題,這些問題可能是由於溝通上,schema 產生誤會所導致。它們也是一種有效的方法來發現 backend-to-backend 的整合問題 (例如微服務 A 將錯誤的訊息發送給微服務 B) 甚至可以檢測出部署上的錯誤,目前後端沒有像前端 UI 測試工具如 [Cypress](https://www.cypress.io/) 或 [Puppeteer](https://github.com/GoogleChrome/puppeteer) 一樣友善且成熟的 E2E 框架。這種測試的缺點是,設定涵蓋這麼多組件的環境的成本很高,而且大多數組件都很脆弱 — 假設有 50 個微服務,只要其中一個死掉,整個 E2E 就會失敗。基於這個原因,我們應該少用這種技術,大概 1-10 個就夠了。也就是說,即使是少量的 E2E 測試也有機會捕獲它們 — 部署或整合的問題。建議在與生產環境相似的 stage 運行它們。 1263 | 1264 |
    1265 | 1266 | ❌ **否則:** UI 可能在功能測試上花費了大量的精力,但最後才發現後端回傳的內容 (UI 要使用的資料格式) 與預期中的不一樣。 1267 | 1268 |
    1269 | 1270 | ## ⚪ ️ 3.8 藉由重複使用登入憑證來加速 E2E 測試 1271 | 1272 | white_check_mark: **建議:** 在涉及真實的後端並必須使用有效的使用者 token 進行 API 呼叫的 E2E 測試中,我們沒有必要將每個測試都從「新增使用者並登錄」開始。相反的,在測試執行開始之前只登錄一次 (使用 before-all hook),將 token 儲存在本地端中,並在每個 request 之間重複使用它。雖然這似乎違反了測試的核心原則之一 — 保持測試的獨立性,不要耦合資源。這是一個合理的擔憂,但在 E2E 測試中,執行測試的性能是一個關鍵問題,在執行每個測試案例之前呼叫 1-3 個 API 可能會大大增加執行時間。重複使用憑證並不意味著測試必須基於相同的使用者資料 — 如果相依於使用者資料 (例如測試使用者付款的歷史記錄),那麼要確保產生這些資料來作為測試的一部分,並避免與其他測試共享它們。還要記住,後端是可以 fake 的 — 如果你的重點是測試前端,那麼最好隔離它,然後 stub 後端 API (參考 3.6 節)。 1273 | 1274 |
    1275 | 1276 | ❌ **否則:** 給定 200 個測試案例,假設登錄需要花費的時間為 100ms,則至少需要花費 20s,在這一遍遍的登錄上。 1277 | 1278 |
    1279 | 1280 |
    程式範例 1281 | 1282 |
    1283 | 1284 | ### :clap: 正例: 在 before-all 中登錄,而不是 before-each 1285 | 1286 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Cypress-blue.svg "Using Cypress to illustrate the idea") 1287 | 1288 | ```javascript 1289 | let authenticationToken; 1290 | 1291 | // happens before ALL tests run 1292 | before(() => { 1293 | cy.request('POST', 'http://localhost:3000/login', { 1294 | username: Cypress.env('username'), 1295 | password: Cypress.env('password'), 1296 | }) 1297 | .its('body') 1298 | .then((responseFromLogin) => { 1299 | authenticationToken = responseFromLogin.token; 1300 | }) 1301 | }) 1302 | 1303 | // happens before EACH test 1304 | beforeEach(setUser => () { 1305 | cy.visit('/home', { 1306 | onBeforeLoad (win) { 1307 | win.localStorage.setItem('token', JSON.stringify(authenticationToken)) 1308 | }, 1309 | }) 1310 | }) 1311 | 1312 | ``` 1313 | 1314 |
    1315 | 1316 |
    1317 | 1318 | ## ⚪ ️ 3.9 寫一個走過整個網站的 E2E 冒煙測試 1319 | 1320 | :white_check_mark: **建議:** 為了 production 環境的監控及開發時期的完整性檢查,執行一個 E2E 測試,讓這個測試走訪過所有或大多數的網站頁面,並確保那些頁面沒有損毀。這類型的測試投資報酬率很高,因為他很容易去撰寫及維護,卻可以檢測出各種類型的故障,包括功能性、網路或佈屬的問題。其他類型的冒煙測試或完整性檢查並沒有那麼可靠及詳盡 - 有些 ops 團隊只是 ping 網站首頁 (在production環境),或開發人員執行了一些整合測試,卻沒發現到打包或瀏覽器的問題。毫無疑問的,冒煙測試並不會取代功能測試,而只是作為一個快速的煙霧偵測器。 1321 | 1322 |
    1323 | 1324 | ❌ **否則:** 一切看似很完美,所有的測試都通過了,在 production 環境的健康狀態檢查也是 ok 的,但 Payment 這個組件有一些打包的問題,導致 /Paymout 這個路徑沒有被渲染。 1325 | 1326 |
    1327 | 1328 |
    程式範例 1329 | 1330 |
    1331 | 1332 | ### :clap: 正例:一個跑過所有頁面的冒煙測試 1333 | 1334 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Cypress-blue.svg "Using Cypress to illustrate the idea") 1335 | 1336 | ```javascript 1337 | it("When doing smoke testing over all page, should load them all successfully", () => { 1338 | // exemplified using Cypress but can be implemented easily 1339 | // using any E2E suite 1340 | cy.visit("https://mysite.com/home"); 1341 | cy.contains("Home"); 1342 | cy.visit("https://mysite.com/Login"); 1343 | cy.contains("Login"); 1344 | cy.visit("https://mysite.com/About"); 1345 | cy.contains("About"); 1346 | }); 1347 | ``` 1348 | 1349 |
    1350 | 1351 |
    1352 | 1353 | ## ⚪ ️ 3.10 將測試作為一個活的協作文件來看待 1354 | 1355 | :white_check_mark: **建議:** 除了提升應用程式的可靠性,測試還有一個非常誘人的應用 - 作為活的程式文件。由於測試程式本質上使用的是一種技術含量較低的產品/ UX 語言,因此使用正確的工具可以將他們轉換成一種易於溝通的媒介,方便開發人員與他們的使用者進行協調。舉例來說,有一些框架可以使用人類可閱讀的語言來表達流程與期望 (如,測試計畫),這樣一來,所有相關人員包括產品經理,都可以對測試進行閱讀、批准以及協作,如此一來.這個測試就成了活的需求文件。這樣的技術也被稱作 "驗收測試",因為它可以讓使用者用簡單的語言定義驗收標準。這是最純粹的 [BDD (行為驅動測試)](https://en.wikipedia.org/wiki/Behavior-driven_development),其中一個支援這個功能的框架是 [Cucumber](https://github.com/cucumber/cucumber-js),可以參考下面的程式範例。另一個相似但不同應用情境的是 [StoryBook](https://storybook.js.org/),它可以把 UI 的組件弄成圖形化的目錄,讓使用者可以瀏覽每個組件的各種狀態 (例如一個 grid w/o filter,讓他畫出多個 row 或沒有 row 等。),看他長得怎樣以及如何去觸發他的不同狀態 - 這也可以提供給產品相關人員,但主要是作為活的文件給使用這些組建的開發者們。 1356 | 1357 | ❌ **否則:** 在測試上已經耗費了大量的資源,如果不好好利用這項投資來獲取更大的價值,非常可惜。 1358 | 1359 |
    1360 | 1361 |
    程式範例 1362 | 1363 |
    1364 | 1365 | ### :clap: 正例:利用 cucumber-js 以人類可閱讀的語言來描述測試 1366 | 1367 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Cucumber-blue.svg "Examples using Cucumber") 1368 | 1369 | ```javascript 1370 | // this is how one can describe tests using cucumber: plain language that allows anyone to understand and collaborate 1371 | 1372 | Feature: Twitter new tweet 1373 | 1374 | I want to tweet something in Twitter 1375 | 1376 | @focus 1377 | Scenario: Tweeting from the home page 1378 | Given I open Twitter home 1379 | Given I click on "New tweet" button 1380 | Given I type "Hello followers!" in the textbox 1381 | Given I click on "Submit" button 1382 | Then I see message "Tweet saved" 1383 | 1384 | ``` 1385 | 1386 | ### :clap: 正例:利用 Storybook 來展示組件的的不同狀態及輸入 1387 | 1388 | ![](https://img.shields.io/badge/🔨%20Example%20using%20StoryBook-blue.svg "Using StoryBook") 1389 | 1390 | ![alt text](assets/story-book.jpg "Storybook") 1391 | 1392 |
    1393 | 1394 |

    1395 | 1396 | ## ⚪ ️ 3.11 使用自動化工作來偵測視覺問題 1397 | 1398 | :white_check_mark: **建議:** 設定自動化工具,在出現變化的時候擷取 UI 畫面,並檢測是否有內容重疊或破圖等問題。這樣做不僅可以確保資料的正確性,使用者也可以很方便的看到他們。這樣的技術沒有被廣泛的使用,我們的測試思維比較傾向於功能測試,但這代表了真實的使用者體驗,而且可以輕易地發現像是會在多個設備上展示的 UI 問題。有些免費的工具可以提供一些基本的功能 - 產生或儲存螢幕截圖,讓肉眼可以檢查。雖然這種方法對於規模較小的應用程式已經足夠,但他的缺點就跟任何手動測試一樣 - 在任何變更後都需要人力來處理。另一方面,由於缺乏清楚的定義,自動檢測 UI 問題非常有挑戰性,視覺回歸 (Visual Regression) 解決這難題的方法是,比較舊的 UI 與最新版的的差異,並顯示檢測結果。一些開源/免費的工具可以提供這樣的能力 (例如,[wraith](https://github.com/BBC-News/wraith), [PhantomCSS](https://github.com/HuddleEng/PhantomCSS)),但他們的安裝比較耗時。一些商業工具 (例如,[Applitools](https://applitools.com/), [Percy.io](https://percy.io/)) 則更進一步,他們簡化了安裝流程,並封裝了許多進階的功能,像是管理 UI、警告、藉由過濾 "視覺噪音"(如,廣告、動畫)來進行智慧抓取,甚至可以分析出造成 DOM/CSS 發生問題的根本原因。 1399 | 1400 |
    1401 | 1402 | ❌ **否則:** 一個顯示內容且通過100%的功能測試的頁面,載入速度非常快,但有一半的內容都被隱藏了,這樣的頁面是好的嗎? 1403 | 1404 |
    1405 | 1406 |
    程式範例 1407 | 1408 |
    1409 | 1410 | ### :thumbsdown: 反例:一個典型的 visual regression,右側內容顯示異常 1411 | 1412 | ![alt text](assets/amazon-visual-regression.jpeg "Amazon page breaks") 1413 | 1414 |
    1415 | 1416 | ### :clap: 正例:設定 wraith 來抓取並比對 UI 截圖 1417 | 1418 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Wraith-blue.svg "Using Wraith") 1419 | 1420 | ``` 1421 | ​# Add as many domains as necessary. Key will act as a label​ 1422 | 1423 | domains: 1424 | english: "http://www.mysite.com"​ 1425 | 1426 | ​# Type screen widths below, here are a couple of examples​ 1427 | 1428 | screen_widths: 1429 | 1430 | - 600​ 1431 | - 768​ 1432 | - 1024​ 1433 | - 1280​ 1434 | 1435 | ​# Type page URL paths below, here are a couple of examples​ 1436 | paths: 1437 | about: 1438 | path: /about 1439 | selector: '.about'​ 1440 | subscribe: 1441 | selector: '.subscribe'​ 1442 | path: /subscribe 1443 | ``` 1444 | 1445 | ### :clap: 正例:使用 Applitools 來獲得截圖的比對結果以及其他進階功能 1446 | 1447 | ![](https://img.shields.io/badge/🔨%20Example%20using%20AppliTools-blue.svg "Using Applitools") ![](https://img.shields.io/badge/🔨%20Example%20using%20Cypress-blue.svg "Using Cypress to illustrate the idea") 1448 | 1449 | ```javascript 1450 | import * as todoPage from "../page-objects/todo-page"; 1451 | 1452 | describe("visual validation", () => { 1453 | before(() => todoPage.navigate()); 1454 | beforeEach(() => cy.eyesOpen({ appName: "TAU TodoMVC" })); 1455 | afterEach(() => cy.eyesClose()); 1456 | 1457 | it("should look good", () => { 1458 | cy.eyesCheckWindow("empty todo list"); 1459 | todoPage.addTodo("Clean room"); 1460 | todoPage.addTodo("Learn javascript"); 1461 | cy.eyesCheckWindow("two todos"); 1462 | todoPage.toggleTodo(0); 1463 | cy.eyesCheckWindow("mark as completed"); 1464 | }); 1465 | }); 1466 | ``` 1467 | 1468 |
    1469 | 1470 |

    1471 | 1472 | # 第 4 章:測量測試效果 1473 | 1474 |

    1475 | 1476 | ## ⚪ ️ 4.1 藉由足夠的覆蓋率來獲得信心,~80% 看起來是個幸運數 1477 | 1478 | :white_check_mark: **建議:** 測試的目的是為了得到足夠的信心去進行更快速的迭代,很顯然地,越多的程式被測試到,團隊會更為自信。測試覆蓋率是用來測量測試程式走過多少行 (或 branch, statement, ...)。那要多少才夠?10% ~ 30% 明顯無法證明專案的正確性,但 100% 則可能會過於浪費時間,而且可能會迫使你關注太多枝微末節的程式。答案是,需要參考很多因素並取決於應用程式的類型,如果你正在建立次世代的空中巴士 A380,那 100% 的覆蓋率是必須的;然而對於一個卡通圖片的網站來說,50% 的覆蓋率可能太高。雖然大部分的測試愛好者都說覆蓋率的最低門檻要依客觀因素來決定,但他們都提到,根據經驗 80% 是個不錯的數字。([Fowler: “in the upper 80s or 90s”](https://martinfowler.com/bliki/TestCoverage.html)),足夠滿足大多數的應用程式。 1479 | 1480 | 實作建議:你可能會想在 CI 工具中設定測試覆蓋率的門檻 ([Jest link](https://jestjs.io/docs/configuration#collectcoverage-boolean)),並中斷那些未達覆蓋率門檻的建置 (也可以為每個組件設定覆蓋率門檻,參考下面的程式範例)。另外,也可以監測建置的覆蓋率是否下降 (當有一個新的且覆蓋率較低的程式被 commit) — 這將促使開發者去提升或至少維持一定的測試數量。說了很多,但測試覆蓋率只是一個量化出來的數值,它並不能證明你的測試是強壯的。或許你也會被他騙到 (參考下一小節的內容)。 1481 | 1482 |
    1483 | 1484 | ❌ **否則:** 自信與數字是相輔相成的,如果無法確保測試已經覆蓋了大部分的系統,你將感到害怕,且恐懼會使你變慢。 1485 | 1486 |
    1487 | 1488 |
    程式範例 1489 | 1490 |
    1491 | 1492 | ### :clap: 正例:一個典型的覆蓋率報告 1493 | 1494 | ![alt text](assets/bp-18-yoni-goldberg-code-coverage.png "A typical coverage report") 1495 | 1496 |
    1497 | 1498 | ### :clap: 正例:為每個組件設定覆蓋率 (使用 Jest) 1499 | 1500 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Jest-blue.svg "Using Jest") 1501 | 1502 | ![alt text](assets/bp-18-code-coverage2.jpeg "Setting up coverage per component (using Jest)") 1503 | 1504 |
    1505 | 1506 |

    1507 | 1508 | ## ⚪ ️ 4.2 檢查測試覆蓋率的報告來發現沒有被測試的區域或奇怪的地方 1509 | 1510 | :white_check_mark: **建議:** 有些問題隱藏在雷達之下,使用傳統的工具很難發現到它們。它們通常不是真正的 bug,大多數情況下是應用程式的奇怪行為,而這些行為可能會造成嚴重影響。例如,一些程式區域幾乎不會或很少被呼叫 — 你以為 "PricingCalculator" 這個 class 只會設定產品價格,結果他幾乎不會被呼叫,即使我們的資料庫中有 10000 件商品以及很多筆交易…… 測試覆蓋率報告可以幫助你發現應用程式是否按照你的期望在執行。除此之外,它會 highlight 出哪些類型的程式沒有被測試到,80% 的程式被測試並不能代表程式中關鍵的部分有被覆蓋到。產生報告很簡單,只需要在執行測試的時候開啟覆蓋率追蹤的功能,然後讓那些花花綠綠的報告來告訴你每個程式區塊被呼叫的頻率。如果你花時間去看這些數據,你可能會發現一些問題。 1511 | 1512 |
    1513 | 1514 | ❌ **否則:** 如果你不知道你的程式裡面有哪些地方沒有被測試到,你將無法知道問題的來源。 1515 | 1516 |
    1517 | 1518 |
    程式範例 1519 | 1520 |
    1521 | 1522 | ### :thumbsdown: 反例:這個測試覆蓋率的報告出了什麼問題? 1523 | 1524 | 基於一個真實世界的情境,我們在 QA 中追蹤了我們應用程式的使用情況,並發現了這個有趣的登錄模式 (提示:登入失敗的數量不成正比的,顯然是有問題的。最後發現,有一些前端的 bug 一直在打後端的登入 API) 1525 | 1526 | ![alt text](assets/bp-19-coverage-yoni-goldberg-nodejs-consultant.png "What’s wrong with this coverage report?") 1527 | 1528 |
    1529 | 1530 |

    1531 | 1532 | ## ⚪ ️ 4.3 使用「變異測試」測量邏輯覆蓋率 1533 | 1534 | :white_check_mark: **建議:** 傳統的測試覆蓋率通常是騙人的,他可能會告訴你有 100% 的測試覆蓋率,但可能你的 function 都沒有回傳正確的值。為什麼會這樣?因為他只是很單純的測量你的測試程式走過哪幾行,而不會檢查測試案例到底測試了什麼,他到底有沒有確實去斷言正確的回應。就像有個人因公出差,他出示了他的護照,他無法證明他做了什麼工作,只能證明有去過哪幾個機場。 1535 | 1536 | 基於變異的測試,是透過測量"實際測試"的程式數量而不僅僅是"訪問"過的數量來提供協助。[Stryker](https://stryker-mutator.io/) 是一個用於進行變異測試的 JavaScript 函示庫,他的實作非常巧妙: 1537 | 1538 | (1) 他會刻意在你的程式中「植入 bug」。例如程式 `newOrder.price === 0` 會被改成 `newOrder.price != 0`,這個 "bug" 就稱為變異。 1539 | 1540 | (2) 他會跑過一次測試,如果測試通過了代表有些問題,這些測試案例沒有達到發現 bug 的目的,導致這些變異活了下來。如果測試失敗了,非常好,那些變異就會被殺掉。 1541 | 1542 | 相較於傳統的測試覆蓋率,如果知道所有的變異都被殺死,會讓你更有自信,而且這兩者花費的時間差不多。 1543 | 1544 |
    1545 | 1546 | ❌ **否則:** 你可能會誤以為 85% 的測試覆蓋率能發現程式中 85% 的 bug。 1547 | 1548 |
    1549 | 1550 |
    程式範例 1551 | 1552 |
    1553 | 1554 | ### :thumbsdown: 反例: 100% coverage, 0% testing 1555 | 1556 | ![](https://img.shields.io/badge/🔨%20Example%20using%20Stryker-blue.svg "Using Stryker") 1557 | 1558 | ```javascript 1559 | function addNewOrder(newOrder) { 1560 | logger.log(`Adding new order ${newOrder}`); 1561 | DB.save(newOrder); 1562 | Mailer.sendMail(newOrder.assignee, `A new order was places ${newOrder}`); 1563 | 1564 | return { approved: true }; 1565 | } 1566 | 1567 | it("Test addNewOrder, don't use such test names", () => { 1568 | addNewOrder({ assignee: "John@mailer.com", price: 120 }); 1569 | }); // Triggers 100% code coverage, but it doesn't check anything 1570 | ``` 1571 | 1572 |
    1573 | 1574 | ### :clap: 正例:Stryker 的報告,一個變異測試的工具,偵測並統計沒有被測試到的程式 (變異) 1575 | 1576 | ![alt text](assets/bp-20-yoni-goldberg-mutation-testing.jpeg "Stryker reports, a tool for mutation testing, detects and counts the amount of code that is not tested (Mutations)") 1577 | 1578 |
    1579 | 1580 |

    1581 | 1582 | ## ⚪ ️4.4 使用 Test linter 來避免測試程式的問題 1583 | 1584 | :white_check_mark: **建議:** 有一系列的 ESLint 外掛可以檢查測試程式的風格並發現問題。比如 [eslint-plugin-mocha](https://www.npmjs.com/package/eslint-plugin-mocha) 會警告一個寫在 global 層的測試案例 (不是寫在 describe() 底下),或者當測試案例被 [skip](https://mochajs.org/#inclusive-tests) 時會發出警告,因為這可能會導致你誤會所有測試都通過了。類似的像,[eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) 可以在一個測試案例沒有任何斷言 (沒有檢查任何內容) 時給出警告。 1585 | 1586 |
    1587 | 1588 | ❌ **否則:** 當你滿足於 90% 的測試覆蓋率或 100% 的綠色報告時,卻發現很多測試都沒什麼斷言,或是很多測試直接被 skip 掉了。但願你沒有把這份程式佈署出去。 1589 | 1590 |
    1591 |
    程式範例 1592 | 1593 |
    1594 | 1595 | ### :thumbsdown: 反例:一個充滿錯誤的測試案例,還好都被 Linter 抓到了 1596 | 1597 | ```javascript 1598 | describe("Too short description", () => { 1599 | const userToken = userService.getDefaultToken() // *error:no-setup-in-describe, use hooks (sparingly) instead 1600 | it("Some description", () => {}); // *error: valid-test-description. Must include the word "Should" + at least 5 words 1601 | }); 1602 | 1603 | it.skip("Test name", () => { // *error:no-skipped-tests, error:error:no-global-tests. Put tests only under describe or suite 1604 | expect("somevalue"); // *error:no-assert 1605 | }); 1606 | 1607 | it("Test name", () => { // *error:no-identical-title. Assign unique titles to tests 1608 | }); 1609 | ``` 1610 | 1611 |
    1612 | 1613 |

    1614 | 1615 | # 第 5 章:持續整合 (CI) 或其他提高品質的手段 1616 | 1617 |

    1618 | 1619 | ## ⚪ ️ 5.1 豐富你的 linter 並捨棄有 linting 問題的建置 1620 | 1621 | :white_check_mark: **建議:** 只需要 5 分鐘的設定,就可以免費得到自動保護程式碼的工具來偵測出程式中的問題。Linter 不再只是樣式工具,現在的 linter 可以抓到許多嚴重的問題,像是 error 沒有被正確的拋出或訊息的遺失。在基本的規則 (如 [ESLint standard](https://www.npmjs.com/package/eslint-plugin-standard) 或 [Airbnb style](https://www.npmjs.com/package/eslint-config-airbnb)) 之上,可以考慮加上一些特殊的 linter,像是 [eslint-plugin-chai-expect](https://www.npmjs.com/package/eslint-plugin-chai-expect) 可以用來偵測測試案例有沒有寫斷言,[eslint-plugin-promise](https://www.npmjs.com/package/eslint-plugin-promise?activeTab=readme) 可以發現 Promise 有沒有 resolve (否則會導致你的程式不能繼續),[eslint-plugin-security](https://www.npmjs.com/package/eslint-plugin-security?activeTab=readme) 可以發現可能會導致 DOS 攻擊的正規表示式,還有 [eslint-plugin-you-dont-need-lodash-underscore](https://www.npmjs.com/package/eslint-plugin-you-dont-need-lodash-underscore) 會在程式碼使用到 V8 的核心方法的時候給予警告,例如 Lodash.\_map(…)。 1622 |
    1623 | 1624 | ❌ **否則:** 想像在某個雨天中,你的程式一直 crash,而且 log 沒有顯示 stack trace 的訊息。到底發生什麼事了?你的程式錯誤地拋出了一個非 error 的物件,而且 stack trace 都不見了,這會讓你想去撞牆。只要用 5 分鐘來設定 linter 就可以幫你偵測出這種 typo 錯誤,並拯救你一整天。 1625 | 1626 |
    1627 | 1628 |
    程式範例 1629 | 1630 |
    1631 | 1632 | ### :thumbsdown: 反例:Error 物件被拋出,這樣的錯誤不會出現 stack trace。幸運的是,ESLint 抓到了這個 bug。 1633 | 1634 | ![alt text](assets/bp-21-yoni-goldberg-eslint.jpeg "The wrong Error object is thrown mistakenly, no stack-trace will appear for this error. Luckily, ESLint catches the next production bug") 1635 | 1636 |
    1637 | 1638 |

    1639 | 1640 | ## ⚪ ️ 5.2 透過本地端的 CI 來縮短回饋循環 1641 | 1642 | :white_check_mark: **建議:** 在本地端使用一個包含測試、Lint、穩定性檢查等功能的 CI,可以幫助開發者快速得到回饋並縮短 [回饋循環](https://www.gocd.org/2016/03/15/are-you-ready-for-continuous-delivery-part-2-feedback-loops/)。因為一個有效的測試流程包含很多迭代循環 (1) 嘗試 -> (2) 回饋 -> (3) 重構。所以回饋越快,開發者可以在每個流程中可以執行的迭代就越多,且可以得到更好的結果。反過來,如果回饋來得很慢,一天只能執行很少個迭代,那團隊可能會因為急需執行下一個主題/任務/循環,而不再優化當前的循環。 1643 | 1644 | 目前有些 CI 的服務供應商 (如:[CircleCI local CLI](https://circleci.com/docs/2.0/local-cli/)) 允許在本地端執行 CI pipeline。有些商業工具像是 [wallaby](https://wallabyjs.com/) 為開發提供了非常有用的測試功能。或者你可以在 package.json 中增加 npm script 來跑一些提升程式品質的指令 — 使用工具如 [concurrently](https://www.npmjs.com/package/concurrently) 來並行執行,並在任何工具執行失敗後拋出非 0 的結束碼。開發者只需執行一個指令(如 `npm run quality` )來快速獲取回饋。也可以用 githook 來取消沒有通過程式品質檢查的提交( [husky](https://github.com/typicode/husky) 可以幫助你)。 1645 |
    1646 | 1647 | ❌ **否則** 如果品質檢查的結果在程式提交後第二天才收到,那測試就不算開發的一部分了。 1648 | 1649 |
    1650 | 1651 |
    程式範例 1652 | 1653 |
    1654 | 1655 | ### :clap: 正例:用來執行程式品質檢查的 npm script,在開發者主動觸發或嘗試提交新程式時執行。 1656 | 1657 | ```javascript 1658 | "scripts": { 1659 | "inspect:sanity-testing": "mocha **/**--test.js --grep \"sanity\"", 1660 | "inspect:lint": "eslint .", 1661 | "inspect:vulnerabilities": "npm audit", 1662 | "inspect:license": "license-checker --failOn GPLv2", 1663 | "inspect:complexity": "plato .", 1664 | 1665 | "inspect:all": "concurrently -c \"bgBlue.bold,bgMagenta.bold,yellow\" \"npm:inspect:quick-testing\" \"npm:inspect:lint\" \"npm:inspect:vulnerabilities\" \"npm:inspect:license\"" 1666 | }, 1667 | "husky": { 1668 | "hooks": { 1669 | "precommit": "npm run inspect:all", 1670 | "prepush": "npm run inspect:all" 1671 | } 1672 | } 1673 | 1674 | ``` 1675 | 1676 |
    1677 | 1678 |

    1679 | 1680 | ## ⚪ ️5.3 在真正 production 的鏡像環境中執行 e2e 測試 1681 | 1682 | :white_check_mark: **建議:** End to end (e2e) 測試是每個 CI pipeline 會面臨的大挑戰 - 即時創建一個與真正 production 環境相同的鏡像環境並擁有所有相關的服務,是很費時費力的。你需要找到適合的折衷點:[Docker-compose](https://serverless.com/) 藉由一個純文字檔將 docker 化的環境放在獨立的 container 中,但他背後使用的技術 (例如網路與佈署模型) 仍然與真實世界有所差異。可以將其與 [AWS Local](https://github.com/localstack/localstack) 整合,在真正的 AWS服務中做使用。如果你使用 [serverless](https://serverless.com/) 框架,[AWS SAM](https://docs.aws.amazon.com/lambda/latest/dg/serverless_app.html) 可以讓你在本地端調用 FaaS 程式碼。 1683 | 1684 | 龐大的 Kubernetes 生態系還沒有一個標準、方便的本地端、CI鏡像的工具,儘管現在已有許多工具的出現。有一種方法是使用像是 [Minikube](https://kubernetes.io/docs/setup/minikube/) 和 [MicroK8s](https://microk8s.io/) 這樣的工具來運行一個 "最小化的 Kubernetes",這些工具更貼近現實,且成本花費很小。另一種方法是在遠端的 "真實 Kubernetes" 環境上運行測試,一些 CI 的服務供應商 (如 [Codefresh](https://codefresh.io/)) 與 Kubernetes 環境擁有原生的整合,讓在 CI pipeline 上執行真實的環境變得更為容易。有的供應商則可以讓你針對遠端的 Kubernetes 自訂腳本。 1685 |
    1686 | 1687 | ❌ **否則:** 在生產環境和測試環境中使用不同的技術,就會需要維護兩種佈署模型,會使開發人員與維運人員分開。 1688 | 1689 |
    1690 | 1691 |
    程式範例 1692 | 1693 |
    1694 | 1695 | ### :clap: 正例:動態產生 Kubernetes cluster 的 CI pipeline ([Credit: Dynamic-environments Kubernetes](https://container-solutions.com/dynamic-environments-kubernetes/)) 1696 | 1697 | ```yaml 1698 | deploy: 1699 | stage: deploy 1700 | image: registry.gitlab.com/gitlab-examples/kubernetes-deploy 1701 | script: 1702 | - ./configureCluster.sh $KUBE_CA_PEM_FILE $KUBE_URL $KUBE_TOKEN 1703 | - kubectl create ns $NAMESPACE 1704 | - kubectl create secret -n $NAMESPACE docker-registry gitlab-registry --docker-server="$CI_REGISTRY" --docker-username="$CI_REGISTRY_USER" --docker-password="$CI_REGISTRY_PASSWORD" --docker-email="$GITLAB_USER_EMAIL" 1705 | - mkdir .generated 1706 | - echo "$CI_BUILD_REF_NAME-$CI_BUILD_REF" 1707 | - sed -e "s/TAG/$CI_BUILD_REF_NAME-$CI_BUILD_REF/g" templates/deals.yaml | tee ".generated/deals.yaml" 1708 | - kubectl apply --namespace $NAMESPACE -f .generated/deals.yaml 1709 | - kubectl apply --namespace $NAMESPACE -f templates/my-sock-shop.yaml 1710 | environment: 1711 | name: test-for-ci 1712 | ``` 1713 | 1714 |
    1715 | 1716 |

    1717 | 1718 | ## ⚪ ️5.4 並行測試工作 1719 | 1720 | :white_check_mark: **建議:** 在合理的情況下,測試是你 24/7 的好朋友,他為你帶來即時的回饋。實際上,在單線程的 CPU 上執行 500 個單元測試可能會非常耗時。幸運的是,近代的測試執行器或 CI 平台 (如 [Jest](https://github.com/facebook/jest), [AVA](https://github.com/avajs/ava) 或 [Mocha extensions](https://github.com/yandex/mocha-parallel-tests)) 可以將測試並行為多個程序來執行,藉此來大幅縮短回饋的時間。一些 CI 的廠商也支援跨容器並行測試,更進一步地縮短回饋的時間。無論是在本地端使用多個程序,或是在一些 cloud CLI 上使用多台機器來執行測試,並行化的重點是要保持測試的自主性,因為每個測試都可能在不同的程序上做執行。 1721 | 1722 | ❌ **否則:** 如果送出程式碼一個小時後才收到測試結果,但你已經在開發下一個功能了,這會導致測試對你來說變的不是那麼重要。 1723 | 1724 |
    1725 | 1726 |
    程式範例 1727 | 1728 |
    1729 | 1730 | ### :clap: 正例:藉由測試並行化,Mocha parallel 與 Jest 可以輕易的超越傳統 Mocha ([Credit: JavaScript Test-Runners Benchmark](https://medium.com/dailyjs/javascript-test-runners-benchmark-3a78d4117b4)) 1731 | 1732 | ![alt text](assets/bp-24-yonigoldberg-jest-parallel.png "Mocha parallel & Jest easily outrun the traditional Mocha thanks to testing parallelization (Credit: JavaScript Test-Runners Benchmark)") 1733 | 1734 |
    1735 | 1736 |

    1737 | 1738 | ## ⚪ ️5.5 使用 License 和抄襲檢查來避免法務上的問題 1739 | 1740 | :white_check_mark: **建議:** License 和抄襲的問題或許不是你現在關注的點,但為什麼不在 10 分鐘內把這件工作設定好呢?許多 npm 的套件,像是 [license check](https://www.npmjs.com/package/license-checker) 和 [plagiarism check](https://www.npmjs.com/package/plagiarism-checker) (商業軟體,但有免費使用版本) 可以很容易的整合進你的 CI pipeline 中,並檢查那些像是使用限制性 license 或從 Stack Overflow 複製貼上明顯侵犯版權的程式。 1741 | 1742 | ❌ **否則:** 在不經意的情況下,開發人員可能會使用具有不適當 License 的套件,或將商業程式複製貼上,從而遇到法律上的問題。 1743 | 1744 |
    1745 | 1746 |
    程式範例 1747 | 1748 |
    1749 | 1750 | ### :clap: 正例: 1751 | 1752 | ```javascript 1753 | //install license-checker in your CI environment or also locally 1754 | npm install -g license-checker 1755 | 1756 | //ask it to scan all licenses and fail with exit code other than 0 if it found unauthorized license. The CI system should catch this failure and stop the build 1757 | license-checker --summary --failOn BSD 1758 | 1759 | ``` 1760 | 1761 |
    1762 | 1763 | ![alt text](assets/bp-25-nodejs-licsense.png) 1764 | 1765 |
    1766 | 1767 |

    1768 | 1769 | ## ⚪ ️5.6 持續檢查有漏洞的相依套件 1770 | 1771 | :white_check_mark: **建議:** 即使是最有信譽的相依套件,如 Express,也有已知的漏洞。可以藉由使用社群工具 (如 [npm audit](https://docs.npmjs.com/getting-started/running-a-security-audit)) 或商業工具 (如 [snyk](https://snyk.io/) (也有免費版本)) 來輕鬆解決問題。可以在每次的建置中,透過 CI pipeline 調用他們。 1772 | 1773 | ❌ **否則:** 在沒有專用工具的情況下,要保持你的程式沒有漏洞,就需要不斷追蹤網路上新發佈的漏洞威脅資訊,這會相當令人乏味。 1774 | 1775 |
    1776 | 1777 |
    程式範例 1778 | 1779 |
    1780 | 1781 | ### :clap: 正例:NPM Audit 的結果 1782 | 1783 | ![alt text](assets/bp-26-npm-audit-snyk.png "NPM Audit result") 1784 | 1785 |
    1786 | 1787 |

    1788 | 1789 | ## ⚪ ️5.7 自動升級相依套件 1790 | 1791 | :white_check_mark: **建議:** Yarn 和 npm 的 package-lock.json 間接導入了一個嚴重的問題(本意是好的,但卻通往地獄)- 預設情況下,套件將不再得到更新。即使團隊使用 `npm install` 和 `npm update` 也不會獲得任何更新。會導致專案相依於不好的套件版本,或者最壞的情況是使用到容易被攻擊的程式。現在,團隊依靠開發人員的善意和記憶來手動更新 package.json 或手動使用像 [ncu]((https://www.npmjs.com/package/npm-check-updates)) 這樣的工具。然而更靠譜的方式是自動獲取可靠的相依套件版本,雖然沒有最佳的解決方案,但目前有兩種可能的自動化方式: 1792 | 1793 | (1) 使用 [npm outdated](https://docs.npmjs.com/cli/outdated) 或 npm-check-updates (ncu),當有過時的相依套件時,讓 CI 的建置失敗。這樣可以強制開發人員來更新相依套件。 1794 | 1795 | (2) 使用商業工具,他們可以掃描程式並自動發送更新相依套件的 PR。剩下的有趣問題是相依套件的更新策略 — 若每個補丁都更新會產生太多的開銷,而大版本發佈時更新可能會指向一個不穩定的版本(許多套件在發佈後的幾天內被爆出漏洞,請參閱 [eslint-scope]((https://nodesource.com/blog/a-high-level-post-mortem-of-the-eslint-scope-security-incident/)) 事件)。 1796 | 1797 | 有效的更新策略可能是允許一些 "容忍期" — 讓程式可以延後 @latest 一段時間和版本,再將本地端的副本視為過時(例如本地版本為 1.3.1 ,存儲庫版本為1.3.8)。 1798 | 1799 |
    1800 | 1801 | ❌ **否則:** 你在 production 環境所使用的相依套件,可能已經被該作者標示為是有風險的。 1802 | 1803 |
    1804 | 1805 |
    程式範例 1806 | 1807 |
    1808 | 1809 | ### :clap: 正例:[ncu](https://www.npmjs.com/package/npm-check-updates) 可以手動或在 CI pipeline 中使用,以檢測程式落後最新版本多少。 1810 | 1811 | ![alt text](assets/bp-27-yoni-goldberg-npm.png "ncu can be used manually or within a CI pipeline to detect to which extent the code lag behind the latest versions") 1812 | 1813 |
    1814 | 1815 |

    1816 | 1817 | ## ⚪ ️ 5.8 其他,與 node 無關的 CI 建議 1818 | 1819 | :white_check_mark: **建議:** 本文的重點是與 Node 有點關係的測試建議。但本節整理了一些眾所周知的與 Node 無關的技巧: 1820 | 1821 | 1. 使用聲明式語法。這是大多數工具的唯一選擇,雖然舊版本的 Jenkins 允許使用程式或 UI。 1822 | 1. 選擇具有本地端 Docker 支援的工具。 1823 | 1. 盡快失敗,先執行最快的測試。設立一個"冒煙測試"的 step/milestone,對多個快速檢查工具(如 linting,單元測試)進行分組,為程式提交者提供快速回饋。 1824 | 1. 設法方便地瀏覽建置的所有產出,包括測試報告,覆蓋率報告,變異報告,log 等。 1825 | 1. 為每個事件創建多個 pipelines/jobs。例如,為 feature branch 的提交設定一個 job,為 master PR 設定另一個。 (大多數工具提供了一些程式重用的機制) 1826 | 1. 永遠不要在 job 定義中加入機密信息,從 secret store 或 job 的設定中獲取。 1827 | 1. 在 release 中明確定義版本號。 1828 | 1. 僅建置一次,並對整個 build artifact(例如 Docker image)執行所有的檢查。 1829 | 1. 在一個臨時的環境中執行測試,在不同建置之間不會改變狀態。快取 node_modules 可能是唯一的例外。 1830 | 1831 |
    1832 | 1833 | ❌ **否則:** 你會錯過多年的智慧結晶 1834 | 1835 |

    1836 | 1837 | ## ⚪ ️ 5.9 建置模型(Matrix):使用多個 Node 版本執行同一個 CI 流程 1838 | 1839 | :white_check_mark: **建議:** 品質的檢查是用於發現意外,測試覆蓋的部分越多,你就越可能儘早地發現問題。在開發會重複使用的套件或執行具有各種設定和 Node 版本的多用戶生產環境時,CI pipeline 必須在所有設定的組合上執行測試。例如,假設我們的某些客戶使用 MySQL,另一批客戶使用 Postgres。一些 CI 工具支持一種稱為"Matrix"的功能,該功能可以針對 MySQL、Postgres 或多個 Node 版本(如8、9、10)的所有組合執行測試。只要設定即可完成而無需任何額外工作。其他不支援 Matrix 的 CI 可能可以通過安裝外掛或一定程度的調整來實現這個功能。 1840 | 1841 |
    1842 | 1843 | ❌ **否則:** 在做了那麼多辛苦的測試工作之後,怎麼能讓錯誤僅僅因為設定的問題而出現。 1844 | 1845 |
    1846 | 1847 |
    程式範例 1848 | 1849 |
    1850 | 1851 | ### :clap: 正例:使用 Travis (CI 供應商) 的建置定義,在多個 Node 版本上執行相同的測試 1852 | 1853 |
    language: node_js
    node_js:
    - "7"
    - "6"
    - "5"
    - "4"
    install:
    - npm install
    script:
    - npm run test
    1854 |
    1855 | 1856 |

    1857 | 1858 | # Team 1859 | 1860 | ## Yoni Goldberg 1861 | 1862 |
    1863 | 1864 |
    1865 | 1866 | **Role:** Writer 1867 | 1868 | **About:** I'm an independent consultant who works with Fortune 500 companies and garage startups on polishing their JS & Node.js applications. More than any other topic I'm fascinated by and aims to master the art of testing. I'm also the author of [Node.js Best Practices](https://github.com/goldbergyoni/nodebestpractices) 1869 | 1870 | **📗 Online Course:** Liked this guide and wish to take your testing skills to the extreme? Consider visiting my comprehensive course [Testing Node.js & JavaScript From A To Z](https://www.testjavascript.com) 1871 | 1872 |
    1873 | 1874 | **Follow:** 1875 | 1876 | - [🐦 Twitter](https://twitter.com/goldbergyoni/) 1877 | - [📞 Contact](https://testjavascript.com/contact-2/) 1878 | - [✉️ Newsletter](https://testjavascript.com/newsletter//) 1879 | 1880 |
    1881 |
    1882 |
    1883 | 1884 | ## [Bruno Scheufler](https://github.com/BrunoScheufler) 1885 | 1886 | **Role:** Tech reviewer and advisor 1887 | 1888 | Took care to revise, improve, lint and polish all the texts 1889 | 1890 | **About:** full-stack web engineer, Node.js & GraphQL enthusiast 1891 | 1892 |
    1893 |
    1894 | 1895 | ## [Ido Richter](https://github.com/idori) 1896 | 1897 | **Role:** Concept, design and great advice 1898 | 1899 | **About:** A savvy frontend developer, CSS expert and emojis freak 1900 | 1901 | ## [Kyle Martin](https://github.com/js-kyle) 1902 | 1903 | **Role:** Helps keep this project running, and reviews security related practices 1904 | 1905 | **About:** Loves working on Node.js projects and web application security. 1906 | --------------------------------------------------------------------------------