Unit testing with Angular and ineeda

Auto-mocking for TypeScript (and JavaScript!)

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.

The AngularJS version of the new Trade Me homepage viewed on a mobile device

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:

An example of using ineeda to make a mock Hero object.

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…

Unit testing

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/it BDD-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:

An example of a unit test — in this case for our DateTimePipe

The 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

There are lots of great resources that cover how DI works. As a quick refresher, let’s imagine we have a Car class:

The Car class. Not that it is responsible for creating its own engine.

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:

The Engine class. This class defines the general “shape” of an engine.

Then we change the Car constructor so that it expects something that fulfils the Engine interface:

The BetterCar class. It doesn’t create its engine, it just expects to be given an object that looks like an Engine.

Now we can be more flexible!

Creating several different types of car just by passing a different sub-type of Engine.

We could then extend our BetterCar to take different types of wheels, or stereos:

The BettererCar class. In addition to taking some kind of Engine, it also expects a Stereo and some Wheels.

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 Engine class (or Stereo or Wheels).
  • 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:

A test for the MemberSummaryComponent. It uses the real MemberService, and is therefore not an isolated unit test.

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 MemberService does.

This is where the Injection Tokens and Providers come into play:

This configuration tells Angular to provide the object declared in the “useValue” field instead of the real MemberService.

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 MemberSummaryComponent calls 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 MemberService.

For more detailed info on unit testing in Angular, check out this article by Pascal Precht, or this one by Gerard Sans.

Mocks

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:

  1. Using useValue means that all the tests will share the same mock object. That means the tests will share state, which is bad!
  2. Different components that use the same service will want to mock that service too.
  3. Different tests for that one component will likely want different data.
  4. 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 MemberService:

An example of a mock class. This is our mock implementation of the MemberService.

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:

A better test for the MemberSummaryComponent. It uses a mock implementation of the MemberService.

We’ve solved each of the problems we had before:

  1. We use useClass instead of useValue. That means each test that runs will get a new instance of the mock class, so no more shared state (👏!)
  2. Our mock is now its own class, which means it is easy to share it with other tests (👏!)
  3. We’ve used sinon.stub to specify how getMember should behave just for this one test. Each test can now control its own data (👏!)
  4. 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.
The basic behaviour of an ineeda mock is almost identical to our handcrafted mock classes.

The only behaviour of our custom mocks that we couldn’t reproduce is the falsy by default behaviour. It turns out, it’s currently impossible to do with JavaScript! Hopefully that changes in the future…

For now, you just have to explicitly set falsy values. Luckily, it’s easy to do!

Setting values

ineeda lets us override the proxy to return a value for a property:

Overriding simple properties. The rest of the proxy still behaves as normal.

It can easily handle nested complex values:

Overriding more complex values. Note that you can use an ineeda mock within another.

Sometimes you need to modify the mock after it’s been created. For that, you can use intercept:

Overriding additional values after the mock has been created by using “intercept”.

intercept can also be used like the Proxy get trap. Any property access will run through the interceptor:

“intercept” can also be used to intercept property access.

Adding types

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:

TypeScript makes it much safer to use ineeda by limiting the power of the underlying Proxy.

ineeda is powerful, but there isn’t that much code to it. If you want to know more about how it works, I suggest starting here.

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:

An even better test for the MemberSummaryComponent. It uses a ineeda mock instead of the real MemberService.

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! 👍

What’s next?

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!