Unit testing with Angular and ineeda
Here at Trade Me, we’ve been working on a new Angular application to replace our current ASP.NET front-end! The application was originally implemented in AngularJS, but due to its below-par performance, we’ve been working hard to upgrade the codebase to the new, shiny version of the Angular framework.
There’s a significant amount of technical risk in doing a piece of work like this, but we are convinced that the benefits are worth the effort. Thankfully, we have a good spec, as we’re aiming for a like-for-like remake of the AngularJS app (with a few improvements 😎). We also have a fairly robust suite of automated tests including:
- Over 9000 unit/integration tests (and hundreds more over our component library)
- 114 Cucumber/Protractor UI/E2E tests made up of 344 individual scenarios
Having this degree of test coverage makes us confident that we can safely recreate the first version of the app, so being able to easily write (and maintain) tests is critical. We have some really exciting stuff to share about all our different test automation approaches, but for this post we’re going to focus our new auto-mocking tool ineeda!
ineeda lets us do stuff like this:
Neat aye? But before we get into the details of how ineeda works, let’s have a look at why we made it in the first place…
A unit test is a piece of code that tests the smallest possible part of a larger system. We write new unit tests when we are building features or fixing bugs, and we run them regularly to make sure we haven’t unexpectedly broken anything! If we have a failing unit test it means that something definitely broke. If they all pass, we can be reasonably confident that it’s okay for the work to move along to the next step in our Quality Assurance process.
Our unit testing stack uses:
- Karma — the de facto test runner for Angular, that allows us to runs our tests in a real browser,
- Jasmine — a testing framework that provides the
describe/itBDD-style test structure,
- Sinon — a powerful spying/stubbing library,
- Chai — an extensible assertion library for validating test expectations.
A simple unit test might look something like the following:
DateTimePipe is an ideal unit of code. It has no dependencies, and is “pure”, so it’s fairly easy to test its behaviour in isolation. More often than not, though, the code we want to test depends on other things, such as services, components, or models. That makes it more difficult to break down the code into a testable unit. Thankfully, the Angular framework is designed to make this easy for us! It uses a technique called Dependency Injection (DI).
Dependency Injection in Angular
Whenever we call
new Car() we get a new
Car back (surprise!), which is great, and works fine. But what happens if we want to make a car with a different type of engine, say, a hybrid? We have to change our
Car constructor to make a hybrid engine. What if we then want to make a fully electric car? You can see that this current implementation isn’t very flexible.
One way to improve things would be to flip it so that the
Car doesn’t make the engine, it expects to be given an
engine argument that has already been built. We can first define an
Engine class that defines what an engine must look like:
Then we change the
Car constructor so that it expects something that fulfils the
Now we can be more flexible!
We could then extend our
BetterCar to take different types of wheels, or stereos:
This is a rather silly example, but it exemplifies a pretty powerful idea. By passing in the dependencies from outside, we can be more flexible with how we make up larger things. Dependency Injection in Angular is controlled using Injection Tokens and Providers.
- An Injection Token is an object which is used to look up a dependency. In our above example the token would be the
- A Provider is a bit of configuration that tells Angular what to provide when given an Injection Token.
It is very rare to actually instantiate a dependency in Angular. In most situations you just ask about something and the framework will use the Provider configuration to work out what to give you. Let’s look at a more complex testing situation to see how these things play together:
Without going into too much detail of the implementation of
MemberSummaryComponent, we can infer from the test that it calls the
MemberService to get information about the member, and does some sort of calculation to get the
feedbackScore. That is totally reasonable, but a bit problematic from a testing point of view, in that it uses the real
MemberService. We need to have control over the member data for this test, so we need to be able to control what the
This is where the Injection Tokens and Providers come into play:
Now whenever Angular sees the
MemberService injection token, it uses the object declared in the
useValue field instead of the real
MemberService. That means our fake member object will be returned when
getMember() and we can therefore control the environment and data for our test. We have successfully turned this test into a unit test by providing a different, more simple implementation of the
For more detailed info on unit testing in Angular, check out this article by Pascal Precht, or this one by Gerard Sans.
This simple implementation is an example of what is known as a test double. Test doubles are often referred to as “stubs”, or “fakes”, but they can be generically described as “mocks”.
In our example above, we declared the mock directly in the test providers. That doesn’t work to well in practice, for a few reasons:
useValuemeans that all the tests will share the same mock object. That means the tests will share state, which is bad!
- Different components that use the same service will want to mock that service too.
- Different tests for that one component will likely want different data.
- We want to be sure our mock actually matches the type of the dependency we are providing for. The above example is completely untyped!
Our initial solution to that problem was to manually craft mock implementations of our dependencies. Here’s the mock implementation of the
There are a few interesting things here:
- Nested mocks —
public member: IMemberModel = new MemberModelMock()
- Methods throw errors by default —
throw new MockNotStubbedError()The string that is passed to the error is primarily to help when debugging tests, so you can see which method hasn’t been stubbed out.
- Default falsy values —
public isLoggedIn: boolean = false
How does this look when being used in a test? Here’s our
MemberSummaryComponent test again, this time using mock classes:
We’ve solved each of the problems we had before:
- We use
useValue. That means each test that runs will get a new instance of the mock class, so no more shared state (👏!)
- Our mock is now its own class, which means it is easy to share it with other tests (👏!)
- We’ve used
sinon.stubto specify how
getMembershould behave just for this one test. Each test can now control its own data (👏!)
- Because our mock implements the same interface as our real service, the types must match (👏!)
It’s not all sunshine and butterflies though. While there are some good ideas here, there are some new problems too. Changing the real implementation of the real service is now significantly more annoying. Add a property? You need to add it to the mock. Change a method name? You have to remember to update the error thrown by the mock too, or you’ll get lost trying to find a method that doesn’t exist anymore. And we have a lot of dependencies, so our AngularJS project had more than 200 mock implementations too! And quite frankly, it’s just too much code.
Overall, there’s too much manual effort involved, even though the payoff is reasonable. Can we do better? We thought so️!
Auto-mocking with ineeda
ineeda is our implementation of an auto-mocker, designed with Angular projects in mind, but still useful for non-Angular projects. ineeda uses the power of ES2015 Proxies to dynamically create an mock that can behave like anything you want! It’s been tested with Jest, Jasmine and Mocha, but should work in any JS testing stack. To top it off, an ineeda mock behaves almost identically to our hand-crafted mock classes from before:
- Nested mocks — An ineeda mock is a recursive proxy. That just means that no matter how many properties deep you go, there will always be another proxy waiting for you to manipulate.
- Methods throw errors by default — Every property on the mock is a callable function that throws an error by default.
For now, you just have to explicitly set falsy values. Luckily, it’s easy to do!
ineeda lets us override the proxy to return a value for a property:
It can easily handle nested complex values:
Sometimes you need to modify the mock after it’s been created. For that, you can use
intercept can also be used like the Proxy get trap. Any property access will run through the interceptor:
The inherent flexibility of an ineeda mock can be a bit dangerous though. If you make a typo, the proxy will happily keep going! We use ineeda with TypeScript to protect us from those kinds of issues:
Using ineeda with Angular
Now that we know a bit about this new tool, let’s go back to the
MemberService example. Instead of having to import the
MemberServiceMock, we can just use
ineeda.factory, which will create a new mock before each test:
We have essentially the same test, and in fact it’s become a little bit tidier! And we can now delete all those mock classes! Neat! 👍
We have some more ideas on where to take ineeda. For example:
- Can we use TypeScript runtime type metadata to provide better default values?
- What happens if we set up a global interceptor that automatically uses sinon to stub any function? (Hint — something good!)
- What would it take to make it so that we can have falsy values by default?
For now though, we’d love for you to check the project out on Github, try it in your projects, and share with us your thoughts, ideas, and any other feedback you might have!