Developers Can Remove Barriers From Testing Code and Gain Team Buy-in
Blog Barista: Jim Rasche | July 10, 2019 | Web Development | Brew time: 10 min
What if I told you it took 10 minutes to create a test suite for an Angular component? How likely would you be to write unit tests? I contend that with the adoption of just a few basic testing patterns any team developing for Angular can make this a reality.
Although it’s not the only way to run unit tests in Angular, I’ve found that using Karma gives developers a productive testing environment. So, that is what this post will be using. The testing patterns described here are very particular to a style of testing my team has found to be helpful. This style is a form of front-end integration tests that span from the DOM manipulation to API calls. This approach tests a large swath of the application and has proven, over thousands of runs through our CI process, to be very stable while giving excellent feature coverage.
Please note that this post is setup a little differently than our previous blog posts. Each hyperlink within this post will direct you to the exact line of code needed to understand the example. It will allow you to see the bigger picture (the whole code) and where each code example is within the bigger picture.
Wrap the Component Under Test
Surrounding the component under test with a “dummy component” allows our test cases to invoke the Angular lifecycle hooks. As these events (e.g. ngOnInit, ngOnChanges) are primary sources of application logic it is important to test them too.
In this example, we are:
- Invoking the component under test in the host’s template
- Setting up a view child to enable easy access the component
- Injecting our component’s services that control access to global data in order to mock them later.
- Setting up a hook setupTestData for our test cases to modify the inputs being passed into the component
Build Testing Module
Angular’s modular dependency structure sets us up for a conflict between making our tests fast and durable. We want to ensure all dependencies are available when Karma builds our component in isolation. However, the more dependencies we add into this isolated testing module, the longer it will take to compile. Our new approach is to tightly couple our testing module to the module exposing our component. This ensures the test will fail only if our component’s module doesn’t have something it explicitly needs. This process takes some deliberate architecting to ensure module size is as small as possible because any unneeded dependency will add to the test run time.
Here, the module is exposing the entire configuration object through a static method. This config is used in our test suite and eventually by our testing abstraction to build the testing module, but more on that later.
To test real world scenarios, it is important to use real-world data. As a standard, we keep our testing data files in the same folder as the test files and simply add a .data.ts after the file name. Regardless of how the component gets its data (inputs, services, or route), we like to test all the logic between data request and page rendering. This means either mocking out the function making the request or answering the request with a response using the methods in HttpTestingController.
Delegate to an Abstraction
This step will likely be the most time consuming and most rewarding. The goal here is to make the interaction with common component elements (edit buttons, toggles, inputs) simple and declaring our expectations of how the component behaves (element exists, input is read only) easy. We are building a common language that allows us to turn these 6 lines of code into just a single call and for that line to be used in test cases throughout our application.
Specifically, we have two application abstractions that we cover pretty thoroughly with testing abstractions, for example:
- Data grids have methods to compare rows of data, click row buttons, or make expectations about a row
- Form methods allow one to expect inputs to exist and have certain values, expect inputs to be in certain states, or set input values
These methods allow our tests to be more simple and clear.
These 4 steps helped make testing like less of a boogy man and more of a useful tool. Wrapping the component allowed us to test components as they run in our app. Using our component’s module to build the testing module made tests faster and less brittle. Mock data extended the coverage of our test case to all front end code and building an abstraction simplified how we write test cases. While it still might be hard to get buy-in on writing test cases from everyone on the team, this will at least give them less of a reason to not do it.
Other recent posts:
Blog Barista: Anthony Wolf | May 20, 2020 | Development Practices | Brew time: 6 min
Thinking About Your Data Model
I often read code in forums or Stack Overflow from people who are beginners at C#, and see them using FirstOrDefault in every situation where they need a single item from an IEnumerable. If I ask them why they made this choice, the reply is typically something like “it always works” or…
Blog Barista: Jonathan Nicholson | May 6, 2020 | Privacy & Security | Brew time: 7 min
There are many things that you can do to slightly increase your privacy in this digital age. A lot can be accomplished without being too extreme like swearing off all social media, self-encrypting all of your emails, and using Tor—a software tool for…