---
status: "published"
title: "Using Angular's Dependency Injection Sparingly"
date: "2018-10-01"
description: "How excessive abstraction slowed down our unit tests to a crawl."
image: "https://github.com/user-attachments/assets/52b77f8a-0a08-438e-8b01-f4c9b8838d59"
imageAlt: "Photo by Ricardo Gomez Angel on Unsplash"
author: "Vlad Sabev"
authorUrl: "https://github.com/vdsabev"
authorAvatar: "https://github.com/vdsabev.png"
tags: ["angular","dependency injection","software development","testing"]
navigationIndex: 0
---

Photo by [Ricardo Gomez Angel](https://unsplash.com/photos/R3K93pdG0t0?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText) on [Unsplash](https://unsplash.com/@ripato?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText)

Our team had been working on a mid-sized Angular application for the past few months. Unfortunately, we hadn't started with testing in mind at first. As the project kept growing and at the same time we were slowly approaching our goal of > 90% code coverage, we started asking these questions more and more often:

- Were the tests we were writing meaningful?
- Were they sufficiently decoupled from our code?
- Was code coverage really an adequate metric for reliable software?

To find an answer, we had an internal team discussion about our unit testing strategy and using Angular's built-in Dependency Injection mechanism in particular.

In this post we'll now go over some of the points we touched.

## Requirements

- Basic knowledge of unit testing
- Familiarity with TypeScript and the [Dependency Injection](https://en.wikipedia.org/wiki/Dependency_injection#Examples) pattern (DI) used in Angular (specifically, [Constructor Injection](https://en.wikipedia.org/wiki/Dependency_injection#Constructor_injection))

> In the interest of brevity, I'll skip most of the "plumbing" code like imports and `@Injectable` decorators. In doing so, I hope that both those of you who *are* familiar with Angular and those who aren't will be able to follow along.

## Use Case

As an example, let's take a very simple service for working with strings:

```typescript
export class TextService {
  startsWith(text: string, query: string): boolean {
    return text.indexOf(query) === 0;
  }
}
```

Now inject `TextService` into another service:

```typescript
export class UrlService {
  constructor(private textService: TextService) {
  }

  isSecure(url: string): boolean {
    return this.textService.startsWith(url, 'https:');
  }
}
```

Finally, let's write some tests for our `UrlService`:

```javascript
describe(`UrlService`, () => {
  // ... create the instance of urlService

  it(`should return true if URL starts with 'https:'`, () => {
    expect(urlService.isSecure('https://medium.com')).toBe(true);
  });

  it(`should return false if URL starts with 'http:'`, () => {
    expect(urlService.isSecure('http://medium.com')).toBe(false);
  });
});
```

But how was the instance of `urlService` created? There are a few different ways to do it, each with its trade-offs.

### Angular's TestBed

This is the approach most often recommended by tutorials and documentation, and the one we started with.

Our experience was that there were lots of mysterious bugs using it, for example this issue when calling `fixture.detectChanges()`:

```
SyntaxError
      at viewWrappedDebugError (node_modules/@angular/core/bundles/core.umd.js:9844:15)
      at callWithDebugContext (node_modules/@angular/core/bundles/core.umd.js:15146:15)
      at Object.debugCheckAndUpdateView [as checkAndUpdateView] (node_modules/@angular/core/bundles/core.umd.js:14673:12)
      at ViewRef_.Object.<anonymous>.ViewRef_.detectChanges (node_modules/packages/core/esm5/src/view/refs.js:508:14)
      at ComponentFixture.Object.<anonymous>.ComponentFixture._tick (node_modules/@angular/core/bundles/core-testing.umd.js:220:32)
      at node_modules/packages/core/esm5/testing/src/component_fixture.js:108:10
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/dist/zone-node.js:388:26)
      at ProxyZoneSpec.Object.<anonymous>.ProxyZoneSpec.onInvoke (node_modules/zone.js/dist/proxy.js:128:39)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/dist/zone-node.js:387:32)
      at Object.onInvoke (node_modules/packages/core/esm5/src/zone/ng_zone.js:594:10)
      at ZoneDelegate.Object.<anonymous>.ZoneDelegate.invoke (node_modules/zone.js/dist/zone-node.js:387:32)
      at Zone.Object.<anonymous>.Zone.run (node_modules/zone.js/dist/zone-node.js:138:43)
      at NgZone.Object.<anonymous>.NgZone.run (node_modules/@angular/core/bundles/core.umd.js:4615:69)
      at ComponentFixture.Object.<anonymous>.ComponentFixture.detectChanges (node_modules/packages/core/esm5/testing/src/component_fixture.js:108:10)
```

You can imagine exactly how much fun debugging that was. **Spoiler alert**: we had forgotten to provide a service when configuring the `TestBed`. A similarly weird issue was caused by passing `{}` instead of `[]` to a component's input.

Anyway, let's use the `TestBed` properly:

```javascript
describe(`UrlService`, () => {
  let urlService: UrlService;
  beforeEach(() => {
    TestBed.configureTestingModule({ providers: [UrlService] });
    urlService = TestBed.get(UrlService);
  });

  it(`should return true if URL starts with 'https:'`, () => {
    expect(urlService.isSecure('https://medium.com')).toBe(true);
  });

  it(`should return false if URL starts with 'http:'`, () => {
    expect(urlService.isSecure('http://medium.com')).toBe(false);
  });
});
```

After figuring out all other weird issues, it looks simple enough — we configure what the different providers in our application are, then use `TestBed.get(UrlService)` to get a reference to the instance of the injected service.

However, after using it for a while, we noticed `TestBed` was painfully slow on Windows — for some of our developers up to 3 times slower than on UNIX systems. We looked for ways to speed the test runs up, but in the end even [switching to Jest](https://www.xfive.co/blog/testing-angular-faster-jest) didn't seem to help much.

This was a no go — unit tests should only take a few seconds to run, not 30! If tests are slow they lose their ability to provide rapid feedback during development.

So `TestBed` was out of the game.

### Manually injecting dependencies

While looking for other approaches besides `TestBed` we found something in the [Angular docs](https://angular.io/guide/testing#testing-without-beforeeach):

> There's another school of testing that… prefers to create classes explicitly rather than use the `TestBed`.

Here's an example with our `UrlService`:

```javascript
describe(`UrlService`, () => {
  let urlService: UrlService;
  beforeEach(() => {
    const textService = new TextService();
    urlService = new UrlService(textService);
  });

  it(`should return true if URL starts with 'https:'`, () => {
    expect(urlService.isSecure('https://medium.com')).toBe(true);
  });

  it(`should return false if URL starts with 'http:'`, () => {
    expect(urlService.isSecure('http://medium.com')).toBe(false);
  });
});
```

Overall, this approach looked straightforward (and it was also running much faster), but it did raise a question — what would happen if we added a dependency to `TextService` itself?

### Injecting secondary dependencies

Let's get another level of Dependency Injection going by adding a `SecurityService` that checks the application for some common security errors:

```typescript
export class SecurityService {
  constructor(private urlService: UrlService) {
  }

  getSecurityErrors(url: string): boolean {
    const errors = [];

    if (!this.urlService.isSecure(url)){
      errors.push('This application is insecure!');
    }

    // ... some other security checks

    return errors;
  }
}
```

Now let's add some tests using the approach where we instantiate services manually — and here's where things get unwieldy:

```javascript
describe(`SecurityService`, () => {
  let securityService: SecurityService;
  beforeEach(() => {
    const textService = new TextService();
    const urlService = new UrlService(textService);
    securityService = new SecurityService(urlService);
  });

  it(`should return no errors if URL starts with 'https:'`, () => {
    expect(
      securityService.getSecurityErrors('https://medium.com')
    ).toHaveLength(0);
  });

  it(`should return one error if URL starts with 'http:'`, () => {
    expect(
      securityService.getSecurityErrors('http://medium.com')
    ).toHaveLength(1);
  });
});
```

Great, so now we also have to instantiate `TextService` - a piece of code the `SecurityService` doesn't use directly and shouldn't really know anything about?!

Me and my colleague called this "mocking the world". Sure, we've gained speed and (at first look) simplicity by going around Angular's `TestBed`, but we'd completely lost any benefits of the inversion of control that Dependency Injection provides!

### No Dependency Injection

Of course, we could write all those services without using DI altogether. That's exactly what we ended up doing in many parts of our application that were dealing with simple data processing — replacing the service class with a simple object:

```typescript
export const textService = {
  startsWith(text: string, query: string): boolean {
    return text.indexOf(query) === 0;
  },
};
```

Then our tests wouldn't have to deal with DI at all.

Of course, the trade-off is that we can no longer inject any Angular dependencies in those services. Once we start using the DI pattern anywhere in our application, mixing it into code that doesn't becomes hard.

## Conclusion

So is it that dependency injection is just bad?

I believe as with most other tools it should be used sparingly. DI is meant to make things simpler, and if it doesn't — use something else. For example [Jest mocks](https://jestjs.io/docs/en/mock-functions) virtually eliminate the need for using DI when unit testing.

We also probably wouldn't write a unit test to check whether clicking on a button in one of our components calls some method. Perhaps we should invest more time in writing [E2E tests with Cypress](https://www.cypress.io) or [Integration tests](https://blog.kentcdodds.com/write-tests-not-too-many-mostly-integration-5e8c7fff591c) instead.

How has your experience with Dependency Injection and unit testing turned out?
