Angular 測(cè)試服務(wù)

2022-07-07 10:18 更新

測(cè)試服務(wù)

為了檢查你的服務(wù)是否正常工作,你可以專門(mén)為它們編寫(xiě)測(cè)試。

如果你要試驗(yàn)本指南中所講的應(yīng)用,請(qǐng)在瀏覽器中運(yùn)行它下載并在本地運(yùn)行它。

服務(wù)往往是最容易進(jìn)行單元測(cè)試的文件。下面是一些針對(duì) ?ValueService ?的同步和異步單元測(cè)試,甚至不需要 Angular 測(cè)試工具的幫助。

// 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();
    });
  });
});

有依賴的服務(wù)

服務(wù)通常依賴于 Angular 在構(gòu)造函數(shù)中注入的其它服務(wù)。在很多情況下,調(diào)用服務(wù)的構(gòu)造函數(shù)時(shí),很容易手動(dòng)創(chuàng)建和注入這些依賴。

?MasterService ?就是一個(gè)簡(jiǎn)單的例子:

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

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

這里有幾種測(cè)試方法。

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);
  });
});

第一個(gè)測(cè)試使用 ?new ?創(chuàng)建了一個(gè) ?ValueService?,并把它傳給了 ?MasterService ?的構(gòu)造函數(shù)。

然而,注入真實(shí)服務(wù)很難工作良好,因?yàn)榇蠖鄶?shù)被依賴的服務(wù)都很難創(chuàng)建和控制。

相反,可以模擬依賴、使用仿制品,或者在相關(guān)的服務(wù)方法上創(chuàng)建一個(gè)測(cè)試間諜。

我更喜歡用測(cè)試間諜,因?yàn)樗鼈兺ǔJ悄M服務(wù)的最佳途徑。

這些標(biāo)準(zhǔn)的測(cè)試技巧非常適合對(duì)服務(wù)進(jìn)行單獨(dú)測(cè)試。

但是,你幾乎總是使用 Angular 依賴注入機(jī)制來(lái)將服務(wù)注入到應(yīng)用類中,你應(yīng)該有一些測(cè)試來(lái)體現(xiàn)這種使用模式。Angular 測(cè)試實(shí)用工具可以讓你輕松調(diào)查這些注入服務(wù)的行為。

使用 TestBed 測(cè)試服務(wù)

你的應(yīng)用依靠 Angular 的依賴注入(DI)來(lái)創(chuàng)建服務(wù)。當(dāng)服務(wù)有依賴時(shí),DI 會(huì)查找或創(chuàng)建這些被依賴的服務(wù)。如果該被依賴的服務(wù)還有自己的依賴,DI 也會(huì)查找或創(chuàng)建它們。

作為服務(wù)的消費(fèi)者,你不應(yīng)該關(guān)心這些。你不應(yīng)該關(guān)心構(gòu)造函數(shù)參數(shù)的順序或它們是如何創(chuàng)建的。

作為服務(wù)的測(cè)試人員,你至少要考慮第一層的服務(wù)依賴,但當(dāng)你用 ?TestBed ?測(cè)試實(shí)用工具來(lái)提供和創(chuàng)建服務(wù)時(shí),你可以讓 Angular DI 來(lái)創(chuàng)建服務(wù)并處理構(gòu)造函數(shù)的參數(shù)順序。

Angular TestBed

?TestBed ?是 Angular 測(cè)試實(shí)用工具中最重要的。?TestBed ?創(chuàng)建了一個(gè)動(dòng)態(tài)構(gòu)造的 Angular 測(cè)試模塊,用來(lái)模擬一個(gè) Angular 的 ?@NgModule?。

?TestBed.configureTestingModule()? 方法接受一個(gè)元數(shù)據(jù)對(duì)象,它可以擁有?@NgModule?的大部分屬性。

要測(cè)試某個(gè)服務(wù),你可以在元數(shù)據(jù)屬性 ?providers ?中設(shè)置一個(gè)要測(cè)試或模擬的服務(wù)數(shù)組。

let service: ValueService;

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

將服務(wù)類作為參數(shù)調(diào)用 ?TestBed.inject()?,將它注入到測(cè)試中。

注意:
?TestBed.get()? 已在 Angular 9 中棄用。為了幫助減少重大變更,Angular 引入了一個(gè)名為 ?TestBed.inject()? 的新函數(shù),你可以改用它。
it('should use ValueService', () => {
  service = TestBed.inject(ValueService);
  expect(service.getValue()).toBe('real value');
});

或者,如果你喜歡把這個(gè)服務(wù)作為設(shè)置代碼的一部分進(jìn)行注入,也可以在 ?beforeEach()? 中做。

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

測(cè)試帶依賴的服務(wù)時(shí),需要在 ?providers ?數(shù)組中提供 mock。

在下面的例子中,mock 是一個(gè)間諜對(duì)象。

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>;
});

該測(cè)試會(huì)像以前一樣使用該間諜。

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);
});

沒(méi)有 beforeEach() 的測(cè)試

本指南中的大多數(shù)測(cè)試套件都會(huì)調(diào)用 ?beforeEach()? 來(lái)為每一個(gè) ?it()? 測(cè)試設(shè)置前置條件,并依賴 ?TestBed ?來(lái)創(chuàng)建類和注入服務(wù)。

還有另一種測(cè)試,它們從不調(diào)用 ?beforeEach()?,而是更喜歡顯式地創(chuàng)建類,而不是使用 ?TestBed?。

你可以用這種風(fēng)格重寫(xiě) ?MasterService ?中的一個(gè)測(cè)試。

首先,在 setup 函數(shù)中放入可供復(fù)用的預(yù)備代碼,而不用 ?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()? 函數(shù)返回一個(gè)包含測(cè)試可能引用的變量(如 ?masterService?)的對(duì)象字面量。你并沒(méi)有在 ?describe()? 的函數(shù)體中定義半全局變量(比如 ?let masterService: MasterService?)。

然后,每個(gè)測(cè)試都會(huì)在第一行調(diào)用 ?setup()?,然后繼續(xù)執(zhí)行那些操縱被測(cè)主體和斷言期望值的步驟。

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);
});

請(qǐng)注意測(cè)試如何使用解構(gòu)賦值來(lái)提取它需要的設(shè)置變量。

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

許多開(kāi)發(fā)人員都覺(jué)得這種方法比傳統(tǒng)的 ?beforeEach()? 風(fēng)格更清晰明了。

雖然這個(gè)測(cè)試指南遵循傳統(tǒng)的樣式,并且默認(rèn)的CLI 原理圖會(huì)生成帶有 ?beforeEach()? 和 ?TestBed ?的測(cè)試文件,但你可以在自己的項(xiàng)目中采用這種替代方式。

測(cè)試 HTTP 服務(wù)

對(duì)遠(yuǎn)程服務(wù)器進(jìn)行 HTTP 調(diào)用的數(shù)據(jù)服務(wù)通常會(huì)注入并委托給 Angular 的 ?HttpClient?服務(wù)進(jìn)行 XHR 調(diào)用。

你可以測(cè)試一個(gè)注入了 ?HttpClient ?間諜的數(shù)據(jù)服務(wù),就像測(cè)試所有帶依賴的服務(wù)一樣。

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 ?方法會(huì)返回 ?Observables?。你必須訂閱一個(gè)可觀察對(duì)象(a)讓它執(zhí)行,(b)斷言該方法成功或失敗。
?subscribe()? 方法會(huì)接受成功(?next?)和失敗(?error?)回調(diào)。確保你會(huì)同時(shí)提供這兩個(gè)回調(diào)函數(shù),以便捕獲錯(cuò)誤。如果不這樣做就會(huì)產(chǎn)生一個(gè)異步的、沒(méi)有被捕獲的可觀察對(duì)象的錯(cuò)誤,測(cè)試運(yùn)行器可能會(huì)把它歸因于一個(gè)完全不相關(guān)的測(cè)試。

HttpClientTestingModule

數(shù)據(jù)服務(wù)和 ?HttpClient ?之間的擴(kuò)展交互可能比較復(fù)雜,并且難以通過(guò)間諜進(jìn)行模擬。

?HttpClientTestingModule ?可以讓這些測(cè)試場(chǎng)景更易于管理。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)