When I first started unit testing with AngularJS, Karma, and Jasmine, I read everything I could get my hands on about the subject. While there’s a lot of good material out there, I wouldn’t want to write tests, let alone ask my team to do so based on the material I originally found. This post is the one I wish I’d found when I first started writing tests, less of a “just how to get it working” and more of a “how to make writing tests a sustainable part of your process.” Some of these tips seem relatively trivial, but even little things can add up to make writing tests easier for a team.
Angular.mock.dump() and karma.dump()
One thing that is hard about unit testing with Karma is it’s not that practical to set break points. When I first started doing unit testing, I didn’t even know that you could use console.log() to print out useful messages from the tests and code.
When I figured that out, it helped quite a bit. However, complex objects could get hard to read and sometimes even would be truncated. It turns out that angular.mock has a method, dump() that will format the object to be legible from the command prompt window. Karma also has a dump() function that’s basically a shortcut to console.log().
Setting flags to only print information from certain tests
One thing that’s a little scary about using angular.mock.dump() in the code under test is that you’re going to get an error when you try to run that code and it’s not from a test. In addition, if a lot of tests run a particular piece of code, it can be difficult finding the output that matters to the test you’re working on from a firehose of output statements.
I like to give my AngularJS “classes” a method that sets a flag to turn these statements on and off. I usually call it setDebug(), and then I will call that from the test I’m currently working on (or in a beforeEach, depending on how early I need it set).
Shared Data Sources
It’s pretty common for multiple Services and Controllers to need similar data sets in their tests, so the data sets are easier to use and maintain if they are not kept in each spec, which is what you’ll see in most test examples on the internet. The biggest thing that has made testing sustainable for my team is to develop a convention for our data sources, both in how they’re named and how they’re structured.
The service name will usually be “myDataTypeData”. In turn, the mocks that are mocking services that retrieve those data sets can be given a path of “myDataType” and will return the data from the myDataTypeData service by asking for it from the injector.
Each data service has a method, “getAllData,” that returns converted json from an entire XML file, including a collection of data items with some header information that pertains to the collection as a whole. It also has a “getNormalizedDataItems” method that returns the collection of data items pre-processed to remove the XML artifacts. So tests that are testing large pieces of the application and tests that are testing smaller pieces of the application can get the data they need.
Balancing DAMP with DRY
Most of us have heard of DRY (Don’t Repeat Yourself) for production code, which means that rather than duplicating code to other places that need similar functionality, you extract that code and call it from all the different places that need it. Unit tests often are running almost the same code, but with slightly different inputs. One of the purposes of tests is to document what the software should do, so developers need to be able to see at a glance what the inputs are, where they are going, and what the expected outputs are. So tests are often written to be DAMP (Descriptive and Meaningful Phrases), which can translate to a lot of repeated code.
If you’ve ever wanted to make a major change in your code and felt that sinking feeling because you knew that too many places referenced it directly, you understand why DRY is a good thing. Change things in one place and you’re done. It can be hard to make a business case for tests to non-technical managers, so the last thing you ever want to say is “yes, we can make that change but it will take a while because there are a lot of tests around that functionality.”
After being bitten by that one, I developed a strategy that works in part because of my shared data sources. Most Jasmine specs have a setup() method. The first argument will usually be the data source name. If we’re testing at the individual data item level, the second argument will be the index of the data item I want.
Here’s where it gets tricky, because the setup step is usually going to instantiate something, whether it’s $controller building a controller for me to test with the data item attached to its $scope or a Factory (not an angular factory, specifically, but the Factory pattern) that is responsible for turning that data item into something else. So customization often needs to happen in between when we retrieve the data from the service and when we construct the item.
To handle that, I use a third parameter that takes a function to perform whatever extra massaging I need to customize the data before the actual object gets made. This means that once you know the pattern, you can read setup(‘someType’, 2, function (dataItem) dataItem.foo=’bar’}), and know exactly what is going on. In most cases, it’s not critical to see all the steps to retrieve the data and make the thing, with all the $rootScope.apply() calls that keep angular happy. What you care about is what’s special about this data as an input.
The flip side of that is that most examples you’ll see on blogs, etc., have a beforeEach() method at the top of the spec that pulls absolutely everything you’ll need from the $injector and stores it in a variable. This can get kind of hard to read, especially if for whatever reason you don’t use the actual name of the service as the variable name. And of course there’s that little part of my developer soul that is slightly bothered by exposing a variable for an entire spec that is only used in one place.
It took me a while to realize it, but you can use inject() any place that takes a Function in a spec. So, you can put an inject() in any beforeEach, not just the first one, or as the second parameter for a describe() or it() statement. Probably the only thing stopping you from injecting at the top-level describe block and simplifying your variable declarations is the need to bootstrap the module(s) under test.
When I took over the project I am currently leading, it was basically a prototype grown out of control. It had no tests and few modules. Our first priority was to start writing tests on new code, and then we started breaking out modules based on features. But our tests all bootstrapped the full app. The tests began to take longer and longer to run, so we knew that had to stop. Our first efforts at just bootstrapping a few modules in tests broke the tests pretty hard, so we put that task on the back burner while we continued to think about it.
Finally, it occurred to me that we had made a bad assumption when we decided what to leave in the main “app” module. It seemed logical at the time to leave pieces that were used in a lot of places in the app in the app itself. That was exactly backward, because any test that tested anything that depended on any of these commonly-used services had to bootstrap the app, which bootstraps everything. So the lesson learned there is “the more places it’s used, the more it needs to be in its own module.”
We also added a “mocks” module, which contains overrides of some of the services that we don’t want to really use in tests (like the one that plays audio or the one that reads the XML into JSON). By doing it this way, we can choose the real service simply by not bootstrapping “mocks.” The exception to this is sometimes you have to bootstrap the module the service is actually in, and that module can “win.” To resolve that situation, you can register a mock service in the module where the original service was, which will overwrite it, and also register it in the mocks module.
What are your tips?
I by no means claim that these tips and tricks are the best way to do things, but they are things I have found out for myself through trial and error that I have found helpful. Maybe you’ll find them helpful. If you have some killer ways to rock your jasmine tests, please feel free to share them in the comments.