📄 Page
1
Build Your Own Test Framework A Practical Guide to Writing Better Automated Tests — Daniel Irvine Foreword by Maaret Pyhäjärvi
📄 Page
2
Build Your Own Test Framework: A Practical Guide to Writing Better Automated Tests ISBN-13 (pbk): 978-1-4842-9246-4 ISBN-13 (electronic): 978-1-4842-9247-1 https://doi.org/10.1007/978-1-4842-9247-1 Copyright © 2023 by Daniel Irvine This work is subject to copyright. All rights are reserved by the Publisher, whether the whole or part of the material is concerned, specifically the rights of translation, reprinting, reuse of illustrations, recitation, broadcasting, reproduction on microfilms or in any other physical way, and transmission or information storage and retrieval, electronic adaptation, computer software, or by similar or dissimilar methodology now known or hereafter developed. Trademarked names, logos, and images may appear in this book. Rather than use a trademark symbol with every occurrence of a trademarked name, logo, or image we use the names, logos, and images only in an editorial fashion and to the benefit of the trademark owner, with no intention of infringement of the trademark. The use in this publication of trade names, trademarks, service marks, and similar terms, even if they are not identified as such, is not to be taken as an expression of opinion as to whether or not they are subject to proprietary rights. While the advice and information in this book are believed to be true and accurate at the date of publication, neither the authors nor the editors nor the publisher can accept any legal responsibility for any errors or omissions that may be made. The publisher makes no warranty, express or implied, with respect to the material contained herein. Managing Director, Apress Media LLC: Welmoed Spahr Acquisitions Editor: Divya Modi Development Editor: James Markham Coordinating Editor: Divya Modi Cover designed by eStudioCalamar Cover image designed by Pixabay Distributed to the book trade worldwide by Springer Science+Business Media New York, 1 New York Plaza, Suite 4600, New York, NY 10004-1562, USA. Phone 1-800-SPRINGER, fax (201) 348-4505, e-mail orders-ny@springer-sbm.com, or visit www.springeronline.com. For information on translations, please e-mail booktranslations@springernature.com; for reprint, paperback, or audio rights, please e-mail bookpermissions@springernature.com. Apress titles may be purchased in bulk for academic, corporate, or promotional use. eBook versions and licenses are also available for most titles. For more information, reference our Print and eBook Bulk Sales web page at http://www.apress.com/bulk-sales. Any source code or other supplementary material referenced by the author in this book is available to readers on GitHub (github.com/apress). For more detailed information, please visit http://www.apress.com/source-code. Printed on acid-free paper Daniel Irvine London, UK
📄 Page
3
iii About the Author ���������������������������������������������������������������������������������xi About the Technical Reviewer �����������������������������������������������������������xiii Acknowledgments ������������������������������������������������������������������������������xv Foreword ������������������������������������������������������������������������������������������xvii Introduction ���������������������������������������������������������������������������������������xix Part I: Building the Core of a Test Framework �����������������������������1 Chapter 1: Creating a Barebones Test Runner ��������������������������������������3 What Is an Entrypoint? ������������������������������������������������������������������������������������������3 The First Stage of Automated Test Enlightenment �������������������������������������������5 The Second Stage of Automated Test Enlightenment ��������������������������������������6 The Inner Workings of a Unit Test Runner �������������������������������������������������������������7 The Third Stage of Automated Test Enlightenment ������������������������������������������8 Cloning the Project Repository ����������������������������������������������������������������������������10 Creating an NPM Project for the Test Runner ������������������������������������������������������11 Setting Package Configuration ����������������������������������������������������������������������11 Creating the Test Runner Entrypoint �������������������������������������������������������������������12 Making the Package Available for Use ����������������������������������������������������������13 Using the Sample Application Project������������������������������������������������������������14 Update the Test Runner to Run Our Test Suite �����������������������������������������������15 Table of Contents
📄 Page
4
iv Verify That It Works by Breaking a Test ���������������������������������������������������������������17 Catch Exceptions from the Await Call ������������������������������������������������������������19 Committing to Git ������������������������������������������������������������������������������������������������20 Discussion Questions ������������������������������������������������������������������������������������������21 Summary�������������������������������������������������������������������������������������������������������������21 Chapter 2: Building a Function to Represent a Test Case ������������������23 The it Function ����������������������������������������������������������������������������������������������������24 Common Test Verbs ���������������������������������������������������������������������������������������25 Using the it Function to Group and Describe Test Cases �������������������������������������26 Handling Exceptions �������������������������������������������������������������������������������������������33 Printing Test Descriptions �����������������������������������������������������������������������������������34 Support CI with Correct Exit Codes ���������������������������������������������������������������������37 Summarizing a Test Run �������������������������������������������������������������������������������������39 Exercises �������������������������������������������������������������������������������������������������������������40 Summary�������������������������������������������������������������������������������������������������������������41 Chapter 3: Grouping Tests ������������������������������������������������������������������43 The Basics of Unit Test Organization �������������������������������������������������������������������43 A Starting Point for describe �������������������������������������������������������������������������������45 Rethinking Test Output ����������������������������������������������������������������������������������������46 Printing a Pass/Fail Response for Each Test��������������������������������������������������48 Ending a Test Report with Test Failure Details�����������������������������������������������49 Saving Test Context Information �������������������������������������������������������������������������52 Supporting Nested describe Blocks ��������������������������������������������������������������������54 Exercises �������������������������������������������������������������������������������������������������������������60 Summary�������������������������������������������������������������������������������������������������������������60 Table of ConTenTs
📄 Page
5
v Chapter 4: Promoting Conciseness with Shared Setup and Teardown ��������������������������������������������������������������������������������������������61 The Arrange, Act, Assert Pattern �������������������������������������������������������������������������62 Arrange ����������������������������������������������������������������������������������������������������������63 Act �����������������������������������������������������������������������������������������������������������������63 Assert ������������������������������������������������������������������������������������������������������������63 Why Is This Pattern Important? ���������������������������������������������������������������������64 Introducing the beforeEach Function ������������������������������������������������������������66 Applying beforeEach Blocks to Our Test �������������������������������������������������������������67 Refactoring describeStack to an Object ��������������������������������������������������������69 Defining beforeEach ��������������������������������������������������������������������������������������70 Updating the Sample Application ������������������������������������������������������������������������72 Defining afterEach ����������������������������������������������������������������������������������������������75 Generalizing beforeEach �������������������������������������������������������������������������������������75 Generalizing invokeBefores ��������������������������������������������������������������������������������76 Exercise ��������������������������������������������������������������������������������������������������������������77 Discussion Question ��������������������������������������������������������������������������������������������78 Summary�������������������������������������������������������������������������������������������������������������78 Chapter 5: Improving Legibility with Expectations and Matchers ������79 Using Matchers for Clarity and Reuse �����������������������������������������������������������������80 Building the First Matcher: toBeDefined �������������������������������������������������������������81 Creating an Error Subtype �����������������������������������������������������������������������������������88 Allowing Multiple Failures per Test ���������������������������������������������������������������������91 Making a Mess ����������������������������������������������������������������������������������������������������92 Testing Your Solution �������������������������������������������������������������������������������������95 Table of ConTenTs
📄 Page
6
vi Exercises �������������������������������������������������������������������������������������������������������������96 Discussion Questions ������������������������������������������������������������������������������������������97 Summary�������������������������������������������������������������������������������������������������������������98 Part II: Constructing a Usable Framework ���������������������������������99 Chapter 6: Formatting Expectation Errors ����������������������������������������101 Utilizing Stack Traces with Testing Workflows ��������������������������������������������������101 Building stackTraceFormatter ���������������������������������������������������������������������������105 Joining Up Our Formatter with the Runner �������������������������������������������������������115 Exercises �����������������������������������������������������������������������������������������������������������117 Discussion Questions ����������������������������������������������������������������������������������������117 Summary�����������������������������������������������������������������������������������������������������������118 Chapter 7: Automatically Discovering Test Files ������������������������������119 Development Workflows and Test Runners �������������������������������������������������������119 What About Watch Mode? ����������������������������������������������������������������������������121 Discovering Files �����������������������������������������������������������������������������������������������122 Running a Single File ����������������������������������������������������������������������������������������124 Exercises �����������������������������������������������������������������������������������������������������������126 Discussion Question ������������������������������������������������������������������������������������������127 Summary�����������������������������������������������������������������������������������������������������������128 Chapter 8: Focusing on Specific Tests ����������������������������������������������129 The Refactor Workflow ��������������������������������������������������������������������������������������130 Introducing a Parse Step �����������������������������������������������������������������������������������130 Implementing the Parse Phase �������������������������������������������������������������������������131 Parsing the describe Function ���������������������������������������������������������������������132 Adding the it Function ���������������������������������������������������������������������������������134 Adding the beforeEach and afterEach Functions ����������������������������������������135 Table of ConTenTs
📄 Page
7
vii Implementing the Run Phase ����������������������������������������������������������������������������136 The Global currentTest Variable �������������������������������������������������������������������138 Updating the Runner �����������������������������������������������������������������������������������������139 Moving the expect Function ������������������������������������������������������������������������������141 Adding the Focus Functions ������������������������������������������������������������������������������142 Filtering Tests ����������������������������������������������������������������������������������������������������143 Exercises �����������������������������������������������������������������������������������������������������������145 Discussion Question ������������������������������������������������������������������������������������������145 Summary�����������������������������������������������������������������������������������������������������������146 Chapter 9: Supporting Asynchronous Tests ��������������������������������������147 Event Loops in Runtime Environments �������������������������������������������������������������148 Synchronicity and the Test Runner ��������������������������������������������������������������149 Waiting for Promise Completion ������������������������������������������������������������������150 Testing It Out �����������������������������������������������������������������������������������������������153 Catching Exceptions ������������������������������������������������������������������������������������153 Timing Out Tests After a Period of Time with the it�timesOutAfter Modifier ����� 154 Testing It Out �����������������������������������������������������������������������������������������������157 Exercise ������������������������������������������������������������������������������������������������������������158 Discussion Questions ����������������������������������������������������������������������������������������158 Summary�����������������������������������������������������������������������������������������������������������159 Chapter 10: Reporting ����������������������������������������������������������������������161 The Observer Pattern, in a Half-Baked Style �����������������������������������������������������161 Adding an Event Dispatcher ������������������������������������������������������������������������162 Dispatching a Begin Suite Event �����������������������������������������������������������������164 Dispatching a Test Finished Event ���������������������������������������������������������������166 Exercises �����������������������������������������������������������������������������������������������������������172 Summary�����������������������������������������������������������������������������������������������������������173 Table of ConTenTs
📄 Page
8
viii Part III: Extending for Power Users �����������������������������������������175 Chapter 11: Shared Examples ����������������������������������������������������������177 Methods of Reuse ���������������������������������������������������������������������������������������������177 Implementing a Shared Example Repository�����������������������������������������������180 Importing Shared Examples ������������������������������������������������������������������������184 Exercise ������������������������������������������������������������������������������������������������������������186 Discussion Questions ����������������������������������������������������������������������������������������186 Summary�����������������������������������������������������������������������������������������������������������186 Chapter 12: Tagging Tests ����������������������������������������������������������������187 Another Way of Slicing Tests �����������������������������������������������������������������������������188 Thinking Through Design Options ����������������������������������������������������������������189 Supporting Polymorphic Calls to Test Functions �����������������������������������������192 Filtering Test Runs Using the tags Property �������������������������������������������������196 Reading Tags from the Process Arguments �������������������������������������������������198 Adding Tags to the Sample Application �������������������������������������������������������199 Exercises �����������������������������������������������������������������������������������������������������������200 Discussion Question ������������������������������������������������������������������������������������������200 Summary�����������������������������������������������������������������������������������������������������������200 Chapter 13: Skipping Tests ���������������������������������������������������������������203 Taming the Test Runner ������������������������������������������������������������������������������������203 Adding the it�skip Modifier ��������������������������������������������������������������������������������205 Testing It Out �����������������������������������������������������������������������������������������������������206 Supporting Empty-Body describe and it Calls ���������������������������������������������������208 Exercises �����������������������������������������������������������������������������������������������������������209 Discussion Question ������������������������������������������������������������������������������������������210 Summary�����������������������������������������������������������������������������������������������������������210 Table of ConTenTs
📄 Page
9
ix Chapter 14: Randomizing Tests ��������������������������������������������������������211 Test Runner Use Cases �������������������������������������������������������������������������������������212 Randomizing Tests in CI Environments ��������������������������������������������������������212 Randomizing Tests on Developer Machines ������������������������������������������������213 Adding the Flag �������������������������������������������������������������������������������������������������213 Testing It Out �����������������������������������������������������������������������������������������������215 Exercises �����������������������������������������������������������������������������������������������������������216 Discussion Question ������������������������������������������������������������������������������������������216 Summary�����������������������������������������������������������������������������������������������������������216 Part IV: Test Doubles and Module Mocks ���������������������������������219 Chapter 15: Deep Equality and Constraining Matchers ��������������������221 How Are Constraining Functions Useful? ����������������������������������������������������������222 Implementing the equals Function��������������������������������������������������������������������223 Checking if Two Arrays Have Equal Size ������������������������������������������������������224 Checking if Two Objects Have Equal Keys ���������������������������������������������������224 Recursively Checking Element Equality ������������������������������������������������������225 Implementing the contains Constraint ��������������������������������������������������������������226 Exercises �����������������������������������������������������������������������������������������������������������228 Discussion Questions ����������������������������������������������������������������������������������������229 Summary�����������������������������������������������������������������������������������������������������������230 Chapter 16: Test Doubles ������������������������������������������������������������������231 How Are Test Doubles Useful? ��������������������������������������������������������������������������231 Spies and Stubs ������������������������������������������������������������������������������������������������232 Adding the New Tests ���������������������������������������������������������������������������������������234 Implementing the spy Function �������������������������������������������������������������������������236 Implementing the toBeCalledWith Matcher ������������������������������������������������������237 Table of ConTenTs
📄 Page
10
x Exercises �����������������������������������������������������������������������������������������������������������238 Discussion Questions ����������������������������������������������������������������������������������������239 Summary�����������������������������������������������������������������������������������������������������������240 Chapter 17: Module Mocks ���������������������������������������������������������������241 How Are Module Mocks Useful? �����������������������������������������������������������������������242 The Module Mock API ����������������������������������������������������������������������������������������243 Implementing the Mock Registry ����������������������������������������������������������������������245 Implementing the Node Loader �������������������������������������������������������������������������246 Adding the New Bin Script ��������������������������������������������������������������������������249 Bypassing the Module Cache ����������������������������������������������������������������������������250 Creating a Forwarding Reporter ������������������������������������������������������������������252 Defining the Worker Thread Entrypoint ��������������������������������������������������������253 Updating the Test Runner to Use Worker Threads ���������������������������������������254 Changing Reported Test Objects to Be Serializable �������������������������������������257 Exercises �����������������������������������������������������������������������������������������������������������259 Discussion Question ������������������������������������������������������������������������������������������259 Summary�����������������������������������������������������������������������������������������������������������260 Index �������������������������������������������������������������������������������������������������261 Table of ConTenTs
📄 Page
11
xi About the Author Daniel Irvine is a freelance software developer and technical coach. He specializes in simplifying software codebases and improving the technical confidence of dev teams. His first exposure to automated testing was in 2005, when he was tasked with writing Ruby test suites for a million-line C++ application. Since then, he’s been an advocate for developer- led testing practices. He is the author of Mastering React Test-Driven Development, now in its second edition. He can be contacted via his website at www.danielirvine.com.
📄 Page
12
xiii About the Technical Reviewer Sourabh Mishra is an entrepreneur, developer, speaker, author, corporate trainer, and animator. He is a Microsoft guy; he is very passionate about Microsoft technologies and a true .Net Warrior. Sourabh started his career when he was just 15 years old. He’s loved computers from childhood. His programming experience includes C/C++, ASP.Net, C#, VB.net, WCF, SQL Server, Entity Framework, MVC, Web API, Azure, jQuery, Highcharts, and Angular. Sourabh has been awarded a Most Valuable Professional (MVP) status. He has the zeal to learn new technologies and shares his knowledge on several online community forums. He is the author of Practical Highcharts with Angular (Apress, 2020), which talks about how you can develop stunning and interactive dashboards using Highcharts with Angular. He is the founder of “IECE Digital” and “Sourabh Mishra Notes,” an online knowledge-sharing platform where one can learn new technologies very easily and comfortably. He can be reached via the following platforms: • YouTube: sourabhmishranotes • Twitter: sourabh_mishra1 • Facebook: facebook.com/sourabhmishranotes • Instagram: sourabhmishranotes • Email: sourabh_mishra1@hotmail.com You can find his books on Amazon via www.amazon.com/stores/ author/B084DMG1WG.
📄 Page
13
xv Acknowledgments I would like to thank all the readers of the original Leanpub version of the book who have made this new edition possible. The whole team at Apress have been wonderful and made the process an absolute breeze. Particular thanks go to Divya Modi, the lead editor, and Shobana Srinivasan, the project coordinator, both of whom were extremely patient with me as I juggled tasks. Finally, I am indebted to all of my past and present clients, who have made my own journey to expertise possible.
📄 Page
14
xvii Foreword My first thought on Build Your Own Test Framework was curious: Why would I want to? The experience I come from is that there’s plenty of test frameworks to go around, and endless conversations on which of those is worth choosing, even more so among those working in the software testing space. I found my answer to this question reading this book. Moving from choosing a test framework to building a test framework showed me a great way of leveling learning in the tests space. It guides focus on what tests are for, what features are useful for tests, what features are generally available, and how we might build them. Daniel Irvine does a brilliant job at leveling this learning for the reader. Reinventing a wheel, you learn a lot about wheels. This is exactly what we need. Breaking a slightly abstract tool like a test framework into features and walking us through design and implementation helps us not only build a test framework but a foundation from which we can contribute on other test frameworks and understand our own tests better. Work through the chapters and code along and I’m sure you will enjoy features such as it.behavesLike to an extent you start missing the concept in frameworks you may have run into and are inspired to extend your own framework—and build your own for practice if not production use. Maaret Pyhäjärvi Principal Test Engineer at Vaisala Oyj
📄 Page
15
xix Introduction This book is a follow-along practical exercise in building an automated unit test framework, written in the JavaScript language and running on the Node platform. You can think of it as a less functional replacement for well-known packages like Jest,1 Mocha,2 Jasmine,3 and Vitest.4 The framework is called concise-test, and it takes the form of an NPM package that you’ll start building in Chapter 1. The book also makes use of a package named todo-example, which is a sample application that will make use of concise-test, as a package dependency. By the end of Chapter 1, you’ll be able to run the command npm test in your todo-example project directory and have your test runner execute the application test scripts. Do I Need to Know JavaScript? You’ll need to know modern JavaScript syntax to use this book, including arrow functions,5 destructuring assignments,6 and rest parameters.7 It also uses ECMAScript Modules8 for importing and exporting values. 1 https://jestjs.io 2 https://mochajs.org 3 https://jasmine.github.io 4 https://vitest.dev 5 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ Functions/Arrow_functions 6 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ Operators/Destructuring_assignment 7 https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/ Functions/rest_parameters 8 https://nodejs.org/api/esm.html#modules-ecmascript-modules
📄 Page
16
xx That being said, don’t stop reading if you don’t know any JavaScript; as long as you have experience in at least one programming language, you should be able to pick it up as we go along. If you’re unsure what some of the syntax means, take a moment to stop and look it up online. JavaScript is a wonderfully flexible language. It is dynamic and functional at its core, but also has support for objects in a rather unique way, which is empowering. The beauty of unit testing is that it is universally applicable to all mainstream programming languages, so if you were really keen, you could rebuild all of the same code in your own favorite language. The only chapters that are directly related to JavaScript are Chapters 9 and 17. Is This Book for You? If you’ve ever written unit tests, then, yes. If you’re a novice, this book is a wealth of practical advice that will give you highly valuable skills, helping you to be a more productive developer. If you’ve got plenty of automated testing experience already, the breadth of this book means that there are likely to be new ideas for you to uncover. In particular, the discussion questions will help you question your own biases and opinions. I encourage you to get together with a group of colleagues to complete the practical exercises and work through the discussion questions. Although the book is designed to be followed in order, the chapters are self- contained enough that you can pick and choose where to stop and start. How the Book Is Structured There are 17 chapters in this book. Each chapter of this book follows a similar layout: beginning with some detailed theory of unit testing, continuing with a bit of follow-along coding, and ending with a set of practical exercises and discussion questions. InTroduCTIon
📄 Page
17
xxi What we’re building here is a spike. That means that, ironically enough, there are no tests for what we’re about to build. Spikes help us quickly explore a problem space without getting too bogged down in plumbing code. The following diagram shows the full extent of the system we’re going to build. In Part 1, “Building the Core of a Test Framework,” you will build a barebones implementation of a test runner that features all the major components of an xUnit-style test runner. In Part 2, “Constructing a Usable Framework,” you extend the test framework to include ergonomic features that make the concise-test runner into a viable competitor to mainstream test frameworks. In Part 3, “Extending for Power Users,” we add some unique features that are borrowed from other programming environments. In Part 4, “Test Doubles and Module Mocks,” we end the book with a look at unit testing’s most misunderstood feature, the humble test double. InTroduCTIon
📄 Page
18
xxii What Is an Automated Unit Test Framework? Let’s start with automated: we are talking about the opposite of manual testing. Manual testing is when you spin up your application and use it: clicking around, entering data, observing results. Manual testing is time consuming and error prone. Automated testing, on the other hand, should be very quick and give the same results every time. Unit test is harder to define. Very often people think of them as tests for specific classes or objects within your application code, which are the “unit.” But this isn’t a rule. It’s just a tendency that unit tests have. And each unit test tends to be small and exercise just a tiny sliver of functionality. And we tend to end up with a whole lot of unit tests that together exercise the entire system. For me, the key defining point about unit tests is that they do not instrument any behavior external to the application process, like performing or relying on network requests. For many developers that also means file system access or database access. The benefit to avoiding external behavior is that it keeps the tests extremely fast and keeps tests behaving consistently across test runs. Finally, a framework means that it’s more than a library. In this case, a test framework consists of two things: the test runner and the test API. To explain what those are, let’s look specifically at what we’ll build in this book. What You’ll Need to Get Started You’ll need to install a recent version of Node, instructions for which can be found at https://nodejs.org. You’ll need at least version 18.12. You’ll need to have Git installed to be able to access the source code repository. If you’re new to Git, a good place to start is the website InTroduCTIon
📄 Page
19
xxiii https://github.com, which is also where the source code for this book is stored. You will also need basic command-line knowledge, like how to navigate between directories with the cd command and how to use the git and npm commands. Working with the Source Code Repository You can find the code repository at the following URL: github.com/Apress/Build-your-Own-Test-Framework-by- Daniel-Irvine You will find a directory for each chapter, from Chapter01 to Chapter17. Within each of these directories, there are two subdirectories: • A Start directory, which is the starting point for each chapter, which you can use if you want to follow along with the text in the book • An Exercises directory, which is the end of each chapter text, but before any of the exercises have been completed In each of these, you’ll find two further subdirectories: • A concise-test directory, which stores the concise- test package • A todo-example directory, which stores the todo- example package Initializing Projects for Each Chapter Every time you switch into a new chapter location, you will need to link the two packages together. This is described in detail in Chapter 1, InTroduCTIon
📄 Page
20
xxiv but the essence of it is the following commands that you’d enter on the command line: cd Chapter01/Start # choose your starting point here cd concise-test npm link cd ../todo-example npm link concise-test That will correctly set up the todo-example package dependency so that when you type the npm test command, you are executing code from its sibling concise-test directory. Simultaneously Working in Two Packages Because this book involves two dependent packages, you should take a moment to think about your development environment. Whatever editor or IDE you choose, make sure you can easily open files across both project directories and have easy access to a command shell so that you can enter the npm test command (you’ll be running that one a whole lot). InTroduCTIon