阅读(2682) (0)

Angular 测试服务

2022-07-01 14:04:45 更新

测试服务

为了检查你的服务是否正常工作,你可以专门为它们编写测试。

如果你要试验本指南中所讲的应用,请在浏览器中运行它下载并在本地运行它

服务往往是最容易进行单元测试的文件。下面是一些针对 ​ValueService ​的同步和异步单元测试,甚至不需要 Angular 测试工具的帮助。

// Straight Jasmine testing without Angular's testing support
describe('ValueService', () => {
  let service: ValueService;
  beforeEach(() => { service = new ValueService(); });

  it('#getValue should return real value', () => {
    expect(service.getValue()).toBe('real value');
  });

  it('#getObservableValue should return value from observable',
    (done: DoneFn) => {
    service.getObservableValue().subscribe(value => {
      expect(value).toBe('observable value');
      done();
    });
  });

  it('#getPromiseValue should return value from a promise',
    (done: DoneFn) => {
    service.getPromiseValue().then(value => {
      expect(value).toBe('promise value');
      done();
    });
  });
});

有依赖的服务

服务通常依赖于 Angular 在构造函数中注入的其它服务。在很多情况下,调用服务的构造函数时,很容易手动创建和注入这些依赖。

MasterService ​就是一个简单的例子:

@Injectable()
export class MasterService {
  constructor(private valueService: ValueService) { }
  getValue() { return this.valueService.getValue(); }
}

MasterService ​只把它唯一的方法 ​getValue ​委托给了所注入的 ​ValueService​。

这里有几种测试方法。

describe('MasterService without Angular testing support', () => {
  let masterService: MasterService;

  it('#getValue should return real value from the real service', () => {
    masterService = new MasterService(new ValueService());
    expect(masterService.getValue()).toBe('real value');
  });

  it('#getValue should return faked value from a fakeService', () => {
    masterService = new MasterService(new FakeValueService());
    expect(masterService.getValue()).toBe('faked service value');
  });

  it('#getValue should return faked value from a fake object', () => {
    const fake =  { getValue: () => 'fake value' };
    masterService = new MasterService(fake as ValueService);
    expect(masterService.getValue()).toBe('fake value');
  });

  it('#getValue should return stubbed value from a spy', () => {
    // create `getValue` spy on an object representing the ValueService
    const valueServiceSpy =
      jasmine.createSpyObj('ValueService', ['getValue']);

    // set the value to return when the `getValue` spy is called.
    const stubValue = 'stub value';
    valueServiceSpy.getValue.and.returnValue(stubValue);

    masterService = new MasterService(valueServiceSpy);

    expect(masterService.getValue())
      .withContext('service returned stub value')
      .toBe(stubValue);
    expect(valueServiceSpy.getValue.calls.count())
      .withContext('spy method was called once')
      .toBe(1);
    expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
      .toBe(stubValue);
  });
});

第一个测试使用 ​new ​创建了一个 ​ValueService​,并把它传给了 ​MasterService ​的构造函数。

然而,注入真实服务很难工作良好,因为大多数被依赖的服务都很难创建和控制。

相反,可以模拟依赖、使用仿制品,或者在相关的服务方法上创建一个测试间谍

我更喜欢用测试间谍,因为它们通常是模拟服务的最佳途径。

这些标准的测试技巧非常适合对服务进行单独测试。

但是,你几乎总是使用 Angular 依赖注入机制来将服务注入到应用类中,你应该有一些测试来体现这种使用模式。Angular 测试实用工具可以让你轻松调查这些注入服务的行为。

使用 TestBed 测试服务

你的应用依靠 Angular 的依赖注入(DI)来创建服务。当服务有依赖时,DI 会查找或创建这些被依赖的服务。如果该被依赖的服务还有自己的依赖,DI 也会查找或创建它们。

作为服务的消费者,你不应该关心这些。你不应该关心构造函数参数的顺序或它们是如何创建的。

作为服务的测试人员,你至少要考虑第一层的服务依赖,但当你用 ​TestBed ​测试实用工具来提供和创建服务时,你可以让 Angular DI 来创建服务并处理构造函数的参数顺序。

Angular TestBed

TestBed ​是 Angular 测试实用工具中最重要的。​TestBed ​创建了一个动态构造的 Angular 测试模块,用来模拟一个 Angular 的 ​@NgModule​。

TestBed.configureTestingModule()​ 方法接受一个元数据对象,它可以拥有​@NgModule​的大部分属性。

要测试某个服务,你可以在元数据属性 ​providers ​中设置一个要测试或模拟的服务数组。

let service: ValueService;

beforeEach(() => {
  TestBed.configureTestingModule({ providers: [ValueService] });
});

将服务类作为参数调用 ​TestBed.inject()​,将它注入到测试中。

注意:
TestBed.get()​ 已在 Angular 9 中弃用。为了帮助减少重大变更,Angular 引入了一个名为 ​TestBed.inject()​ 的新函数,你可以改用它。
it('should use ValueService', () => {
  service = TestBed.inject(ValueService);
  expect(service.getValue()).toBe('real value');
});

或者,如果你喜欢把这个服务作为设置代码的一部分进行注入,也可以在 ​beforeEach()​ 中做。

beforeEach(() => {
  TestBed.configureTestingModule({ providers: [ValueService] });
  service = TestBed.inject(ValueService);
});

测试带依赖的服务时,需要在 ​providers ​数组中提供 mock。

在下面的例子中,mock 是一个间谍对象。

let masterService: MasterService;
let valueServiceSpy: jasmine.SpyObj<ValueService>;

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

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

该测试会像以前一样使用该间谍。

it('#getValue should return stubbed value from a spy', () => {
  const stubValue = 'stub value';
  valueServiceSpy.getValue.and.returnValue(stubValue);

  expect(masterService.getValue())
    .withContext('service returned stub value')
    .toBe(stubValue);
  expect(valueServiceSpy.getValue.calls.count())
    .withContext('spy method was called once')
    .toBe(1);
  expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
    .toBe(stubValue);
});

没有 beforeEach() 的测试

本指南中的大多数测试套件都会调用 ​beforeEach()​ 来为每一个 ​it()​ 测试设置前置条件,并依赖 ​TestBed ​来创建类和注入服务。

还有另一种测试,它们从不调用 ​beforeEach()​,而是更喜欢显式地创建类,而不是使用 ​TestBed​。

你可以用这种风格重写 ​MasterService ​中的一个测试。

首先,在 setup 函数中放入可供复用的预备代码,而不用 ​beforeEach()​。

function setup() {
  const valueServiceSpy =
    jasmine.createSpyObj('ValueService', ['getValue']);
  const stubValue = 'stub value';
  const masterService = new MasterService(valueServiceSpy);

  valueServiceSpy.getValue.and.returnValue(stubValue);
  return { masterService, stubValue, valueServiceSpy };
}

setup()​ 函数返回一个包含测试可能引用的变量(如 ​masterService​)的对象字面量。你并没有在 ​describe()​ 的函数体中定义半全局变量(比如 ​let masterService: MasterService​)。

然后,每个测试都会在第一行调用 ​setup()​,然后继续执行那些操纵被测主体和断言期望值的步骤。

it('#getValue should return stubbed value from a spy', () => {
  const { masterService, stubValue, valueServiceSpy } = setup();
  expect(masterService.getValue())
    .withContext('service returned stub value')
    .toBe(stubValue);
  expect(valueServiceSpy.getValue.calls.count())
    .withContext('spy method was called once')
    .toBe(1);
  expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
    .toBe(stubValue);
});

请注意测试如何使用解构赋值来提取它需要的设置变量。

const { masterService, stubValue, valueServiceSpy } = setup();

许多开发人员都觉得这种方法比传统的 ​beforeEach()​ 风格更清晰明了。

虽然这个测试指南遵循传统的样式,并且默认的CLI 原理图会生成带有 ​beforeEach()​ 和 ​TestBed ​的测试文件,但你可以在自己的项目中采用这种替代方式

测试 HTTP 服务

对远程服务器进行 HTTP 调用的数据服务通常会注入并委托给 Angular 的 ​HttpClient​服务进行 XHR 调用。

你可以测试一个注入了 ​HttpClient ​间谍的数据服务,就像测试所有带依赖的服务一样。

let httpClientSpy: jasmine.SpyObj<HttpClient>;
let heroService: HeroService;

beforeEach(() => {
  // TODO: spy on other methods too
  httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
  heroService = new HeroService(httpClientSpy);
});

it('should return expected heroes (HttpClient called once)', (done: DoneFn) => {
  const expectedHeroes: Hero[] =
    [{ id: 1, name: 'A' }, { id: 2, name: 'B' }];

  httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));

  heroService.getHeroes().subscribe({
    next: heroes => {
      expect(heroes)
        .withContext('expected heroes')
        .toEqual(expectedHeroes);
      done();
    },
    error: done.fail
  });
  expect(httpClientSpy.get.calls.count())
    .withContext('one call')
    .toBe(1);
});

it('should return an error when the server returns a 404', (done: DoneFn) => {
  const errorResponse = new HttpErrorResponse({
    error: 'test 404 error',
    status: 404, statusText: 'Not Found'
  });

  httpClientSpy.get.and.returnValue(asyncError(errorResponse));

  heroService.getHeroes().subscribe({
    next: heroes => done.fail('expected an error, not heroes'),
    error: error  => {
      expect(error.message).toContain('test 404 error');
      done();
    }
  });
});
HeroService ​方法会返回 ​Observables​。你必须订阅一个可观察对象(a)让它执行,(b)断言该方法成功或失败。
subscribe()​ 方法会接受成功(​next​)和失败(​error​)回调。确保你会同时提供这两个回调函数,以便捕获错误。如果不这样做就会产生一个异步的、没有被捕获的可观察对象的错误,测试运行器可能会把它归因于一个完全不相关的测试。

HttpClientTestingModule

数据服务和 ​HttpClient ​之间的扩展交互可能比较复杂,并且难以通过间谍进行模拟。

HttpClientTestingModule ​可以让这些测试场景更易于管理。