foreword to the second edition xiv
foreword to the first edition xvi
preface xviii
acknowledgments xx
about this book xxi
about the authors xxv
about the cover illustration xxvi
PART 1 GETTING STARTED 1
1 The basics of unit testing 3
1.1 The first step 5
1.2 Defining unit testing, step by step 5
1.3 Entry points and exit points 6
1.4 Exit point types 11
1.5 Different exit points, different techniques 12
1.6 A test from scratch 12
1.7 Characteristics of a good unit test 15
What is a good unit test? 15
A unit test checklist 16
1.8 Integration tests 17
1.9 Finalizing our definition 21
1.10 Test-driven development 22
TDD: Not a substitute for good unit tests 24
Three core skills needed for successful TDD 25
2 A first unit test 28
2.1 Introducing Jest 29
Preparing our environment 29
Preparing our working folder 29
Installing Jest 30
Creating a test file 30
Executing Jest 31
2.2 The library, the assert, the runner, and the reporter 33
2.3 What unit testing frameworks offer 34
The xUnit frameworks 36
xUnit, TAP, and Jest structures 36
2.4 Introducing the Password Verifier project 37
2.5 The first Jest test for verifyPassword 37
The Arrange-Act-Assert pattern 38
Testing the test 39
USE naming 39
String comparisons and maintainability 40
Using describe() 40
Structure implying context 41
The it() function 42
Two Jest flavors 42
Refactoring the production code 43
2.6 Trying the beforeEach() route 45
beforeEach() and scroll fatigue 47
2.7 Trying the factory method route 49
Replacing beforeEach() completely with factory methods 50
2.8 Going full circle to test() 52
2.9 Refactoring to parameterized tests 52
2.10 Checking for expected thrown errors 55
2.11 Setting test categories 56
PART 2 CORE TECHNIQUES 59
3 Breaking dependencies with stubs 61
3.1 Types of dependencies 62
3.2 Reasons to use stubs 64
3.3 Generally accepted design approaches to stubbing 66
Stubbing out time with parameter injection 66
Dependencies, injections, and control 68
3.4 Functional injection techniques 69
Injecting a function 69
Dependency injection via partial application 70
3.5 Modular injection techniques 70
3.6 Moving toward objects with constructor functions 73
3.7 Object-oriented injection techniques 74
Constructor injection 74
Injecting an object instead of a function 76
Extracting a common interface 79
4 Interaction testing using mock objects 83
4.1 Interaction testing, mocks, and stubs 84
4.2 Depending on a logger 85
4.3 Standard style: Introduce parameter refactoring 87
4.4 The importance of differentiating between mocks and stubs 88
4.5 Modular-style mocks 89
Example of production code 90
Refactoring the production code in a modular injection style 91
A test example with modular-style injection 92
4.6 Mocks in a functional style 92
Working with a currying style 92
Working with higher-order functions and not currying 93
4.7 Mocks in an object-oriented style 94
Refactoring production code for injection 94
Refactoring production code with interface injection 96
4.8 Dealing with complicated interfaces 98
Example of a complicated interface 98
Writing tests with
complicated interfaces 99
Downsides of using complicated
interfaces directly 100
The interface segregation principle 101
4.9 Partial mocks 101
A functional example of a partial mock 101
An object-oriented partial mock example 102
5 Isolation frameworks 104
5.1 Defining isolation frameworks 105
Choosing a flavor: Loose vs. typed 105
5.2 Faking modules dynamically 106
Some things to notice about Jest’s API 108
Consider abstracting away direct dependencies 109
5.3 Functional dynamic mocks and stubs 109
5.4 Object-oriented dynamic mocks and stubs 110
Using a loosely typed framework 110
Switching to a type-friendly framework 112
5.5 Stubbing behavior dynamically 114
An object-oriented example with a mock and a stub 114
Stubs and mocks with substitute.js 116
5.6 Advantages and traps of isolation frameworks 117
You don’t need mock objects most of the time 118
Unreadable test code 118
Verifying the wrong things 118
Having more than one mock per test 119
Overspecifying the tests 119
6 Unit testing asynchronous code 121
6.1 Dealing with async data fetching 122
An initial attempt with an integration test 123
Waiting for the act 124
Integration testing of async/await 124
Challenges with integration tests 125
6.2 Making our code unit-test friendly 125
Extracting an entry point 126
The Extract Adapter pattern 131
6.3 Dealing with timers 138
Stubbing timers out with monkey-patching 138
Faking setTimeout with Jest 139
6.4 Dealing with common events 141
Dealing with event emitters 141
Dealing with click events 142
6.5 Bringing in the DOM testing library 144
PART 3 THE TEST CODE 147
7 Trustworthy tests 149
7.1 How to know you trust a test 150
7.2 Why tests fail 150
A real bug has been uncovered in the production code 151
A buggy test gives a false failure 151
The test is out of date due to a change in functionality 152
The test conflicts with
another test 152
The test is flaky 153
7.3 Avoiding logic in unit tests 153
Logic in asserts: Creating dynamic expected values 153
Other forms of logic 155
Even more logic 156
7.4 Smelling a false sense of trust in passing tests 156
Tests that don’t assert anything 157
Not understanding the tests 157
Mixing unit tests and flaky integration tests 158
Testing multiple exit points 158
Tests that keep changing 160
7.5 Dealing with flaky tests 161
What can you do once you’ve found a flaky test? 163
Preventing flakiness in higher-level tests 163
8 Maintainability 165
8.1 Changes forced by failing tests 166
The test is not relevant or conflicts with another test 166
Changes in the production code’s API 166
Changes in other tests 169
8.2 Refactoring to increase maintainability 173
Avoid testing private or protected methods 173
Keep tests DRY 175
Avoid setup methods 175
Use parameterized tests to remove duplication 176
8.3 Avoid overspecification 177
Internal behavior overspecification with mocks 177
Exact outputs and ordering overspecification 179
PART 4 DESIGN AND PROCESS 185
9 Readability 187
9.1 Naming unit tests 188
9.2 Magic values and naming variables 189
9.3 Separating asserts from actions 190
9.4 Setting up and tearing down 191
10 Developing a testing strategy 194
10.1 Common test types and levels 195
Criteria for judging a test 196
Unit tests and component tests 196
Integration tests 197
API tests 198
E2E/UI isolated tests 198
E2E/UI system tests 199
10.2 Test-level antipatterns 199
The end-to-end-only antipattern 199
The low-level-only test antipattern 202
Disconnected low-level and high-level tests 204
10.3 Test recipes as a strategy 205
How to write a test recipe 205
When do I write and use a test recipe? 207
Rules for a test recipe 207
10.4 Managing delivery pipelines 208
Delivery vs. discovery pipelines 208
Test layer parallelization 209
11 Integrating unit testing into the organization 213
11.1 Steps to becoming an agent of change 213
Be prepared for the tough questions 214
Convince insiders: Champions and blockers 214
Identify possible starting points 215
11.2 Ways to succeed 216
Guerrilla implementation (bottom-up) 217
Convincing management (top-down) 217
Experiments as door openers 217
Get an outside champion 218
Make progress visible 219
Aim for specific goals, metrics, and KPIs 220
Realize that there will be hurdles 222
11.3 Ways to fail 222
Lack of a driving force 223
Lack of political support 223
Ad hoc implementations and first impressions 223
Lack of team support 224
11.4 Influence factors 224
11.5 Tough questions and answers 226
How much time will unit testing add to the current process? 226
Will my QA job be at risk because of unit testing? 227
Is there proof that unit testing helps? 228
Why is the QA department still finding bugs? 228
We have lots of code without tests: Where do we start? 228
What if we develop a combination of software and hardware? 229
How can we know we don’t have bugs in our tests? 229
Why do I need tests if my debugger shows that my code works? 229
What about TDD? 229
12 Working with legacy code 231
12.1 Where do you start adding tests? 232
12.2 Choosing a selection strategy 234
Pros and cons of the easy-first strategy 234
Pros and cons of the hard-first strategy 234
12.3 Writing integration tests before refactoring 235
Read Michael Feathers’ book on legacy code 236
Use CodeScene to investigate your production code 236
appendix Monkey-patching functions and modules 238
index 251
Unit testing is more than just a collection of tools and practices—it’s a state of mind! This bestseller reveals the master’s secrets for delivering robust, maintainable, and trustworthy code.
Thousands of developers have learned to hone their code quality under the tutelage of The Art of Unit Testing. This revised third edition updates an international bestseller to reflect modern development tools and practices, as well as to cover JavaScript.
Create readable, maintainable, and trustworthy tests
Work with fakes, stubs, mock objects, and isolation frameworks
Apply simple dependency injection techniques
Refactor legacy code with confidence
Test both frontend and backend code
Effective unit tests streamline your software development process and ensure you deliver consistent high-quality code every time. With practical examples in JavaScript and Node, this hands-on guide takes you from your very first unit tests all the way to comprehensive test suites, naming standards, and refactoring techniques. You’ll explore test patterns and organization, working with legacy code and even “untestable” code. The many tool-agnostic examples are presented in JavaScript and carefully designed so that they apply to code written in any language.
The art of unit testing is more than just learning the right collection of tools and practices. It’s about understanding what makes great tests tick, finding the right strategy for each unique situation, and knowing what to do when the testing process gets messy. This book delivers insights and advice that will transform the way you test your software.
The Art of Unit Testing, Third Edition shows you how to create readable and maintainable tests. It goes well beyond basic test creation into organization-wide test strategies, troubleshooting, working with legacy code, and “merciless” refactoring. You’ll love the practical examples and familiar scenarios that make testing come alive as you read. This third edition has been updated with techniques specific to object-oriented, functional, and modular coding styles. The examples use JavaScript.
Deciding on test types and strategies
Test Entry & Exit Points
Refactoring legacy code
Fakes, stubs, mock objects, and isolation frameworks
Object-Oriented, Functional, and Modular testing styles
Examples use JavaScript, TypeScript, and Node.js.