为什么我的模拟服务 属性 在 Jasmine 单元测试中未定义?

Why my mocked service property is undefined in Jasmine unit test?

我正在学习 Angular 使用 Jasmine 进行单元测试。我正在关注 documentation`s guide for testing services,但对于教程中的另一项服务,也非常简单。

SUT:

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { MessageService } from './message.service';

@Injectable({ providedIn: 'root' })
export class HeroService {

  constructor(private messageService: MessageService) { }

  getHeroes(): Observable<Hero[]> {
    // TODO: send the message _after_ fetching the heroes
    this.messageService.add('HeroService: fetched heroes');
    return of(HEROES);
  }

  getHero(id: number): Observable<Hero> {
    // TODO: send the message _after_ fetching the hero
    this.messageService.add(`HeroService: fetched hero id=${id}`);
    return of(HEROES.find(hero => hero.id === id));
  }
}

并注入 MessageService:

import { HeroService } from './hero.service';
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MessageService {
  messages: string[] = [];

  add(message: string) {
    this.messages.push(message);
  }

  clear() {
    this.messages = [];
  }
}

根据指南,我使用 createSpyObj 模拟了注入服务并调用了 SUT 的 getHeroes

import { MessageService } from './message.service';
import { HeroService } from './hero.service';

describe('HeroService without Angular testing support', () => {
  let heroService: HeroService;
  let messageServiceSpy: MessageService;

  it('#getHeroes should add message to MessageService', () => {
    //prepare
    messageServiceSpy = jasmine.createSpyObj(
      'MessageService',
      ['add'],
      ['messages']
    );
    heroService = new HeroService(messageServiceSpy);
    messageServiceSpy.messages = ['one', 'two'];//only difference with the guide

    //act
    heroService.getHeroes();

    //assert
    expect(messageServiceSpy.messages.length).toBe(3);
    expect(messageServiceSpy.messages[2]).toBe('HeroService: fetched heroes');
  });
});

因为某些原因,在测试的时候,messageServiceSpy.messagesundefined:

TypeError: Cannot read property 'length' of undefined

我做错了什么?

我的最终(Jasmine 3.6)hero.service.spec.ts 文件是:

import { MessageService } from './message.service';
import { HeroService } from './hero.service';

describe('HeroService without Angular testing support', () => {
  let heroService: HeroService;
  let messageServiceSpy = jasmine.createSpyObj(
    'MessageService',
    ['add'],
    ['clear']
  );
  heroService = new HeroService(messageServiceSpy);

  it('#getHeroes should call add() once with string value', () => {
    //prepare
    messageServiceSpy.add.calls.reset();

    //act
    heroService.getHeroes();

    //assert
    expect(messageServiceSpy.add).toHaveBeenCalledTimes(1);
    expect(messageServiceSpy.add).toHaveBeenCalledWith(
      'HeroService: fetched heroes'
    );
  });

  it('#getHero should call add() once with proper string value', () => {
    //prepare
    messageServiceSpy.add.calls.reset();
    let id = 1;

    //act
    heroService.getHero(id);

    //assert
    expect(messageServiceSpy.add).toHaveBeenCalledTimes(1);
    expect(messageServiceSpy.add).toHaveBeenCalledWith(
      'HeroService: fetched hero id=' + id
    );
  });
});

以及 Angular TestBed 支持的版本:

import { MessageService } from './message.service';
import { HeroService } from './hero.service';
import { TestBed } from '@angular/core/testing';

describe('HeroService with Angular testing support', () => {
  let heroService: HeroService;
  let messageServiceSpy: jasmine.SpyObj<MessageService>;

  beforeEach(() => {
    const spy = jasmine.createSpyObj('MessageService', ['add']);

    TestBed.configureTestingModule({
      // Provide both the service-to-test and its (spy) dependency
      providers: [HeroService, { provide: MessageService, useValue: spy }],
    });
    // Inject both the service-to-test and its (spy) dependency
    heroService = TestBed.inject(HeroService);
    messageServiceSpy = TestBed.inject(MessageService) as jasmine.SpyObj<
      MessageService
    >;
  });

  it('#getHeroes should call add() once with string value', () => {
    //prepare
    messageServiceSpy.add.calls.reset();

    //act
    heroService.getHeroes();

    //assert
    expect(messageServiceSpy.add).toHaveBeenCalledTimes(1);
    expect(messageServiceSpy.add).toHaveBeenCalledWith(
      'HeroService: fetched heroes'
    );
  });

  it('#getHero should call add() once with proper string value', () => {
    //prepare
    messageServiceSpy.add.calls.reset();
    let id = 1;

    //act
    heroService.getHero(id);

    //assert
    expect(messageServiceSpy.add).toHaveBeenCalledTimes(1);
    expect(messageServiceSpy.add).toHaveBeenCalledWith(
      'HeroService: fetched hero id=' + id
    );
  });
});

并且没有 TestBed 的方法:

import { MessageService } from './message.service';
import { HeroService } from './hero.service';
import { TestBed } from '@angular/core/testing';

describe('HeroService with Angular testing support', () => {
  function setup() {
    const messageServiceSpy = jasmine.createSpyObj('MessageService', ['add']);
    const heroService = new HeroService(messageServiceSpy);

    return { heroService, messageServiceSpy };
  }
  let heroService: HeroService;
  let messageServiceSpy: jasmine.SpyObj<MessageService>;

  it('#getHeroes should call add() once with string value', () => {
    //prepare
    const { heroService, messageServiceSpy } = setup();

    //act
    heroService.getHeroes();

    //assert
    expect(messageServiceSpy.add).toHaveBeenCalledTimes(1);
    expect(messageServiceSpy.add).toHaveBeenCalledWith(
      'HeroService: fetched heroes'
    );
  });

  it('#getHero should call add() once with proper string value', () => {
    //prepare
    const { heroService, messageServiceSpy } = setup();
    let id = 1;

    //act
    heroService.getHero(id);

    //assert
    expect(messageServiceSpy.add).toHaveBeenCalledTimes(1);
    expect(messageServiceSpy.add).toHaveBeenCalledWith(
      'HeroService: fetched hero id=' + id
    );
  });
});