Unit Testing Angular: TestBed Considered Harmful

Marko Bjelac
6 min readMay 31, 2019

I already see it —another one of my “short replies” to a Medium article (this one) morphed into a huge article in itself. Oh well…

The article on simplifying Angular unit testing is very well done. I applaud articles explaining testing techniques, especially when TDD is encouraged. This one does a good job of explaining a pattern for testing Angular components. The only problem is — it goes in a wrong direction.

The article proposes we isolate the component in order to be able to write a unit test for it (possibly even before we start implementing the component, so we can test-drive it). This is the correct approach. However, it goes on describing the standard Angular component testing scheme, i.e. using TestBed. Here is what that looks like:

Testing architecture using Angular TestBed

Lets explore this.

Testing components is challenging because components continuously balance on the line between logic and periphery.

Wait… What?

The Zones

Every software system has two “zones”:

  • logic: this is where behaviour happens and where all the “functional” features live
  • periphery: this is where our system connects to the real world — other software (services, files, network, database) & human interface contraptions (screen, keyboard, mouse, etc.)

In production, the logic is always hidden behind a wall of periphery. We see only the results it leaves on the periphery — which are typically reactions to inputs coming in through the periphery.

See a more in-depth explanation from Kevlin Henney’s talk about the Structure and Interpretation of Test Cases.

UI components live in both zones.

You are here! Between logic and periphery — but actually within both.

Problems

When you test both the template and the class together in one test you are:

  • doomed to slow tests (TestBed takes time to boot up and wire the test module) — this is especially bad for TDD since you want to run all your tests every 20–30 seconds
  • not really testing one thing (the component logic) but also Angular functionality itself — in order for your test to be a unit test (i.e. a useful tool pinpointing the cause of the problem when it occurs), the scope of things that can fail the tests needs to be small
  • coupling your tests to a 3rd party framework, making it difficult to refactor (for example — to move the behaviour deeper into the architecture where it is no longer in a @Component)

The only tests which should cover code in both zones are acceptance tests — they test the whole system and do not isolate parts of it. They isolate the entire system from all external parties (those that are connected through the periphery), but both the logic and the entire periphery are in scope of the acceptance test. (Except when you’re using subcutaneous acceptance tests, in which case you’re faking out the entire periphery, but that’s for another article.)

Periphery and logic should be tested separately:

  • logic should be tested with microtests (tests which test very small pieces of code like individual classes and/or functions)
  • periphery should be tested with integration tests — because periphery is used to integrate the system with the outside world

Lets first see about the first part because its easier to do and maybe even the only necessary (provided we have the above mentioned acceptance tests).

Testing a Component

This is how we should test an Angular component in isolation:

Testing an Angular component — class-only

We treat the component class as a plain Typescript class.

We can minimise the amount of logic in the template to push as much code as we can under the microtest. Practically — only *ngIfs and *ngFors can be left.

This solves the above mentioned problems:

  • these microtests are much, much faster then TestBed tests
  • we are testing the component’s immediate behaviour and nothing else, which gives us a clean & readable test and an accurate pinpoint when something goes wrong
  • the test is not coupled to any library or framework so it can freely move with the code it is testing

But who tests the template code?

This approach doesn’t test whether your component template is wired to the component class correctly.

There are three solutions:

1. Acceptance tests

If your acceptance tests are not subcutaneous (i.e. they’re testing a deployed system through its public API or UI) then the tests are necessarily also testing the periphery.

In the case of Angular, the tests are using some library like Selenium to check actual HTML elements and simulate clicks, scrolls & key typing.

This is not ideal since whenever there’s a bug in the template, only the acceptance tests will fail signifying that something is wrong with the behaviour, and actually its just the template wiring.

Periphery is covered with acceptance tests

2. Layer isolation

It is possible to design your system with the presentation layer so decoupled from the behaviour layer that the system can be deployed on a test environment with the entire behaviour layer substituted with a faked one.

The fake behaviour layer is not just supplying dummy data for rendering, it is also storing the event forwarding from the template so that can also be checked.

The library used for interaction with the periphery in this case (i.e. web UI) can also be Selenium or similar — we still cannot use TestBed here since that will not cover the entire periphery. Only deploying the system and using it as our customers are going to (i.e. with a browser) covers the whole periphery and we truly test the entire thing between customer and behaviour.

Testing only the periphery

3. No automated tests

A trivial solution, but currently my favourite.

If your templates are really thin because you pushed all possible logic down into the class (which is micro-tested), there’s not much to test. You can manually test the component in your browser to see if it works, or better yet — design the template using Storybook.

Storybook offers a couple of benefits:

  • you can build your template in a kind of TDD style — with machine checking replaced by your eyes
  • the component is designed in isolation from the rest of the system, forcing you to think about its API (similar to what TDD does)
  • the component’s story remains in your source code — so it can be used as a manual for developers re-using your component and also as a showcase of components for your customers

Generally…

All this applies to any other UI framework and its test tools (React’s Enzyme, I’m looking at you).

It also applies for backend code. If you’re using an MVC framework, you are maybe testing your controllers by booting up the whole framework in order to check whether you annotated your controller properly. Don’t!

Please, refrain from jamming these tools into your tests. Don’t couple behaviour (logic) & presentation (periphery). Think about what you’re testing and why.

Good luck & toodaloo!

--

--