Как подделать сервис, внедренный в компонент, который выполняет HTTP-вызовы?

Я новичок в Angular и пытаюсь изучить модульное тестирование. У меня очень простой компонент, который зависит от службы (UserService). В ловушке жизненного цикла ngOnInit () я вызываю метод службы и сохраняю возвращаемые им данные в свойстве. На мой взгляд, у меня есть список, привязанный к этому свойству.

Это код моего компонента.

import { Component, OnInit } from "@angular/core";
import { UserService } from "../services/user.service";
import { User } from "./models/user";

@Component({
 selector: "app-home",
 templateUrl: "./home.component.html",
 styleUrls: ["./home.component.css"]
})
export class HomeComponent implements OnInit {
 users: User[] = [];
 constructor(private userService: UserService) {}

 ngOnInit() {
   this.userService.getUsers().subscribe(x => {
     this.users = x;
   });
 }
}

Это HTML

<ul>
  <li *ngFor="let user of users" class="user-item">{{ user.name }}</li>
</ul>

Пользовательский сервис выглядит так

import { Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { User } from "../home/models/user";

@Injectable({
  providedIn: "root"
})
export class UserService {
  private baseUrl = "https://jsonplaceholder.typicode.com";
  constructor(private http: HttpClient) {}

  getUsers() {
    return this.http.get<User[]>(`${this.baseUrl}/users`);
  }
}

Все работает как положено. Я пытаюсь написать несколько модульных тестов для этого компонента. Я создал фальшивые данные

import { User } from "../home/models/user";

export const FakeData = {
  users: <User[]>[
    {
      id: 1,
      name: "User 1",
      username: "User1",
      email: "[email protected]",
      phone: "123 456 789",
      website: "afraz.com"
    },
    {
      id: 1,
      name: "User 2",
      username: "User2",
      email: "[email protected]",
      phone: "123 456 789",
      website: "afraz.com"
    }
  ]
};

Это тесты, которые я добавил

import {
  async,
  ComponentFixture,
  TestBed,
  fakeAsync,
  flush
} from "@angular/core/testing";
import { HttpClientModule } from "@angular/common/http";

import { HomeComponent } from "./home.component";
import { UserService } from "../services/user.service";
import { of } from "rxjs";
import { FakeData } from "../fakes/fake-data";
import { By } from "@angular/platform-browser";

describe("HomeComponent", () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;
  let userService: UserService;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientModule],
      declarations: [HomeComponent],
      providers: [UserService]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();

    // Mock the user service
    userService = TestBed.get(UserService);
    spyOn(userService, "getUsers").and.returnValue(of(FakeData.users));
  });

  afterEach(() => {
    fixture.destroy();
    component = null;
  });

  it("should create component", () => {
    expect(component).toBeTruthy();
  });

  it("should get 'Users' from the service", fakeAsync(() => {
    fixture.whenStable().then(() => {
      expect(component.users.length).toEqual(2);
    });
  }));

  it("should bind 'Users' to the view", fakeAsync(() => {
    fixture.whenStable().then(() => {
      const userList = fixture.debugElement.queryAll(By.css(".user-item"));
      expect(userList.length).toEqual(2);
    });
  }));
});

Вот где я запутался, я получаю следующую ошибку на выходе

'Spec' HomeComponent должен связывать 'Users' с представлением 'не имеет никаких ожиданий.'

'Spec' HomeComponent должен получать 'Users' от службы 'не имеет никаких ожиданий.'

Проблема в том, что если я использую Async вместо fakeAsync, то вызывается фактическая служба пользователя, а я не хочу этого делать. Я ожидаю, что, поскольку у меня есть следующая строка кода

spyOn(userService, "getUsers").and.returnValue(of(FakeData.users));

в блоке beforeEach (), поэтому фактический код не должен выполняться, а вместо этого должны быть возвращены эти жестко закодированные данные. Может ли кто-нибудь помочь и объяснить, что мне не хватает?

ОБНОВЛЕНИЕ. Вот мой последний рабочий код тестов. Спасибо за быструю помощь! Надеюсь, это поможет и кому-то другому :)

import { async, ComponentFixture, TestBed } from "@angular/core/testing";

import { HomeComponent } from "./home.component";
import { UserService } from "../services/user.service";
import { of } from "rxjs";
import { FakeData } from "../fakes/fake-data";
import { By } from "@angular/platform-browser";

describe("HomeComponent", () => {
  let component: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;
  let userServiceSpy: jasmine.SpyObj<UserService>;

  beforeEach(async(() => {
    userServiceSpy = jasmine.createSpyObj(["getUsers"]);

    TestBed.configureTestingModule({
      imports: [],
      declarations: [HomeComponent],
      providers: [{ provide: UserService, useValue: userServiceSpy }]
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(HomeComponent);
    component = fixture.componentInstance;
    userServiceSpy.getUsers.and.returnValue(of(FakeData.users));
    fixture.detectChanges();
  });

  afterEach(() => {
    fixture.destroy();
    component = null;
  });

  it("should create component", () => {
    expect(component).toBeTruthy();
  });

  it("should have 'Users' populated", () => {
    expect(component.users.length).toEqual(2);
  });

  it("should bind 'Users' to the view", () => {
    const userList = fixture.debugElement.queryAll(By.css(".user-item"));
    expect(userList.length).toEqual(2);
  });
});

person Afraz Ali    schedule 09.10.2019    source источник
comment
Я не хочу использовать поддельный сервис, я ищу способ каким-то образом подделать вызов фактического сервиса или создать подделку в тесте с помощью Jasmine и использовать его для возврата данных.   -  person Afraz Ali    schedule 09.10.2019
comment
Думаю, проблема в том, что component.ngOnInit (); не вызывается при выполнении тестов. Если я вызываю его вручную и не помечаю свой тест как fakeAsync или Async, тогда тесты проходят.   -  person Afraz Ali    schedule 09.10.2019


Ответы (1)


Вы должны иметь возможность вообще избегать использования fakeAsync. Я полагаю, что в вашем случае вы звоните fixture.detectChanges() до того, как настраиваете своего шпиона - вот почему вызывается «настоящая» служба.

Создайте своего UserService шпиона в своем тесте и зарегистрируйте его в качестве провайдера HomeComponent:

describe('HomeComponent', () => {
  let comp: HomeComponent;
  let fixture: ComponentFixture<HomeComponent>;

  let userServiceSpy: SpyObj<UserService>;

  beforeEach(async(() => {
    userServiceSpy= createSpyObj(['getUsers']);

    TestBed.configureTestingModule({
      imports: [],
      declarations: [ HomeComponent ],
      schemas: [ NO_ERRORS_SCHEMA ]
    })
    .overrideComponent(HomeComponent , {
      set: {
        providers: [
          { provide: UserService, useValue: userServiceSpy }
        ]
      }
    })
    .compileComponents()
      .then(() => {
        fixture = TestBed.createComponent(HomeComponent);
        comp = fixture.componentInstance;
    });
  }));

  beforeEach(() => {
    userServiceSpy.getUsers.and.returnValue(of(FakeData.users));
    fixture.detectChanges();
  });

  ...
person MS_AU    schedule 09.10.2019
comment
Спасибо за ваш ответ. Я не могу скомпилировать часть кода, который вы разместили, и не уверен, смогу ли я их откуда-то импортировать. createSpyObj (['getUsers']); это функция, которую вы где-то определили? Кроме того, я не уверен в схемах: [NO_ERRORS_SCHEMA] и TestBed.createComponent (TransferComponent); Нужны ли мне NO_ERRORS_SCHEMA и TransferComponent? - person Afraz Ali; 09.10.2019
comment
Извините, это моя ошибка! Я новичок в Angular и не осознавал, что createSpyObj - это функция в Jasmine. Я изменил его на jasmine.createSpyObj ([getUsers]); Теперь код компилируется! - person Afraz Ali; 09.10.2019
comment
Этот код работает отлично! Большое спасибо. Не могли бы вы пояснить мне одну вещь, чтобы избавиться от моего замешательства? Почему импорт пуст в configureTestingModule, хотя фактическая служба зависит от HttpClient? Это потому, что в тесте мы полностью использовали поддельный сервис? - person Afraz Ali; 09.10.2019
comment
Да, именно так. Вам не нужен импорт, так как вы на самом деле не вызываете функцию getUsers и, следовательно, не нуждаетесь в ее зависимостях. - person Chund; 09.10.2019
comment
Извините, я объединял часть своего кода с вашим примером и пропустил импорт. @RonaldHund верен - чтобы тесты могли создать экземпляр HomeComponent, он должен знать, как предоставлять какие-либо зависимости. Первоначально у вас был реальный UserService, установленный в качестве провайдера в вашем тестовом модуле. UserService имеет свою зависимость от HttpClient, поэтому необходимо импортировать HttpClientModule. В новой версии указано useValue: userServiceSpy (так что всякий раз, когда что-то запрашивает UserService, используйте вместо этого шпион). Больше ничего не зависит от HttpClient - поэтому импорт не требуется. - person MS_AU; 09.10.2019