One of the most challenging parts of testing is finding seams to reduce the scope of tests. Doing so is important because it make your tests smaller and cleaner which makes them more resilient to changes in the rest of your code base. Testing isn't helping you if every minor change breaks dozens of interconnected tests. Angular's heavy use of Observable provides us a great seam.
This post is part two of a series on angular testing. You can check out the other posts here:
- Keep the number of test bed tests to a minimum.
- Leverage
Observable
as a testing seam (this post) - Leverage monkey patching as a testing seam
- No matter what anybody says e2e tests still need sleeps
Reactive programming is a really nice paradigm which extend the Promise or Task patterns that have become quite popular over the last decade. Instead of returning a single value upon completion as Promises do an observable allows subscriptions which are handlers executed every time a value is returned from the observable.
I have seen a lot of people who ignore the complexity of observables by simply converting to a promise using toPromise
but this ignores some of the really cool things you can do with promises. For instances if I have a component that requires talking to multiple HTTP endpoints I'll zip the responses together so that the rendering doesn't happen until all the requests are complete. There are a ton of other cool patters you can use if you stick with observables.
Anyway, I'm not currently here to sell you on RxJS (it is awesome) but tell you how you can use observables to act as a good seam to limit the scope of your tests.
Let's look at a snippet of a component that makes use of observables and see how to shim in tests.
1 | constructor(private vendorService: VendorService, |
Here we have 4 observables which complete in various ways. The first, vendorService.get()
, simply assigns vendors to an existing variable. The devices observable does the same but also calls a function and, finally, the last two observables are synchronized via a zip operator. It looks like a lot is going on here but we can isolate things to test easily.
First up we want to test to make sure that whatever is returned by the vendor service is properly assigned to the vendors collection. We can us a combination of mocks and observables to focus just on the vendor service like so (I'm using ts-mockito's mocking syntax here):
1 | describe('Demo Component', () => |
As you can see we set up the mocks to return either an Observable
with a single result to test the code or with an empty result to never trigger the subscriptions to that observable. So even though the ngOnInit is quite complex the testing doesn't have to be.
Let's look at one more example for the zip case
1 | it('origins and destinations being complete should trigger setup', () => { |
You might also have equivalent tests to ensure that just completing one or the other of getOrigins
and getDestinations
doesn't cause the setup to be fired.
The crux of this post is that observables provide for a nice place to hook into tests because you can use them to isolate large chunks of subscription code or exercise that code with arbitrary values. The more seams you have the easier testing becomes.
I already gave away a bit of the third post in this series when I overrode the setup method in the last example: this is called monkey patching and it is slick beans for isolating code to test.