angular單元測試準備
- 構建angular框架,並編寫對應的業務、業務測試用例**.spec.ts檔案
- 執行測試
ng test --no-watch --code-coverage //根目錄下會生成coverage目錄,其中index.html記錄元件覆蓋率
複製程式碼
- 檢視
測試單獨service - 框架new例項測試
程式碼如下:
@Injectable() //交給angular管理,幫忙注入依賴
export class ValueService {
value:string;
constructor() { }
getValue() { return this.value}
}
複製程式碼
測試用例如下:
//直接new service 例項
let service: ValueService;
beforeEach(() => { service = new ValueService(); });
it('#getValue should return real value', () => {
expect(service.getValue()).toBe('real value');
});
//or
//直接獲取服務例項進行測試,通過呼叫服務,校驗邏輯
let service: ValueService;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] }); //等效於useClass
});
it('should use ValueService', () => {
service = TestBed.get(ValueService);
expect(service.getValue()).toBe('real value');
});
複製程式碼
測試單獨的元件 -- 程式碼new 元件例項
程式碼如下:
@Component({
selector: 'lightswitch-comp',
template: `
<button (click)="clicked()">Click me!</button>
<span>{{message}}</span>`
})
export class LightswitchComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
複製程式碼
測試程式碼如下:
//直接new
it('#clicked() should set #message to "is on"', () => {
const comp = new LightswitchComponent();
expect(comp.message).toMatch(/is off/i, 'off at first');
comp.clicked();
expect(comp.message).toMatch(/is on/i, 'on after clicked');
});
//or 獲取元件例項,交給框架建立new
let comp:LightswitchComponent;
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
LightswitchComponent,
]
});
// inject both the component and the dependent service.
comp = TestBed.get(LightswitchComponent);
});
it('#clicked() should set #message to "is on"', () => {
expect(comp.message).toMatch(/is off/i, 'off at first');
comp.clicked();
expect(comp.message).toMatch(/is on/i, 'on after clicked');
});
複製程式碼
元件帶有input、output
export class DashboardHeroComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
複製程式碼
測試程式碼如下:
let comp:DashboardHeroComponent;
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
DashboardHeroComponent,
]
});
// inject both the component and the dependent service.
comp = TestBed.get(DashboardHeroComponent);
});
it('raises the selected event when clicked', () => {
const hero: Hero = { id: 42, name: 'Test' };
comp.hero = hero;
comp.selected.subscribe((selectedHero: Hero) => expect(selectedHero).toBe(hero));
comp.click();
});
複製程式碼
測試service帶有依賴 -- mock依賴
利用spy mock依賴
程式碼如下:
@Injectable()
export class MasterService {
constructor(private valueService: ValueService) { }
getValue() { return this.valueService.getValue(); }
}
複製程式碼
獲取真實的依賴服務,常因為服務中依賴原因,難以順利建立。此時spy下服務,是最簡單的方法。
測試如下:
let masterService: MasterService;
let valueServiceSpy: jasmine.SpyObj<ValueService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('ValueService', ['getValue']);//需要注意位置,在beforeEach
TestBed.configureTestingModule({
// Provide both the service-to-test and its (spy) dependency
providers: [
MasterService,
//注入服務,mock提供依賴服務的支援,完成MasterService例項建立
{ provide: ValueService, useValue: spy }
]
});
// Inject both the service-to-test and its (spy) dependency
masterService = TestBed.get(MasterService);
valueServiceSpy = TestBed.get(ValueService);
});
複製程式碼
it('#getValue should return stubbed value from a spy', () => {
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value'); //利用mock依賴返回的值,進行期望判斷業務邏輯
});
複製程式碼
測試元件帶有依賴
WelcomeComponent 依賴於 UserService
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.';
}
}
複製程式碼
測試程式碼
class MockUserService {
isLoggedIn = true;
user = { name: 'Test User'};
};
複製程式碼
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
WelcomeComponent,
{ provide: UserService, useClass: MockUserService }
// {provide: UserService, useVale: userServiceSpy}
]
});
// inject both the component and the dependent service.
comp = TestBed.get(WelcomeComponent);
userService = TestBed.get(UserService);
//或者 userService = fixture.debugElement.injector.get(UserService);
//更容易記住,也不太冗長。但是,只有當Angular在測試的根注入器中將帶有服務例項的元件注入元件時,它才起作用。
});
複製程式碼
it('should not have welcome message after construction', () => {
expect(comp.welcome).toBeUndefined();
});
it('should welcome logged in user after Angular calls ngOnInit', () => {
comp.ngOnInit();
expect(comp.welcome).toContain(userService.user.name);
});
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測試
元件建立測試
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { BannerComponent } from './banner.component';
describe('BannerComponent', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeDefined();
});
});
複製程式碼
頁面元素固定
it('should contain "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
expect(bannerElement.textContent).toContain('banner works!');
});
it('should have <p> with "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
const p = bannerElement.querySelector('p');
expect(p.textContent).toEqual('banner works!');
});
it('should find the <p> with fixture.debugElement.nativeElement)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
const p = bannerEl.querySelector('p');
expect(p.textContent).toEqual('banner works!');
});
//如果querySelector不能使用,
import { By } from '@angular/platform-browser';
it('should find the <p> with fixture.debugElement.query(By.css)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const paragraphDe = bannerDe.query(By.css('p'));
const p: HTMLElement = paragraphDe.nativeElement;
expect(p.textContent).toEqual('banner works!');
});
複製程式碼
頁面元素動態修改
//頁面元素動態修改,測試
it('should display a different test title', () => {
component.title = 'Test Title';
fixture.detectChanges(); //顯示的進行修改檢測
expect(h1.textContent).toContain('Test Title');
});
//or 除去顯示宣告detectChanges,使用自動檢測
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
});
複製程式碼
Render2修改樣式測試
import {Type ,Render2 } from 'angular/core';
let renderer2: Renderer2;
...
beforeEach(async( () => {
TestBed.configureTestingModule({
...
providers: [Renderer2]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
renderer2 = fixture.componentRef.injector.get<Renderer2>(Renderer2 as Type<Renderer2>);
// and spy on it
spyOn(renderer2, 'addClass').and.callThrough();
// or replace
// spyOn(renderer2, 'addClass').and.callFake(..);
// etc
});
it('should call renderer', () => {
expect(renderer2.addClass).toHaveBeenCalledWith(jasmine.any(Object), 'css-class');
});
複製程式碼
Observable測試
code
getQuote() {
this.errorMessage = '';
this.quote = this.twainService.getQuote().pipe(
startWith('...'),
catchError( (err: any) => {
// Wait a turn because errorMessage already set once this turn
() => this.errorMessage = err.message || err.toString()
return of('...'); // reset message to placeholder
})
);
複製程式碼
正常返回
beforeEach(() => {
testQuote = 'Test Quote';
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue( of(testQuote) ); //關鍵在此
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [
{ provide: TwainService, useValue: twainService }
]
});
fixture = TestBed.createComponent(TwainComponent);
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
});
it('should show quote after component initialized', () => {
fixture.detectChanges(); // onInit()
// sync spy result shows testQuote immediately after init
expect(quoteEl.textContent).toBe(testQuote);
expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
});
複製程式碼
返回異常
beforeEach(() => {
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue( throwError('ops') ); //關鍵在此
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [
{ provide: TwainService, useValue: twainService }
]
});
fixture = TestBed.createComponent(TwainComponent);
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
});
it('should show quote after component initialized', () => {
fixture.detectChanges(); // onInit()
// sync spy result shows testQuote immediately after init
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
});
複製程式碼
返回異常,但非同步處理
//code
getQuote() {
this.errorMessage = '';
this.quote = this.twainService.getQuote().pipe(
startWith('...'),
catchError( (err: any) => {
// Wait a turn because errorMessage already set once this turn
setTimeout(() => this.errorMessage = err.message || err.toString());
return of('...'); // reset message to placeholder
})
);
beforeEach(() => {
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue( throwError('ops') ); //關鍵在此
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [
{ provide: TwainService, useValue: twainService }
]
});
fixture = TestBed.createComponent(TwainComponent);
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
});
it('should display error when TwainService fails', fakeAsync(() => { //fakeAsync不適用與ajax
// tell spy to return an error observable
getQuoteSpy.and.returnValue(
throwError('TwainService test failure'));
fixture.detectChanges(); // onInit()
// sync spy errors immediately after init
tick(); // flush the component's setTimeout()
fixture.detectChanges(); // update errorMessage within setTimeout()
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
複製程式碼
非同步程式碼測試
使用fakeAsync
it('should get Date diff correctly in fakeAsync', fakeAsync(() => {
const start = Date.now();
tick(100);
const end = Date.now();
expect(end - start).toBe(100);
}));
複製程式碼
fakeAsync支援以下非同步任務:
- setTimeout
- setInterval
- requestAnimationFrame
- webkitRequestAnimationFrame
- mozRequestAnimationFrame
- rxjs - delay、interval等
ajax請求測試
it('should show quote after getQuote (async)', async(() => {
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
});
}));
複製程式碼
jasmine done
it('should show quote after getQuote (spy done)', (done: DoneFn) => {
fixture.detectChanges();
// the spy's most recent call returns the observable with the test quote
getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
done();
});
});
複製程式碼
元件巢狀測試
服務依賴錯誤
TypeError: ctor is not a constructor
複製程式碼
問題原因:provide中錯誤的配置
//錯誤的
providers: [{provide: OrderService, useClass: new OrderServiceMock()}]
複製程式碼
//正確的
providers: [{provide: OrderService, useValue: new OrderServiceMock()}]
複製程式碼
路由測試
總結:
- 測試用例的編寫,應該儘可能的簡化測試物件邏輯,分而測之。
- 編寫程式碼時候,需要留意,預留測試
參考文獻
window 變數
Angular
d3 測試
本文作者:前端漫漫
聯絡郵箱:simple2012hcz@126.com
版權宣告: 本文章除特別宣告外,均採用 CC BY-NC-SA 4.0 許可協議。轉載請註明出處!