In the past year at Ombud, the engineering team has been firing on all cylinders to reach our goal of becoming the premier intelligent content collaboration platform. The frontend team (all 5 of us!) has been particularly busy pushing out UI enhancements to our automated suggestions, redoing our authorization model, and updating how groups of users are uploaded into our system. At the same time that we have been providing value to the organization we have onboarded two new developers, had some key developers exit the organization, and fundamentally changed how we are QA’ing our own product.
Like any small team at a startup, we have gone through a lot of change and have been able to continue pushing reliable code and increasing our velocity through a renewed fixation on unit testing our code and creating an environment where all developers are invested in cultivating habits of good testing. At Ombud, we’ve definitely learned that unit testing is critical for effective frontend development. In this blog post, I’ll highlight the steps we took, tools we used, and processes we implemented to create a culture of unit testing that has fundamentally altered how we deliver software. If you are a developer on a small team struggling to start testing code (or have inherited an untested codebase) and need to kickstart unit testing habits across your team, this blog post is for you!
Why are tests important?
Before we can begin clarifying the ways to build testing habits on a small frontend team, it’s essential to state the reasons that testing is important and provides value in working collaboratively on a large codebase. First, comprehensive testing gives us confidence in refactoring previously written code. A well written test properly delineates the minimum expected behavior of a previously written function or module. It gives the individual assurance that their modifications or extensions of previously existing code conform to expectations already encoded in the system. This assertion that a developer has not ruptured existing functionality aids in limiting the number of regressions that are released from developers. Less regressions released creates happier clients and higher levels of trust towards the engineering group throughout the internal organization.
Another important reason that writing tests helps a smaller team move faster is that tests communicate product intentions on a technical implementation level. The descriptions of tests (when written well) contain valuable information about the expected behavior of the product itself. Take a look at the following two examples:
That second “it” statement is much more descriptive and will help a developer 3 or 6 months later who needs to update this code understand the exact intentions of the code written. The living record will help a newer developer get coding faster (and more effectively!) rather than having to track down the person who touched that piece of the code base. By getting the intentions behind code expressed through unit tests, the entire team is able to grow, learn, and push code faster and the team avoids having this information siloed within one individual.
Addressing blockers to writing tests
In order to start realizing the benefits of writing more unit tests, it was important that our team worked to remove the typical blockers that we ran into when starting to write tests. In setting up our new test writing framework, we concentrated on the following principles to make the test writing process as effortless as possible:
- Choosing an easy to learn testing utility for easy, readable querying and assertions against the DOM
- Figuring out easy ways to mock and manipulate application wide architectural components that many child components read from through React context or such as the redux store or the Material-UI ThemeProvider component
- Mock application wide data structures - at Ombud many older parts of the application are built on top of Immutable data structures, so we needed an efficient way to mock these with dynamic values to assert against (if you are building your application in TypeScript, you may already have your interfaces defined that you can easily create mocks from)
Based on those criteria, we ended up running with the following React testing tools:
- Test runner / framework - Jest
- Testing utility - React Testing Library (we actually transitioned away from using Enzyme due to React Testing Libraries’ API’s ease of use and how easier / effective it was to assert against the outputted HTML of a React component rather than the structure of a React component that is shallowly rendered by Enzyme).
- Mock Data Provider - faker.js
In terms of improving developer productivity during the test writing experience, the most impactful choice the team made was to actually mock our redux store and the MuiThemeProvider from Material-UI within the render function that @testing-library/react exports. This allowed for any time that we are testing with the render function like:
And that component (in this example EntityMenu) has direct dependency on either Redux through the connection function or the Material-UI themes via context, we won’t have to actually set up the mocks to external libraries within that test file. We accomplished this by following the direction on the React Testing Library website to set up a custom render. This step dramatically reduced code duplication and unneeded complexity in many of our individual tests.
Another way that we made it easier for developers to actually write tests was by investing time in creating factories that easily create mocked data structures for developers to easily use (and customize) in their own tests. For instance, within the Ombud platform, there is the concept of an EditorItem which is essentially the link between a piece of structured content and the slate js open source rich text editor that allows for a user to interact with that content in the browser. Here is an example of the factory for mocked EditorItems:
Creating habitual testing processes
Now that we had lessened the barriers to entry to writing tests by picking the most effective tools, configuring those tools effectively, and creating easy to use mocks, the next step was to actually integrate testing into different phases of our software development process. The most effective step that we took to introduce testing into our development process was simply by altering the template that we use for PR review. We actually all altered the template together in a single meeting after we had agreed that all PRs going forward would require some type of testing to be attached to them. By adding in additional fields for the developer to list out the types of tests that they have written, and doing it together to create a shared commitment as a team, we made the testing much more visible to all people involved in the PR review process. These are the fields that we added to our GitHub PR Template that help us all focus on testing during the PR Review process:
Creating that shared understanding of the importance of testing and integrating it into the PR review process were critical in creating new habits of writing effective tests.
Creating enduring testing habits on a small development team takes shared commitment and effort. At Ombud, we have had success by taking the approach outlined above. By picking the right tools, configuring them properly for our environment, initially investing in creating reusable mocked data, and integrating testing as a formal part of our PR process we have been able to write many more effective tests in the last 3 months. If you or your team are struggling with writing tests while continuing to provide value to your own organizations, I hope you can adopt the steps above (or come up with your own!) and start building positive unit testing habits.