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.
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…
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:
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:
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 Engine
interface:
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
Engine
class (orStereo
orWheels
). - 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 MemberService
does.
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 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:
- Using
useValue
means 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 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:
We’ve solved each of the problems we had before:
- We use
useClass
instead ofuseValue
. 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.stub
to specify howgetMember
should 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.
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:
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
:
intercept
can also be used like the Proxy get trap. Any property access will run through the interceptor:
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:
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:
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!