Photo by Justin Kauffman on Unsplash

Angular TestBed considered harmful!

Georgi Parlakov
Angular In Depth
6 min readMay 19, 2020

--

Why I don’t use Angular TestBed and perhaps you shouldn’t either

In this article, I will do a brief overview of TestBed, talk about some of its properties and practices, and then offer an alternative. An alternative that gives you back control over your tests! And a test-automation option!

Let’s make the state of the component our interface with Angular and let it render HTML and do DOM.

I need to only test MY code.

During my practice, I’ve found using TestBed to be sub-optimal and have developed a strategy that works really well for unit testing components, services, and any other Angular and non-Angular abstractions.

What does TestBed do for us?

  • It creates a dependency injection (DI) context and allows us to override providers, services, and whole modules. It compiles, instantiates, and renders to HTML our components attaching them to the fixture instance.
  • It wants to compile and instantiate our components and then execute tests against the DOM. This forces us, the test authors and maintainers to query DOM and work with the HTML which is the output of Angular. I find that slow, quirky, and most importantly — not our job. I trust Angular that when given a variable that is visible === true to actually visualize my component:
  • And hide it when visible === false. And I should not test that. That's Angular's job and I trust it and its thousands of tests. There are thousands of people looking on the framework's code, chances are bugs will get caught and fixed. I need to test MY code. That's what is usually broken because there are only one pair of eyes on it and zero unit tests until I actually create them.

What I’m getting at is test only our code and let Angular do its job. That is to take the values and bind them to HTML. Which means we do not need to actually compile our components and have TestBed render them. Only to instantiate the underlying class and test the instance’s methods and properties.

Taking the approach of not rendering HTML coerces us into not having logic in the templates because we would not be able to test it! And not having logic in templates is a very nice thing!

TestBed is not required

If we do not need to compile, instantiate, and render our components we do not require TestBed. But its API is convenient with the TestBed.get or inject function - right? It allows us to override modules. And so on. Kinda is, kinda is not because we then need to fill in the prerequisites to our test case inside our test case. And it often looks like this:

So this looks like a normal test case — right? It looks like many test cases I wrote myself. And while it does the job of making sure the upcoming book tour is visible there are a few things it does not answer when it should.

  • What is the prerequisite? — Having a book? Having bookTours? both? Having a name for the author?
  • What are we testing for here? There’s a lot of clutter.
  • Would we need to edit all Author instantiations if the shape of Author class changes? Every test on the author component would need one - right?
  • Where do fixture and debugElement come from? For a newbie in the Angular unit testing that is hard.

What if we rewrote the test to look like this:

There are a few things going on here:

  1. The test case spells out the prerequisites in English and delegates the instantiation logic, clearing the stuff we, as the test reader, do not need for understanding the test case logic. Thus making it more readable and more maintainable. It often adds the value of conciseness to the test case: fewer lines to read and comprehend. At the same time, everything the case requires is explicit. No need to go to the beforeEach or async beforeEach to find out what the setup looks like.
    — You, the Reader, can better judge readability here.
  2. It does not instantiate and render our component. Just tests out the state of the component. No DOM, quicker and less flakier test.
  3. It gives us one place where we control the context of each test case and lets us decide the exact moment of instantiation of our unit under test.

Setup

Introducing the setup function. It works to replace the whole TestBed test workflow. It will take the task to create dependencies and instantiate the class under test.

It takes the place of getMyComponent() in the example above. Using autoSpy which takes away the need to create mock objects manually:

  1. We’ve named the methods of our builder object so that they tell the business use case and hide the implementation. A reader can still go and understand HOW a prerequisite is set up, but they also know WHAT it is setting up. The reader gets one more piece of our context, as unit test authors. This is very important in making the test more maintainable.
  2. The lengthy implementation is out of sight in the test case itself — one more thing improving readability.
  3. We are using the builder pattern here for better developer ergonomics. It seems that the dot-chaining is the preferred way of consuming APIs.
  4. There is one place to control all of our setup in. In the future, where things often change, we would need to change stuff in our setup function only and not all over our test cases.
  5. The setup function does not know how to render HTML! But - "How can I test the logic without dabbling in the DOM and HTML?" By making the state of the component our interface with Angular and let it render HTML and do DOM manipulation. Angular is so good at doing those things and we need not do them :)
  6. The mock logic placed in setup we already have to place somewhere, even when using TestBed. And having the builder gives us that place. Instead of placing those mocks in the test cases themselves.

Using dependencies directly

There are cases where we might want to manipulate the dependencies directly. We might use TestBed.get or inject function for that. The setup equivalent to that is destructuring. Continuing the example above, in our test case we might destructure and get the service:

Usually, I would encourage these setting up activities to be wrapped in a setup method, but the world is a messy place and sometimes we just need to work directly with our dependencies.

Default

Often there are some initialization tasks before we can consider a unit working. For example — instantiate observables, fill in required state, etc. The result is a base line, a working component that we know will not throw when we call ngOnInit on it. These tasks I put in a dedicated default method.

If we had a BooksComponent that depended on BooksService like this:

We would need to make sure that getBooks returns some observable, otherwise we'll get an exception.

Why a separate method, you ask? Why not part of the build for example? Well, the answer is trivial - we might need to actually test the case where the exception is thrown and for that we'd like to not call default, so having it gives us that option.

Why “setup”?

In the first example, I was testing a component so that’s what I initially went with — getMyComponent. We also test services and pipes etc.

Also, I wanted to automate the maintenance of tests and it’s easier to discover the function when it’s called setup.

Team acceptance

This looks like magic!

This will never work!

Yes, I realize its a totally different thing than what you may be used to. I realize that fact will make the setup function seem alien, complex and hazardous. "Looks like magic!" A colleague of mine Симон Казаков told me when he first saw it.

I would understand it if you don’t want to jeopardize your team’s workflow.

And of course, I would be glad if you tried it and gave me some feedback.

Compatibility

Oh, and this also works side by side with TestBed. You can use both or rely on setup for getting the dependencies for the test module.

Automation

Having the setup function allows for a bit of automation. I've created a tool ( SCuri) and a VS Code extension for it, that makes creating and updating unit tests a breeze. It removes the boilerplate part of doing unit tests and leaves you with the part that only you can do. Boilerplate being:

  • generate the setup function and the build method
  • update the setup function and the build method with new dependencies
  • add a test case for each public method of your class
  • generate autoSpy for jest/jasmine

Thanks for reading!

I’ve been doing a lot of unit tests, and this workflow has proven a better experience. Try it and please tell me what you think.

--

--

Georgi Parlakov
Angular In Depth

Angular and DotNet dev. RxJs explorer. Testing proponent. A dad. Educative.io course author. https://gparlakov.github.io/