Angular Unit Tests with Jest - This constructor is not compatible with Angular Dependency Injection

Authors
Posted on
Posted on
Shameless Plug: (YOU can support Ahsan) ❤️
📢   The course  Hands-On App Development with Ionic   on Udemy is on a 89% sale right now at €12.99 💰💰. And if you hurry, I have TEN LIMITED DISCOUNT COUPONS for the first 10 folks who reach out to any of my socials for them.

In this short article, you'll learn how to fix this annyoing issue in Angular when you're starting to implement component unit tests with Jest that have dependencies. Or you might be just migrating away from Jasmine and Karma.

What the error?

If you have an Angular component that depends on a service, and you've just migrated to Jest from Jasmine and Karma, you might see something as follows if you've missed one critical step:

This constructor is not compatible with Angular Dependency Injection because its dependency at index 0 of the parameter list is invalid.
    This can happen if the dependency type is a primitive like a string or if an ancestor of this class is missing an Angular decorator.

    Please check that 1) the type for the parameter at index 0 is correct and 2) the correct Angular decorators are defined for this class and its ancestors.

      at ɵɵinvalidFactoryDep (../packages/core/src/di/injector_compatibility.ts:112:9)

Example:

Let's suppose we have a component named WatchComponent as follows:

import { Component, OnInit } from '@angular/core'
import { WatchService } from 'src/app/services/watch.service'

@Component({
  selector: 'app-watch',
  templateUrl: './watch.component.html',
  styleUrls: ['./watch.component.scss'],
})
export class WatchComponent implements OnInit {
  time: string

  constructor(private watchService: WatchService) {}

  ngOnInit(): void {
    this.time = this.watchService.getTime()
  }

  startTimer() {
    this.watchService.startTimer()
  }

  stopTimer() {
    this.watchService.stopTimer()
  }

  resetTimer() {
    this.watchService.resetTimer()
  }
}

And we have the WatchService as follows:

import { Injectable } from '@angular/core'

@Injectable({
  providedIn: 'root',
})
export class WatchService {
  constructor() {}

  getTime() {
    return new Date()
  }

  startTimer() {
    //some code
  }

  stopTimer() {
    //some code
  }

  resetTimer() {
    //some code
  }
}

If you write the tests as follows, you'll still see the error we've mentioned above:

import { ComponentFixture, TestBed } from '@angular/core/testing'
import { WatchService } from 'src/app/services/watch.service'

import { WatchComponent } from './watch.component'

describe('WatchComponent', () => {
  let component: WatchComponent
  let fixture: ComponentFixture<WatchComponent>
  let watchServiceStub: Partial<WatchService>
  let watchService

  beforeEach(async () => {
    watchServiceStub = {
      startTimer: () => {},
      stopTimer: () => {},
      resetTimer: () => {},
    }
    await TestBed.configureTestingModule({
      declarations: [WatchComponent],
      providers: [{ provide: WatchService, useValue: watchServiceStub }],
    }).compileComponents()
  })

  beforeEach(() => {
    fixture = TestBed.createComponent(CounterComponent)
    component = fixture.componentInstance
    fixture.detectChanges()
    watchService = TestBed.inject(WatchService)
  })

  it('should get the inital value of time from the service on component init', () => {
    component.ngOnInit()
    expect(watchService.getTime).toBeCalled()
  })
})

Solution?

The reason why the tests work with Karma and Jasmine, and not with Jests is just one tiny thing missing from one of the tsconfig files that some of the articles online have missed. For example, while working on the #ngBook I'm writing as of today, I followed this article and ended up with the mentioned issue.

If you've faced the same, just make sure that you have set both the esModuleInterop and emitDecoratorMetadata properties to true inside your tsconfig.spec.json file. It should ideally look something as follows:

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/spec",
    "types": ["jest", "node"],
    "esModuleInterop": true,
    "emitDecoratorMetadata": true
  },
  "files": ["src/polyfills.ts"],
  "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
}

The esModuleInterop property makes sure you don't get any weird warnings from Jest while running the tests. And the emitDecoratorMetadata makes sure that the decorator metadata information is compatible and available beween Angular's Dependency Injection and Jest.

Conclusion

Go through different articles/sources while implementing a solution 😄.
If you liked the article, considering subscribing to the newsletter for more. And share this article on your social links with 🖤. And as always, Happy Coding! 🙌🏼