Angular單元測(cè)試與E2E測(cè)試

本文介紹了Angular 7單元測(cè)試和E2E測(cè)試的配置與測(cè)試方法。使用Angular CLI新建工程,已配置好基礎(chǔ)測(cè)試環(huán)境,生成了測(cè)試樣例代碼。默認(rèn),Angular單元測(cè)試使用Jasmine測(cè)試框架和Karma測(cè)試運(yùn)行器,E2E測(cè)試使用Jasmine和Protractor測(cè)試框架。

成都創(chuàng)新互聯(lián)公司專(zhuān)注于夾江網(wǎng)站建設(shè)服務(wù)及定制,我們擁有豐富的企業(yè)做網(wǎng)站經(jīng)驗(yàn)。 熱誠(chéng)為您提供夾江營(yíng)銷(xiāo)型網(wǎng)站建設(shè),夾江網(wǎng)站制作、夾江網(wǎng)頁(yè)設(shè)計(jì)、夾江網(wǎng)站官網(wǎng)定制、微信小程序開(kāi)發(fā)服務(wù),打造夾江網(wǎng)絡(luò)公司原創(chuàng)品牌,更為您提供夾江網(wǎng)站排名全網(wǎng)營(yíng)銷(xiāo)落地服務(wù)。

配置單元測(cè)試

Jasmine是用于測(cè)試JavaScript的行為驅(qū)動(dòng)(Behavior-Driven)框架,不依賴(lài)于任何其他JavaScript框架。
Karma是測(cè)試運(yùn)行器,為開(kāi)發(fā)人員提供了高效、真實(shí)的測(cè)試環(huán)境,支持多種瀏覽器,易于調(diào)試。

配置文件

單元測(cè)試配置文件test.ts和karma.conf.js:
test.ts

import 'zone.js/dist/zone-testing';
import { getTestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';

declare const require: any;

// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);

默認(rèn),測(cè)試文件擴(kuò)展名必須為.spec.ts。
karma.conf.js

module.exports = function (config) {
  config.set({
    basePath: '',
    frameworks: ['jasmine', '@angular-devkit/build-angular'],
    plug×××: [
      require('karma-jasmine'),
      require('karma-chrome-launcher'),
      require('karma-jasmine-html-reporter'),
      require('karma-coverage-istanbul-reporter'),
      require('@angular-devkit/build-angular/plug×××/karma')
    ],
    client: {
      clearContext: false // leave Jasmine Spec Runner output visible in browser
    },
    coverageIstanbulReporter: {
      dir: require('path').join(__dirname, '../coverage'),
      reports: ['html', 'lcovonly'],
      fixWebpackSourcePaths: true
    },
    reporters: ['progress', 'kjhtml'],
    port: 9876,
    colors: true,
    logLevel: config.LOG_INFO,
    autoWatch: true,
    browsers: ['Chrome'],
    singleRun: false
  });
};

默認(rèn),使用Chrome瀏覽器,可生成單元測(cè)試報(bào)告和覆蓋率報(bào)告,覆蓋率報(bào)告保存在根目錄coverage文件夾內(nèi),啟用autoWatch。
singleRun默認(rèn)為false,如設(shè)為true則測(cè)試結(jié)束后會(huì)自動(dòng)退出并根據(jù)測(cè)試結(jié)果返回代碼0或1,常用于CI環(huán)境。

瀏覽器配置

Karma支持的瀏覽器:

  • Chrome
  • ChromeCanary
  • ChromeHeadless
  • PhantomJS
  • Firefox
  • Opera
  • IE
  • Safari

可同時(shí)配置多個(gè)瀏覽器進(jìn)行測(cè)試,要啟用其他瀏覽器,需安裝依賴(lài),比如啟用Firefox:

npm i karma-firefox-launcher --save-dev

然后在karma.conf.js內(nèi)增加配置:

...
require('karma-chrome-launcher'),
require('karma-firefox-launcher'),
...
browsers: ['Chrome', 'Firefox'],
...

運(yùn)行測(cè)試

用CLI創(chuàng)建App生成了一個(gè)單元測(cè)試文件app.component.spec.ts。執(zhí)行CLI命令ng test即可運(yùn)行單元測(cè)試:

ng test

運(yùn)行后在控制臺(tái)輸出測(cè)試結(jié)果并打開(kāi)瀏覽器:
Angular單元測(cè)試與E2E測(cè)試
瀏覽器會(huì)顯示測(cè)試結(jié)果,總測(cè)試數(shù),失敗數(shù)。在頂部,每個(gè)點(diǎn)或叉對(duì)應(yīng)一個(gè)測(cè)試用例,點(diǎn)表示成功,叉表示失敗,鼠標(biāo)移到點(diǎn)或叉上會(huì)顯示測(cè)試信息。點(diǎn)擊測(cè)試結(jié)果中的某一行,可重新運(yùn)行某個(gè)或某組(測(cè)試套件)測(cè)試。

常用參數(shù):
--browsers 指定使用的瀏覽器
--code-coverage 輸出覆蓋率報(bào)告
--code-coverage-exclude 排除文件或路徑
--karma-config 指定Karma配置文件
--prod 啟用production環(huán)境
--progress 默認(rèn)為true,將編譯進(jìn)度輸出到控制臺(tái)
--watch 默認(rèn)為true,代碼修改后會(huì)重新運(yùn)行測(cè)試

自定義Launcher

karma-chrome-launcher、karma-firefox-launcher、karma-ie-launcher等均支持自定義Launcher,customLaunchers與--browsers結(jié)合使用可滿(mǎn)足多種環(huán)境的測(cè)試需求。每種瀏覽器支持的自定義屬性請(qǐng)查看Karma Browsers文檔。
比如,CI環(huán)境下常用Headless模式,不需顯示瀏覽器界面,在karma.conf.js中增加如下配置:

browsers: ['Chrome'],
customLaunchers: {
  ChromeHeadlessCI: {
    base: 'ChromeHeadless',
    flags: ['--no-sandbox']
  }
},

運(yùn)行如下命令進(jìn)行測(cè)試:

ng test --watch=false --progress=false --browsers=ChromeHeadlessCI

測(cè)試覆蓋率

運(yùn)行如下命令生成測(cè)試覆蓋率報(bào)告,報(bào)告保存在項(xiàng)目根目錄下的coverage文件夾內(nèi):

ng test --watch=false --code-coverage

如想每次測(cè)試都生成報(bào)告,可修改CLI配置文件angular.json:

"test": {
  "options": {
    "codeCoverage": true
  }
}

設(shè)置排除的文件或路徑

ng test --watch=false --code-coverage --code-coverage-exclude=src/app/heroes/heroes.component.ts --code-coverage-exclude=src/app/hero-search/*

同樣可以在angular.json中配置:

"test": {
  "options": {
    "codeCoverage": true,
    "codeCoverageExclude": ["src/app/heroes/heroes.component.ts", "src/app/hero-search/*"]
  }
}

設(shè)定測(cè)試覆蓋率指標(biāo)
編輯配置文件karma.conf.js,增加如下內(nèi)容:

coverageIstanbulReporter: {
  reports: [ 'html', 'lcovonly' ],
  fixWebpackSourcePaths: true,
  thresholds: {
    statements: 80,
    lines: 80,
    branches: 80,
    functions: 80
  }
}

測(cè)試報(bào)告中達(dá)到標(biāo)準(zhǔn)的背景為綠色:
Angular單元測(cè)試與E2E測(cè)試
注意:與CI集成時(shí)不要設(shè)置覆蓋率指標(biāo),否則若未到達(dá)指標(biāo),Job會(huì)終止。
LCOV
coverageIstanbulReporter中reports參數(shù)為[ 'html', 'lcovonly' ],會(huì)生成html和lcov兩種格式的報(bào)告。報(bào)告文件lcov.info可與Sonar集成,在Sonar管理界面配置LCOV Files路徑,即可在Sonar中查看測(cè)試情況。
Angular單元測(cè)試與E2E測(cè)試
另外,與Sonar集成時(shí)需配置TypeScript Exclusions,排除.spec.ts,否則統(tǒng)計(jì)覆蓋率時(shí)將包含測(cè)試文件。
Angular單元測(cè)試與E2E測(cè)試

編寫(xiě)測(cè)試

第一個(gè)測(cè)試

使用CLI創(chuàng)建Service、Component等時(shí)會(huì)自動(dòng)創(chuàng)建測(cè)試文件,我們以創(chuàng)建App時(shí)生成的測(cè)試文件app.component.spec.ts為例:

import {async, TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component';

describe('AppComponent', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));

  it('should create the app', () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app).toBeTruthy();
  });

  it(`should have as title 'hello'`, () => {
    const fixture = TestBed.createComponent(AppComponent);
    const app = fixture.debugElement.componentInstance;
    expect(app.title).toEqual('hello');
  });

  it('should render title in a h2 tag', () => {
    const fixture = TestBed.createComponent(AppComponent);
    fixture.detectChanges();
    const compiled = fixture.debugElement.nativeElement;
    expect(compiled.querySelector('h2').textContent).toContain('Welcome to hello!');
  });
});

測(cè)試結(jié)構(gòu)
從上例我們可以了解測(cè)試的主要結(jié)構(gòu):
describe函數(shù)中包含了beforeEach和it兩類(lèi)函數(shù)。describe相當(dāng)于Java測(cè)試中的suite,也就是測(cè)試組,其中可以包含多個(gè)測(cè)試用例it。一般一個(gè)測(cè)試文件含有一個(gè)describe,當(dāng)然也可以有多個(gè)。beforeEach相當(dāng)于Java測(cè)試中的@Before方法,每個(gè)測(cè)試用例執(zhí)行前調(diào)用一次。同樣,還有afterEach、beforeAll、afterAll函數(shù),afterEach在每個(gè)測(cè)試用例執(zhí)行后調(diào)用一次,beforeAll、afterAll相當(dāng)于Java測(cè)試中的@BeforeClass、@AfterClass方法,每個(gè)describe執(zhí)行前后調(diào)用一次。

describe和it的第一個(gè)參數(shù)是測(cè)試說(shuō)明。一個(gè)it中可以包含一個(gè)或多個(gè)expect來(lái)執(zhí)行測(cè)試驗(yàn)證。

TestBed
TestBed是Angular測(cè)試中最重要的工具。

TestBed.configureTestingModule()方法動(dòng)態(tài)構(gòu)建TestingModule來(lái)模擬Angular @NgModule,支持@NgModule的大多數(shù)屬性。

測(cè)試中需導(dǎo)入測(cè)試的組件及依賴(lài)。在A(yíng)ppComponent頁(yè)面中使用了router-outlet,因此我們導(dǎo)入了RouterTestingModule來(lái)模擬RouterModule。Test Module預(yù)配置了一些元素,比如BrowserModule,不需導(dǎo)入。

TestBed.createComponent()方法創(chuàng)建組件實(shí)例,返回ComponentFixture。ComponentFixture是一個(gè)測(cè)試工具(test harness),用于與創(chuàng)建的組件和相應(yīng)元素進(jìn)行交互。

nativeElement和DebugElement
示例中使用了fixture.debugElement.nativeElement,也可以寫(xiě)成fixture.nativeElement。實(shí)際上,fixture.nativeElement是fixture.debugElement.nativeElement的一種簡(jiǎn)化寫(xiě)法。nativeElement依賴(lài)于運(yùn)行時(shí)環(huán)境,Angular依賴(lài)DebugElement抽象來(lái)支持跨平臺(tái)。Angular創(chuàng)建DebugElement tree來(lái)包裝native element,nativeElement返回平臺(tái)相關(guān)的元素對(duì)象。

我們的測(cè)試樣例僅運(yùn)行在瀏覽器中,因此nativeElement總為HTMLElement,可以使用querySelector()、querySelectorAll()方法來(lái)查詢(xún)?cè)亍?/p>

element.querySelector('p');
element.querySelector('input');
element.querySelector('.welcome');
element.querySelectorAll('span');

detectChanges
createComponent() 函數(shù)不會(huì)綁定數(shù)據(jù),必須調(diào)用fixture.detectChanges()來(lái)執(zhí)行數(shù)據(jù)綁定,才能在組件元素中取得內(nèi)容:

it('should render title in a h2 tag', () => {
  const fixture = TestBed.createComponent(AppComponent);
  fixture.detectChanges();
  const compiled = fixture.debugElement.nativeElement;
  expect(compiled.querySelector('h2').textContent).toContain('Welcome to hello!');
});

當(dāng)數(shù)據(jù)模型值改變后,也需調(diào)用fixture.detectChanges()方法:

it('should render title in a h2 tag', () => {
  const fixture = TestBed.createComponent(AppComponent);
  const app = fixture.componentInstance;
  app.title = 'china';
  fixture.detectChanges();
  const compiled = fixture.nativeElement;
  expect(compiled.querySelector('h2').textContent).toContain('Welcome to china!');
});

可以配置自動(dòng)檢測(cè),增加ComponentFixtureAutoDetect provider:

import { ComponentFixtureAutoDetect } from '@angular/core/testing';
...
TestBed.configureTestingModule({
  providers: [
    { provide: ComponentFixtureAutoDetect, useValue: true }
  ]
});

啟用自動(dòng)檢測(cè)后僅需在數(shù)值改變后調(diào)用detectChanges():

it('should display original title', () => {
  // Hooray! No `fixture.detectChanges()` needed
  expect(h2.textContent).toContain(comp.title);
});

it('should still see original title after comp.title change', () => {
  const oldTitle = comp.title;
  comp.title = 'Test Title';
  // Displayed title is old because Angular didn't hear the change :(
  expect(h2.textContent).toContain(oldTitle);
});

it('should display updated title after detectChanges', () => {
  comp.title = 'Test Title';
  fixture.detectChanges(); // detect changes explicitly
  expect(h2.textContent).toContain(comp.title);
});

同步和異步beforeEach
組件常用 @Component.templateUrl 和 @Component.styleUrls 屬性來(lái)指定外部模板和CSS,Angular編譯器會(huì)在編譯期間讀取外部文件。

@Component({
  selector: 'app-banner',
  templateUrl: './banner-external.component.html',
  styleUrls:  ['./banner-external.component.css']
})
beforeEach(() => {
  TestBed.configureTestingModule({
    declarations: [ BannerComponent ],
  });
  fixture = TestBed.createComponent(BannerComponent);
});

當(dāng)用CLI的ng test命令運(yùn)行含有如上同步beforeEach方法的測(cè)試時(shí)沒(méi)有問(wèn)題,因?yàn)闀?huì)在運(yùn)行測(cè)試之前先編譯。若在非CLI環(huán)境下運(yùn)行這些測(cè)試則可能失敗。要解決這個(gè)問(wèn)題,可以調(diào)用compileComponents()進(jìn)行顯示的編譯。compileComponents()方法是異步的,必須在async()方法中調(diào)用:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [
      RouterTestingModule
    ],
    declarations: [
      AppComponent
    ],
  }).compileComponents();
}));

調(diào)用compileComponents()會(huì)關(guān)閉當(dāng)前的TestBed實(shí)例,不再允許進(jìn)行配置,不能再調(diào)用任何TestBed中的配置方法,既不能調(diào) configureTestingModule(),也不能調(diào)用任何 override... 方法。

常同時(shí)使用同步beforeEach和異步beforeEach來(lái)協(xié)同工作,異步的 beforeEach() 負(fù)責(zé)編譯組件,同步的beforeEach()負(fù)責(zé)執(zhí)行其余的準(zhǔn)備代碼。測(cè)試運(yùn)行器會(huì)先調(diào)用異步beforeEach方法,運(yùn)行完畢后再調(diào)用同步方法。

重構(gòu)
示例中重復(fù)代碼較多,我們用兩個(gè)beforeEach來(lái)簡(jiǎn)化一下:

import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {RouterTestingModule} from '@angular/router/testing';
import {AppComponent} from './app.component';

describe('AppComponent', () => {
  let fixture: ComponentFixture<AppComponent>;
  let app: AppComponent;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        RouterTestingModule
      ],
      declarations: [
        AppComponent
      ],
    }).compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(AppComponent);
    app = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create the app', () => {
    expect(app).toBeTruthy();
  });

  it(`should have as title 'hello'`, () => {
    expect(app.title).toEqual('hello');
  });

  it('should render title in a h2 tag', () => {
    const compiled = fixture.nativeElement;
    expect(compiled.querySelector('h2').textContent).toContain('Welcome to hello!');
  });
});

也可以把這兩個(gè) beforeEach() 重構(gòu)成一個(gè)異步的beforeEach():

beforeEach(async(() => {
  TestBed.configureTestingModule({
     imports: [
        RouterTestingModule
      ],
      declarations: [
        AppComponent
      ],
  })
  .compileComponents()
  .then(() => {
    fixture = TestBed.createComponent(AppComponent);
    app = fixture.componentInstance;
    fixture.detectChanges();
  });
}));

依賴(lài)注入與Mock

對(duì)簡(jiǎn)單對(duì)象進(jìn)行測(cè)試可以用new創(chuàng)建實(shí)例:

describe('ValueService', () => {
  let service: ValueService;
  beforeEach(() => { service = new ValueService(); });
    ...
});

不過(guò)大多數(shù)Service、Component等有多個(gè)依賴(lài)項(xiàng),使用new很不方便。若用DI來(lái)創(chuàng)建測(cè)試對(duì)象,當(dāng)依賴(lài)其他服務(wù)時(shí),DI會(huì)找到或創(chuàng)建依賴(lài)的服務(wù)。要測(cè)試某個(gè)對(duì)象,在configureTestingModule中配置測(cè)試對(duì)象本身及依賴(lài)項(xiàng),然后調(diào)用TestBed.get()注入測(cè)試對(duì)象:

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

單元測(cè)試的原則之一:僅對(duì)要測(cè)試對(duì)象本身進(jìn)行測(cè)試,而不對(duì)其依賴(lài)項(xiàng)進(jìn)行測(cè)試,依賴(lài)項(xiàng)通過(guò)mock方式注入,而不使用實(shí)際的對(duì)象,否則測(cè)試不可控。

Mock優(yōu)先使用Spy方式:

let masterService: MasterService;

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

  TestBed.configureTestingModule({
    // Provide both the service-to-test and its (spy) dependency
    providers: [
      MasterService,
      { provide: ValueService, useValue: spy }
    ]
  });

  masterService = TestBed.get(MasterService);
});

HttpClient、Router、Location

同測(cè)試含其它依賴(lài)的對(duì)象一樣,可以mock HttpClient、Router、Location:

beforeEach(() => {
  const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);

  TestBed.configureTestingModule({
    providers: [
      {provide: HttpClient, useValue: httpClientSpy}
    ]
  });
});
beforeEach(async(() => {
  const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
  const locationSpy = jasmine.createSpyObj('Location', ['back']);

  TestBed.configureTestingModule({
    providers: [
      {provide: Router, useValue: routerSpy},
      {provide: Location, useValue: locationSpy}
    ]
  })
    .compileComponents();
}));

Component測(cè)試

  • 僅測(cè)試組件類(lèi)

測(cè)試組件類(lèi)就像測(cè)試服務(wù)那樣簡(jiǎn)單:
組件類(lèi)

export class WelcomeComponent  implements OnInit {
  welcome: string;
  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.welcome = this.userService.isLoggedIn ?
      'Welcome, ' + this.userService.user.name : 'Please log in.';
  }
}

Mock類(lèi)

class MockUserService {
  isLoggedIn = true;
  user = { name: 'Test User'};
};

測(cè)試

...
beforeEach(() => {
  TestBed.configureTestingModule({
    // provide the component-under-test and dependent service
    providers: [
      WelcomeComponent,
      { provide: UserService, useClass: MockUserService }
    ]
  });
  // inject both the component and the dependent service.
  comp = TestBed.get(WelcomeComponent);
  userService = TestBed.get(UserService);
});
...
it('should ask user to log in if not logged in after ngOnInit', () => {
  userService.isLoggedIn = false;
  comp.ngOnInit();
  expect(comp.welcome).not.toContain(userService.user.name);
  expect(comp.welcome).toContain('log in');
});
  • 組件DOM測(cè)試

只涉及類(lèi)的測(cè)試可以判斷組件類(lèi)的行為是否正常,但不能確定組件是否能正常渲染和交互。
進(jìn)行組件DOM測(cè)試,需要使用TestBed.createComponent()等方法,第一個(gè)測(cè)試即為組件DOM測(cè)試。

TestBed.configureTestingModule({
  declarations: [ BannerComponent ]
});
const fixture = TestBed.createComponent(BannerComponent);
const component = fixture.componentInstance;
expect(component).toBeDefined();

dispatchEvent
為模擬用戶(hù)輸入,比如為input元素輸入值,要找到input元素并設(shè)置它的 value 屬性。Angular不知道你設(shè)置了input元素的value屬性,需要調(diào)用 dispatchEvent() 觸發(fā)輸入框的 input 事件,再調(diào)用 detectChanges():

it('should convert hero name to Title Case', () => {
  // get the name's input and display elements from the DOM
  const hostElement = fixture.nativeElement;
  const nameInput: HTMLInputElement = hostElement.querySelector('input');
  const nameDisplay: HTMLElement = hostElement.querySelector('span');

  nameInput.value = 'quick BROWN  fOx';

  // dispatch a DOM event so that Angular learns of input value change.
  nameInput.dispatchEvent(newEvent('input'));

  fixture.detectChanges();

  expect(nameDisplay.textContent).toBe('Quick Brown  Fox');
});

嵌套組件

組件中常常使用其他組件:

<app-banner></app-banner>
<app-welcome></app-welcome>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
  <a routerLink="/about">About</a>
</nav>
<router-outlet></router-outlet>

對(duì)于無(wú)害的內(nèi)嵌組件可以直接將其添加到declarations中,這是最簡(jiǎn)單的方式:

describe('AppComponent & TestModule', () => {
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [
        AppComponent,
        BannerComponent,
        WelcomeComponent
      ]
    })
    .compileComponents().then(() => {
      fixture = TestBed.createComponent(AppComponent);
      comp    = fixture.componentInstance;
    });
  }));
  ...
});

也可為無(wú)關(guān)緊要的組件創(chuàng)建一些測(cè)試樁:

@Component({selector: 'app-banner', template: ''})
class BannerStubComponent {}

@Component({selector: 'router-outlet', template: ''})
class RouterOutletStubComponent { }

@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}

然后在TestBed的配置中聲明它們:

TestBed.configureTestingModule({
  declarations: [
    AppComponent,
    BannerStubComponent,
    RouterOutletStubComponent,
    WelcomeStubComponent
  ]
})

另一種辦法是使用NO_ERRORS_SCHEMA,要求 Angular編譯器忽略那些不認(rèn)識(shí)的元素和屬性:

TestBed.configureTestingModule({
  declarations: [
    AppComponent,
    RouterLinkDirectiveStub
  ],
  schemas: [ NO_ERRORS_SCHEMA ]
})

NO_ERRORS_SCHEMA方法比較簡(jiǎn)單,但不要過(guò)度使用。NO_ERRORS_SCHEMA 會(huì)阻止編譯器因疏忽或拼寫(xiě)錯(cuò)誤而缺失的組件和屬性,如人工找出這些 bug會(huì)很費(fèi)時(shí)。
RouterLinkDirectiveStub

import { Directive, Input, HostListener } from '@angular/core';

@Directive({
  selector: '[routerLink]'
})
export class RouterLinkDirectiveStub {
  @Input('routerLink') linkParams: any;
  navigatedTo: any = null;

  @HostListener('click')
  onClick() {
    this.navigatedTo = this.linkParams;
  }
}

屬性指令測(cè)試

import { Directive, ElementRef, Input, OnChanges } from '@angular/core';

@Directive({ selector: '[highlight]' })
/** Set backgroundColor for the attached element to highlight color and set the element's customProperty to true */
export class HighlightDirective implements OnChanges {

  defaultColor =  'rgb(211, 211, 211)'; // lightgray

  @Input('highlight') bgColor: string;

  constructor(private el: ElementRef) {
    el.nativeElement.style.customProperty = true;
  }

  ngOnChanges() {
    this.el.nativeElement.style.backgroundColor = this.bgColor || this.defaultColor;
  }
}

屬性型指令肯定要操縱 DOM,如只針對(duì)類(lèi)測(cè)試不能證明指令的有效性。若通過(guò)組件來(lái)測(cè)試,單一的用例一般無(wú)法探索指令的全部能力。因此,更好的方法是創(chuàng)建一個(gè)能展示該指令所有用法的人造測(cè)試組件:

@Component({
  template: `
  <h3 highlight="yellow">Something Yellow</h3>
  <h3 highlight>The Default (Gray)</h3>
  <h3>No Highlight</h3>
  <input #box [highlight]="box.value" value="cyan"/>`
})
class TestComponent { }

測(cè)試程序:

beforeEach(() => {
  fixture = TestBed.configureTestingModule({
    declarations: [ HighlightDirective, TestComponent ]
  })
  .createComponent(TestComponent);

  fixture.detectChanges(); // initial binding

  // all elements with an attached HighlightDirective
  des = fixture.debugElement.queryAll(By.directive(HighlightDirective));

  // the h3 without the HighlightDirective
  bareH2 = fixture.debugElement.query(By.css('h3:not([highlight])'));
});

// color tests
it('should have three highlighted elements', () => {
  expect(des.length).toBe(3);
});

it('should color 1st <h3> background "yellow"', () => {
  const bgColor = des[0].nativeElement.style.backgroundColor;
  expect(bgColor).toBe('yellow');
});

it('should color 2nd <h3> background w/ default color', () => {
  const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
  const bgColor = des[1].nativeElement.style.backgroundColor;
  expect(bgColor).toBe(dir.defaultColor);
});

it('should bind <input> background to value color', () => {
  // easier to work with nativeElement
  const input = des[2].nativeElement as HTMLInputElement;
  expect(input.style.backgroundColor).toBe('cyan', 'initial backgroundColor');

  // dispatch a DOM event so that Angular responds to the input value change.
  input.value = 'green';
  input.dispatchEvent(newEvent('input'));
  fixture.detectChanges();

  expect(input.style.backgroundColor).toBe('green', 'changed backgroundColor');
});

it('bare <h3> should not have a customProperty', () => {
  expect(bareH2.properties['customProperty']).toBeUndefined();
});

Pipe測(cè)試

describe('TitleCasePipe', () => {
  // This pipe is a pure, stateless function so no need for BeforeEach
  let pipe = new TitleCasePipe();

  it('transforms "abc" to "Abc"', () => {
    expect(pipe.transform('abc')).toBe('Abc');
  });

  it('transforms "abc def" to "Abc Def"', () => {
    expect(pipe.transform('abc def')).toBe('Abc Def');
  });

  ...
});

Testing Module

RouterTestingModule
在前面的測(cè)試中我們使用了測(cè)試樁RouterOutletStubComponent,與Router有關(guān)的測(cè)試還可以使用RouterTestingModule:

beforeEach(async(() => {
  TestBed.configureTestingModule({
    imports: [
      RouterTestingModule
    ],
    declarations: [
      AppComponent
    ],
  }).compileComponents();
}));

RouterTestingModule還可以模擬路由:

beforeEach(() => {
  TestBed.configureTestModule({
    imports: [
      RouterTestingModule.withRoutes(
        [{path: '', component: BlankCmp}, {path: 'simple', component: SimpleCmp}]
      )
    ]
  });
});

HttpClientTestingModule

describe('HttpClient testing', () => {
  let httpClient: HttpClient;
  let httpTestingController: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ HttpClientTestingModule ]
    });

    // Inject the http service and test controller for each test
    httpClient = TestBed.get(HttpClient);
    httpTestingController = TestBed.get(HttpTestingController);
  });

  afterEach(() => {
    // After every test, assert that there are no more pending requests.
    httpTestingController.verify();
  });

  it('can test HttpClient.get', () => {
    const testData: Data = {name: 'Test Data'};

    // Make an HTTP GET request
    httpClient.get<Data>(testUrl)
      .subscribe(data =>
        // When observable resolves, result should match test data
        expect(data).toEqual(testData)
      );

    // The following `expectOne()` will match the request's URL.
    // If no requests or multiple requests matched that URL
    // `expectOne()` would throw.
    const req = httpTestingController.expectOne('/data');

    // Assert that the request is a GET.
    expect(req.request.method).toEqual('GET');

    // Respond with mock data, causing Observable to resolve.
    // Subscribe callback asserts that correct data was returned.
    req.flush(testData);

    // Finally, assert that there are no outstanding requests.
    httpTestingController.verify();
  });

    ...
});

調(diào)試

在瀏覽器測(cè)試結(jié)果頁(yè)面,點(diǎn)擊“DEBUG”按鈕會(huì)打開(kāi)新瀏標(biāo)簽頁(yè)并重新運(yùn)行測(cè)試程序。按"F12"打開(kāi)調(diào)試界面,然后進(jìn)入Sources找到測(cè)試文件(CTRL+P),在測(cè)試程序中設(shè)置斷點(diǎn)即可調(diào)試。

配置E2E測(cè)試

E2E測(cè)試使用Jasmine和Protractor測(cè)試框架,Protractor是Angular端到端測(cè)試框架。

安裝Protractor

npm i -g protractor

安裝后,node_modules\protractor\bin目錄含有兩個(gè)命令行工具protractor和webdriver-manager,其中webdriver-manager負(fù)責(zé)管理驅(qū)動(dòng)、啟停Selenium Server。

webdriver-manager命令:

clean      removes all downloaded driver files from the out_dir
start      start up the selenium server
shutdown   shut down the selenium server
status     list the current available drivers
update     update selected binaries
version    get the current version

更新驅(qū)動(dòng):

webdriver-manager update

默認(rèn)安裝chromedriver、geckodriver和selenium standalone,驅(qū)動(dòng)目錄為node_modules\protractor\node_modules\webdriver-manager\selenium,下載使用的url配置在webdriver-manager\config.json文件內(nèi):

"cdnUrls": {
  "selenium": "https://selenium-release.storage.googleapis.com/",
  "chromedriver": "https://chromedriver.storage.googleapis.com/",
  "geckodriver": "https://github.com/mozilla/geckodriver/releases/download/",
  "iedriver": "https://selenium-release.storage.googleapis.com/",
  "androidsdk": "http://dl.google.com/android/"
}

可以修改為其它CDN:

"cdnUrls": {
  "selenium": "https://mirrors.huaweicloud.com/selenium/",
  "chromedriver": "https://mirrors.huaweicloud.com/chromedriver/",
  "geckodriver": "https://mirrors.huaweicloud.com/geckodriver/",
  "iedriver": "https://selenium-release.storage.googleapis.com/",
  "androidsdk": "http://dl.google.com/android/"
}

也可以使用參數(shù)--alternate_cdn:

webdriver-manager update --alternate_cdn=...

配置文件

使用CLI創(chuàng)建的App會(huì)生成一個(gè)e2e項(xiàng)目,其中包含測(cè)試配置protractor.conf.js及測(cè)試代碼。
protractor.conf.js

const { SpecReporter } = require('jasmine-spec-reporter');

exports.config = {
  allScriptsTimeout: 11000,
  specs: [
    './src/**/*.e2e-spec.ts'
  ],
  capabilities: {
    'browserName': 'chrome'
  },
  directConnect: true,
  baseUrl: 'http://localhost:4200/',
  framework: 'jasmine',
  jasmineNodeOpts: {
    showColors: true,
    defaultTimeoutInterval: 30000,
    print: function() {}
  },
  onPrepare() {
    require('ts-node').register({
      project: require('path').join(__dirname, './tsconfig.e2e.json')
    });
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
  }
};

默認(rèn),Protractor使用Jasmine測(cè)試框架,使用直連方式連接Chrome瀏覽器,測(cè)試文件擴(kuò)展名為.e2e-spec.ts。

瀏覽器配置

Protractor支持Chrome、Firefox、Safari、IE等瀏覽器。
多瀏覽器
Protractor支持同時(shí)啟動(dòng)多個(gè)瀏覽器,一個(gè)瀏覽器時(shí),在配置中使用capabilities選項(xiàng);多個(gè)瀏覽器時(shí),使用multiCapabilities:

multiCapabilities: [{
  browserName: 'firefox'
}, {
  browserName: 'chrome'
}]

另外需在package.json中增加配置:

"scripts": {
  "webdriver-update": "webdriver-manager update"
}

在運(yùn)行測(cè)試前更新瀏覽器驅(qū)動(dòng):

npm run webdriver-update

否則項(xiàng)目中的驅(qū)動(dòng)不會(huì)更新(默認(rèn)只有chrome驅(qū)動(dòng),運(yùn)行webdriver-manager update僅更新全局的驅(qū)動(dòng)),運(yùn)行測(cè)試會(huì)報(bào)如下錯(cuò)誤:

No update-config.json found. Run 'webdriver-manager update' to download binaries

瀏覽器選項(xiàng)

capabilities: {
  'browserName': 'chrome',
  'chromeOptions': {
    'args': ['show-fps-counter=true']
  }
},
capabilities: {
  'browserName': 'firefox',
  'moz:firefoxOptions': {
    'args': ['--safe-mode']
  }
},

更多選項(xiàng)請(qǐng)查看相應(yīng)驅(qū)動(dòng)ChromeDriver、GeckoDriver。

Selenium Server配置

使用Standalone Selenium Server時(shí),需安裝JDK。
更新driver后啟動(dòng)Selenium Server:

webdriver-manager update
webdriver-manager start

刪除原配置中的directConnect、baseUrl:

directConnect: true,
baseUrl: 'http://localhost:4200/',

增加seleniumAddress(默認(rèn)為http://localhost:4444/wd/hub):

seleniumAddress: 'http://localhost:4444/wd/hub',

運(yùn)行測(cè)試

運(yùn)行E2E測(cè)試:

ng e2e

常用參數(shù):

--base-url  Base URL for protractor to connect to.
--configuration (-c)  A named configuration environment, as specified in the "configurations" section of angular.json.
--host  Host to listen on.
--port  The port to use to serve the application.
--prod  When true, sets the build configuration to the production environment.
--protractor-config  The name of the Protractor configuration file.
--webdriver-update  Try to update webdriver.

driver安裝好后,若未更新瀏覽器,不必每次都更新driver:

ng e2e --webdriver-update=false

如運(yùn)行測(cè)試時(shí)報(bào)如下錯(cuò)誤:

events.js:167
      throw er; // Unhandled 'error' event
      ^
Error: read ECONNRESET
    at TLSWrap.onStreamRead (internal/stream_base_commons.js:111:27)
Emitted 'error' event

可嘗試更新package后再測(cè)試:

npm i npm@latest -g
npm update

指定配置文件

不同的環(huán)境若配置不同,可使用不同的配置文件。

比如,在CI環(huán)境中啟用Chrome Headless模式:
在e2e根目錄下創(chuàng)建一名為protractor-ci.conf.js的新文件,內(nèi)容如下:

const config = require('./protractor.conf').config;

config.capabilities = {
  browserName: 'chrome',
  chromeOptions: {
    args: ['--headless', '--no-sandbox']
  }
};

exports.config = config;

注意: windows系統(tǒng)要增加參數(shù)--disable-gpu

運(yùn)行以下命令測(cè)試:

ng e2e --protractor-config=e2e\protractor-ci.conf.js --webdriver-update=false

編寫(xiě)E2E測(cè)試

第一個(gè)測(cè)試

import { AppPage } from './app.po';

describe('workspace-project App', () => {
  let page: AppPage;

  beforeEach(() => {
    page = new AppPage();
  });

  it('should display welcome message', () => {
    page.navigateTo();
    expect(page.getTitleText()).toEqual('Welcome to hello!');
  });
});
import { browser, by, element } from 'protractor';

export class AppPage {
  navigateTo() {
    return browser.get('/');
  }

  getTitleText() {
    return element(by.css('app-root h2')).getText();
  }
}

E2E測(cè)試與單元測(cè)試都使用了Jasmine,測(cè)試結(jié)構(gòu)相同。Protractor提供了全局的browser、element、by,分別用來(lái)打開(kāi)頁(yè)面和查找元素。

Protractor

describe('Protractor Demo App', function() {
  it('should add one and two', function() {
    browser.get('http://juliemr.github.io/protractor-demo/');
    element(by.model('first')).sendKeys(1);
    element(by.model('second')).sendKeys(2);

    element(by.id('gobutton')).click();

    expect(element(by.binding('latest')).getText()).
        toEqual('5'); // This is wrong!
  });
});
  • by.model('first') 查找元素ng-model="first"
  • by.id('gobutton') 根據(jù)id查找元素
  • by.binding('latest') 查找綁定變量的元素 {{latest}}

2018上海馬拉松
Angular單元測(cè)試與E2E測(cè)試

參考資料

Angular Testing
Jasmine Behavior-Driven JavaScript
Karma
Protractor - end-to-end testing for Angular

文章標(biāo)題:Angular單元測(cè)試與E2E測(cè)試
文章路徑:http://muchs.cn/article6/phoiog.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供企業(yè)網(wǎng)站制作App設(shè)計(jì)、云服務(wù)器標(biāo)簽優(yōu)化、網(wǎng)站內(nèi)鏈用戶(hù)體驗(yàn)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶(hù)投稿、用戶(hù)轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話(huà):028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

營(yíng)銷(xiāo)型網(wǎng)站建設(shè)