How do I test code using inject()
Tags: testing, angular, webdev, frontend
Published: 2024-03-05 16:00:00

This article discusses testing Angular code, which uses the `inject` function for dependency injection.

If you are more of a visual learner, here's a video for you:

{% embed https://youtu.be/mMiSmStwGOU %}

## Why `inject`?

The `inject` function, introduced in Angular 14, is an alternative to the constructor-based dependency injection. `inject` has the following advantages:

**1. Standardization of Decorators**

Decorators have existed in an experimental setting within the TypeScript configuration.

While TC39 has completed the standardization of decorators, all decorator types must be finalized to disable the experimental mode of the TypeScript compiler.

![tsconfig.json in Angular with experimental decorators](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dsfcpuwx6leygkq39red.png)

Unlike the constructor, the `inject` function works without parameter decorators, eliminating potential risks for future breaking changes associated with dependency injection via the constructor.

```typescript
class FlightSearchComponent {
  constructor(
    @SkipSelf() // not standardized
    @Optional() // not standardized
    private flightService1: FlightService
  ) {}

  private flightService2 = inject(FlightService, {
    skipSelf: true,
    optional: true,
  });
}
```

**2. Erased Type in the constructor**

The type of `inject` is also available during runtime. With the constructor, only the variable name survives the compilation. So, the TypeScript compiler has to add specific metadata where type information is part of the bundle. This setting is not the default behavior of the compiler.

```typescript
// TypeScript
class FlightService {}

class FlightSearchComponent {
  constructor(private flightService: FlightService) {}

  flightService2 = inject(FlightService);
}
```

```javascript
// Compiled JavaScript

class FlightService {
}

class FlightSearchComponent {
  // type is gone
  constructor(flightService) { 
    this.flightService = flightService;

    // type is still there
    this.flightService2 = inject(FlightService); 
  }
}
```

**3. Class Inheritance**

`inject` is more accessible for class inheritance. With the constructor, a subclass has to provide all dependencies for its parents. With `inject`, the parent get their dependencies on their own.

```typescript
// Inheritance and constructor-based dependency injection

class Animal {
  constructor(private animalService: AnimalService) {}
}

class Mammal extends Animal {
  constructor(
    private mammalService: MammalService,
    animalService: AnimalService
  ) {
    super(animalService);
  }
}

class Cat extends Mammal {
  constructor(
    private catService: CatService,
    mammalService: MammalService,
    animalService: AnimalService
  ) {
    super(mammalService, animalService);
  }
}
```

```typescript
// Inheritance via inject

class Animal {
  animalService = inject(AnimalService);
}

class Mammal extends Animal {
  mammalService = inject(MammalService);
}

class Cat extends Mammal {
  catService = inject(CatService);
}
```

**4. Type-Safe Injection Tokens**

Injection Tokens are type-safe.

```typescript
const VERSION = new InjectionToken<number>('current version');

class AppComponent {
  //compiles, although VERSION is of type number
  constructor(@Inject('VERSION') unsafeVersion: string) {} 

  safeVersion: string = inject(VERSION); // fails 👍
}
```

**5. Functional Approaches**

Some functional-based approaches (like the NgRx Store) that don't have a constructor can only work with `inject`.

---

Because of these many advantages, `inject` was the rising star. It almost looked like the constructor-based approach might become deprecated.

At the time of this writing, things shifted a little bit. According to Alex Rickabaugh, property decorators are in Stage 1 regarding standardization. That's why he recommends using whatever fits best and waiting for the [results of the TC39](https://github.com/tc39/proposal-class-method-parameter-decorators).

Time Position: 25:25

{% embed https://youtu.be/QtTLZRIVaKk?si=ztN826SvGspWzbd7&t=1521 %}

## `TestBed.inject`

Many tests face issues when the application code switches to `inject`. One of the main issues is the instantiation. With a constructor-based class, a test could directly instantiate the class, but with `inject`, that is impossible, and we always have to go via the `TestBed`.

Whenever dealing with a Service/`@Injectable`, we can call `TestBed.inject` from everywhere in our test. 

That could be at the beginning of the test, in the middle, or even at the end.

We have the following Service, which we want to test:

```typescript
@Injectable({ providedIn: "root" })
export class AddressAsyncValidator {
  #httpClient = inject(HttpClient);

  validate(ac: AbstractControl<string>): Observable<ValidationErrors | null> {
    return this.#httpClient
      .get<unknown[]>("https://nominatim.openstreetmap.org/search.php", {
        params: new HttpParams()
          .set("format", "jsonv2").set("q", ac.value),
      })
      .pipe(
        map((addresses) =>
          addresses.length > 0 ? null : { address: "invalid" }
        )
      );
  }
}
```

`AddressAsyncValidator` injects the `HttpClient`. So we have to mock that one.

There is no need to import or create a component in our `TestingModule`.

It is pure "logic testing". The test doesn't require a UI, i.e., DOM rendering.

```typescript
describe("AddressAsyncValidator", () => {
  it("should validate an invalid address", waitForAsync(async () => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: HttpClient,
          useValue: { get: () => of([]).pipe(delay(0)) },
        },
      ],
    });

    const validator = TestBed.inject(AddressAsyncValidator);
    const isValid = await lastValueFrom(
      validator.validate({ value: "Domgasse 5" } as AbstractControl)
    );
    expect(isValid).toEqual({ address: "invalid" });
  }));
});
```

That test will succeed. There are two remarks, though.

First, if `AddressAsyncValidator` has no `{providedIn: 'root'}` (only `@Injectable` is available), we have to provide the Service in the `TestingModule`:

```typescript
@Injectable()
export class AddressAsyncValidator {
  // ...
}

describe("AddressAsyncValidator", () => {
  it("should validate an invalid address", waitForAsync(async () => {
    TestBed.configureTestingModule({
      providers: [
        AddressAsyncValidator,
        {
          provide: HttpClient,
          useValue: { get: () => of([]).pipe(delay(0)) },
        },
      ],
    });

    // rest of the test
  }));
});
```

Second, we cannot run `inject` inside the test. That will fail with the familiar error message.

> NG0203: inject() must be called from an injection context such as a constructor, a factory function, a field initializer, or a function used with `runInInjectionContext`.

```typescript
describe("AddressAsyncValidator", () => {
  it("should validate an invalid address", waitForAsync(async () => {
    TestBed.configureTestingModule({
      providers: [
        AddressAsyncValidator,
        {
          provide: HttpClient,
          useValue: { get: () => of([]).pipe(delay(0)) },
        },
      ],
    });

    const validator = inject(AddressAsyncValidator); // not good
  }));
});
```

## `TestBed.runInInjectionContext`

Why would we ever want to run `inject` in a test? Answer: Whenever we have to test a function which uses `inject`. 

In the Angular framework, that could be an `HttpInterceptorFn` or one of the router guards, like `CanActivateFn`.

In the Angular community, we currently see many experiments with functional-based patterns.

A good start might be 

{% embed https://github.com/nxtensions/nxtensions %}

{% embed https://github.com/analogjs/analog/pull/870 %}

We will stick to native features, though, and test a `CanActivateFn`:

```typescript
export const apiCheckGuard: CanActivateFn = (route, state) => {
  const httpClient = inject(HttpClient);

  return httpClient.get("/holiday").pipe(map(() => true));
};
```

`apiCheckGuard` is a simple function that verifies if a request to the URL "/holiday" succeeds. There is no constructor in a function. Therefore, `apiCheckGuard` must use `inject`.

A test could look like this:

```typescript
it("should return true", waitForAsync(async () => {
  TestBed.configureTestingModule({
    providers: [
      { provide: HttpClient, useValue: { get: () => of(true).pipe(delay(1)) } },
    ],
  });

  expect(await lastValueFrom(apiCheckGuard())).toBe(true);
}));
```

That will also not work. We get the NG0203 error again.

The solution is `TestBed.runInInjectionContext`. As the name says, it allows us to run any function, which will run again in the injection context. That means the `inject` is active and will work as expected.

```typescript
describe("Api Check Guard", () => {
  it("should return true", waitForAsync(async () => {
    TestBed.configureTestingModule({
      providers: [
        {
          provide: HttpClient,
          useValue: { get: () => of(true).pipe(delay(1)) },
        },
      ],
    });

    await TestBed.runInInjectionContext(async () => {
      const value$ = apiCheckGuard();
      expect(await lastValueFrom(value$)).toBe(true);
    });
  }));
});
```

Although it looks like `TestBed.runInInjectionContext` provides the injection context asynchronously, that is not true.

The guard calls `inject` synchronously. If it did it in the pipe operator, `inject` would run in the asynchronous task and again fail.

## Summary

`inject` comes with many advantages over the constructor-based dependency injection, and you should consider using it.

`HttpInterceptorFn` and router guards are functions and can only use `inject` to get access to the dependency injection.

To have a working `inject`, we must wrap those function calls within `TestBed.runInInjectionContext`.

There is also `TestBed.inject`, which behaves differently. It is only available in tests and we should use it get instances of classes. Regardless, if those classes use `inject` or the constructor-based dependency injection.

---

You can access the repository at https://github.com/rainerhahnekamp/how-do-i-test

If you encounter a testing challenge you'd like me to address here, please get in touch with me!

For additional updates, connect with me on [LinkedIn](https://www.linkedin.com/in/rainerhahnekamp), [X](https://twitter.com/rainerhahnekamp), and explore our website for workshops and consulting services on testing.

{% embed https://www.angulararchitects.io/en/training/professional-angular-testing-playwright-edition/ %}