The correct way of mocking dependencies in Angular unit tests

Picture of city.
Photo by Pedro Lastra on Unsplash

Recently I had the pleasure of doing a small refactor in an Angular project. Refactor is one of my favorite things to do so I was glad that I had a chance to do it. It wasn't anything especially complicated, a basic name change, but the interesting part is that after my modifications our unit tests started to fail. I was confused by the whole situation so I started to unravel the mystery that was behind the failing tests. After some time I finally found what was the cause of this issue. I was surprised so I decided to share my findings in this article.

The problem

At the beginning let's have a look a the main class, a basic Angular service:

@Injectable()
class UserService {
    getUsers(): Observable<ReadonlyArray<User>> {...}
}

Not long time ago in my team, we decided that we want to use the "on & once" naming convention. This convention states that methods that return observable should start with on and methods that return observable with a single value should be named once. You can read more about it here.

Taking all of that into consideration I decided that I want to change the name of the getUsers method to onUsers. After changes the UserService looked like this:

@Injectable()
class UserService {
    onUsers(): Observable<ReadonlyArray<User>> {...}
}

To do this change, I used the helping hand provided by my favorite IDE (Webstorm), which renamed all of the appearances of this method in our base code. By doing it this way I was completely sure that all of the usages of the getUsers method were changed to onUsers.

Refactor using Webstorm
Refactor using Webstorm

Unfortunately after this action, one of the unit tests started to fail. I wasn't expecting that since I wasn't changing any business logic, only basic signature change.

The process of refactoring is hard.
The process of refactoring is hard.

The failed test was related to the class UserComponent which main responsibility was to check whether the component presents a list of users or not. The code of the Component looks like that:

@Component({
   template: `
   <div *ngFor="let user of users$ | async"></div>
   `
})
class UserComponent {

   users$ = this.userService.onUsers()

   constructor(private readonly userService: UserService) {}
}

As you can see the UserComponent uses the UserService to get a list of users.

One of the characteristics of unit tests is that they have to work in a specific separated scope, related only to the tested code. All of the external dependencies should be mocked to provide the expected level of isolation. That said, the correctly written test for this component should use a mocked version of the UserService. The code that I found looked like this:

beforeEach(() => {
   TestBed.configureTestingModule({
      imports: [CommonModule],
      declarations: [
         UserComponent
      ],
      providers: [
         {
            provide: UserService,
            useValue: {
               getUsers: () => of([...])
            }
         }
      ]
   });
});

it('shows list of users', () => {

   const fixture = TestBed.createComponent(UserComponent);

   // ...

});

The developer who wrote the test created a mocked version of the UserService this way:

{ provide: UserService, useValue: {getUsers: () => of([...])} }

This is a very common practice in the Angular world. Nevertheless, this was the line of code that caused the test to fail. Automatical refactor made with IDE didn't catch this mock implementation of the UserService. The correct implementation should look like this:

{ provide: UserService, useValue: {onUsers: () => of([...])} }

What's interesting about this is that it made me think; the testing practice cost me a lot of time and it shouldn't be used in a large-scale Angular application. This practice makes a project unable to scale nor to be extended. Let's see what can be done to avoid these issues in the future.

The process of refactoring may generate confusion.
The process of refactoring may generate confusion.

Solution

The first thing that comes to mind is to take advantage of polymorphism as TypeScript implements all of the OOP paradigms:

class MockUserService extends UserService {

   onUsers(): Observable<ReadonlyArray<User>> {
      // mock implementation
   }
}
// provide
{ provide: UserService, useClass: MockUserService }

The code above shows a simple and quick solution. You can create a mock class that extends the UserService. Then you can override methods that you want to behave differently. This solution is precise and provides you with full IDE support.

Second solution

After talking with my colleagues about this issue, one of them proposed another great solution for this case:

let mockService: Partial<UserService> = {

   onUsers(): Observable<ReadonlyArray<Task>> {
      // mock implementation
   }
}
// provide
{ UserService, useValue: mockService }

As you can see you can create a variable with a type Partial <UserService> and then assign an object to it which contains only methods that you want to override. To my surprise, this solution also works well with IDE.

Third solution

An alternative solution is a variation of the previous one. Instead of providing the implementation of the object, you can do it with the help of the jest spy feature:

let mockService: Partial<UserService> = {
    onUsers(): Observable<ReadonlyArray<Task>>{}
}
jest.spyOn(mockService, 'onUsers').and.return(...)
// provide
{ UserService, useValue: mockService }

The spyOn function overrides onUsers method and allows it to act differently.

Realization

As I was finishing this article I thought of another simple solution to this issue. It turned out that adding a specific type to an object literal allows IDE to recognize it as a partial implementation of the service:

{ provide: UserService, useValue: {...} as Partial<UserService> }

Webstorm correctly changes the signature of methods when a partial type is provided.

Summary

It is important to write code in a way that it can be easily extended in the future. In my opinion, as a developer, you should always choose solutions that allow your application to scale and to be improved efficiently. I hope provided in this article solutions will save you lots of time and let you write more stable and readable unit tests.

Luke, Software engineer, passionate about best practices, frameworks React, Vue, patterns and architectures for modern web
				development, open-source creator, TypeScript enthusiast, Generic UI Technical Blog

Luke

Software engineer, passionate about best practices, frameworks React & Vue, patterns and architectures for modern web development, open-source creator, TypeScript enthusiast.