6
votes

I want to write a unit test for an Angular (~v7.2.0) Component with Jest (^v24.8.0).

This is the component, it's using the nested service via this.api.manageUser.getUsersStats() and i want to check if this is ever called and mock the result. So best thing would be to able to write a "global" / "manual" mock in a "mocks"-folder.

I already tried a lot of things, but none works as expected.

I really hope somebody can help me!

// users.component

import { TstApiService } from '../../services/TstApi/TstApi.service';

@Component({
  selector: 'app-users',
  templateUrl: './users.component.html',
  styleUrls: ['./users.component.sass']
})
export class UsersComponent implements OnInit, OnDestroy {
  constructor(
    // ...
    private api: TstApiService
    ) { }

  ngOnInit() {
    this.getUsersStats();
  }

  private getUsersStats() {
    this.api.manageUser.getUsersStats().pipe(take(1)).subscribe(userStats => {
      /* ... */ = userStats.allUsers
    })
  }
}

This is my Service:

// TstApi.service

import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class TstApiService {
    private http: TstHttp
    manageUser: ManageUser

    constructor(private httpClient: HttpClient) {
        this.http = new TstHttp(this.httpClient)
        this.manageUser = new ManageUser(this.http)
     }
}

class ManageUser {
    constructor(private http: TstHttp) {}
    getUsersStats(): Observable<TUserStats> { //<-- this is the function used by the component
      return this.http.get('/manage/user/stats')
    }
}

class TstHttp {
    private apiUrl: string
    constructor(private http: HttpClient) {
        this.apiUrl = environment.apiBaseUrl
    }
    /**
     * @throws{ApiError}
     * @throws{Error}
     */
    get<T>(path: string): Observable<T> {
      return this.http.get<T>(environment.apiBaseUrl + path)
    }
}

This is the spec file:

/// <reference types='jest'/>
import { TestBed, ComponentFixture, inject } from '@angular/core/testing';
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Router } from '@angular/router';
import { UsersComponent } from './users.component';
import { UsersService } from './users.service';
import { TstApiService } from "src/app/services/TstApi/TstApi.service";

// Approach No. 3
jest.mock("src/app/services/TstApi/TstApi.service")

// Approach No. 1
// jest.mock("src/app/services/TstApi/TstApi.service", () => {
//  return jest.fn().mockImplementation(() => {
//    return {manageUser: () => { getUsersStats: jest.fn() }};
//  })
// });

describe('Users', () => {
  @Component({ selector: 'app-input', template: '' }) class InputComponent {}
  let router: Router;
  let component: UsersComponent;
  let fixture: ComponentFixture<UsersComponent>;
  const http: HttpClient = new HttpClient(null);
  let apiService: TstApiService = new TstApiService(http);
  const usersService: UsersService = new UsersService(apiService);
  let usersServiceMock;
  // let tstApiServiceMock; // Approach No. 2
  let httpMock: HttpTestingController;

  beforeEach(async () => {
        // Approach No. 2
    // tstApiServiceMock = {
    //   manageUser: jest.fn().mockImplementation(() => ({
    //     getUsersStats: jest.fn().mockImplementation(() => Promise.resolve(null))
    //   }))
    // }
    await TestBed.configureTestingModule({
      declarations: [
        UsersComponent,
        InputComponent
      ],
      imports: [
        HttpClientTestingModule,
        RouterTestingModule.withRoutes([])
      ],
      providers: [
        // TstApiService
        // { provide: TstApiService, useValue: tstApiServiceMock } // Approch No. 2
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA]
    })
      .compileComponents()
      .then(() => {
        // create component and test fixture
        fixture = TestBed.createComponent(UsersComponent);
        router = TestBed.get(Router)
        // get test component from the fixture
        component = fixture.componentInstance;
      });
  });

  beforeEach(
        // Approach No. 4
    inject([TstApiService, HttpTestingController], (_service, _httpMock) => {
      apiService = _service;
      httpMock = _httpMock;
  }));

  test.only('Should getUsersStats on NgInit', () => {
    expect(apiService.manageUser.getUsersStats).toHaveBeenCalled();
  })
})

For the "Approach No. 3", here is the "manual"-mock:

import { ManageUser, TstHttp, ErrorAlert } from './../TstApi.service';
// import { HttpClient } from '@angular/common/http';

// export const TstApiService = jest.genMockFromModule('../TstApi.service.ts');


export class TstApiService {
  private http: TstHttp;
  manageUser: ManageUser;
  error: ErrorAlert;

  constructor(private httpClient) {
    this.error = new ErrorAlert();
    this.http = new TstHttp(this.httpClient, this.error);
    this.manageUser = new ManageUser(this.http);
  }
}
1
Can you add your spec file? I think the answer is in the way you provide and setup your test module for this specific test.Bjorn 'Bjeaurn' S
@Bjorn'Bjeaurn'S just did :)dnepro
You say "None are working as expected", can you add what you see with which attempt? What errors are you seeing?Bjorn 'Bjeaurn' S

1 Answers

1
votes

You can try to use ng-mocks and its MockBuilder.

describe('suite', () => {
  const getUsersStats = jest.fn(() => EMPTY);

  beforeEach(() => {
    // the 2nd param should be the module of UsersComponent
    return MockBuilder(UsersComponent)
      .mock(TstApiService, {
        // because ManageUser is provided via `new` call,
        // we need to provide a mock instance manually.
        manageUser: MockService(ManageUser, {
          getUsersStats,
        }),
      });
  });

  it('spec', () => {
    MockRender(UsersComponent);
    expect(getUsersStats).toHaveBeenCalled();
  });
});