- 每一个spect (it)的命名 [component name/ service name]-[function name]-[branch]
- 运行每一个spect 的生命周期顺序
Constructor => ngOnInit => 再走case 里面的内容 after each -> ngOnDestory
每走完一个it,在执行下一个it 之前会先走ngOnDestory; 然后再执行下一个spect Constructor => ngOnInit => 再走case 里面的内容
当走完当前组件的所有spectU,如果还有下一个组件的spect,那么切换到下一个组件时, 也会再走前一个组件spect ngOnDestory.
- 对于共享的变量或者数据,比如localStorage, sessionStorage, 最好是mock 个假的;否则,UT里面改了,会影响代码的正常运行。
// LocalStorage export class LocalStorageStub { static store = {}; static getItem(key: string): string { return key in LocalStorageStub.store ? LocalStorageStub.store[key] : null; } static setItem(key: string, value: string) { LocalStorageStub.store[key] = `${value}`; } static removeItem(key: string) { delete LocalStorageStub.store[key]; } static clear() { LocalStorageStub.store = {}; } static spyOnStore() { spyOn(localStorage, "getItem").and.callFake(LocalStorageStub.getItem); spyOn(localStorage, "setItem").and.callFake(LocalStorageStub.setItem); spyOn(localStorage, "removeItem").and.callFake(LocalStorageStub.removeItem); spyOn(localStorage, "clear").and.callFake(LocalStorageStub.clear); } } // 使用方法 beforeEach(() => { SessionStorageStub.spyOnStore(); LocalStorageStub.spyOnStore(); TestBed.configureTestingModule({ imports: [ LoginModule, RouterTestingModule ], providers: [ HttpClient, HttpHandler, { provide: Router, useValue: routerSpy } ] }); service = TestBed.inject(AuthorizationService); loginService = TestBed.inject(LoginService); });
// SessionStorage export class SessionStorageStub { static store = {}; static getItem(key: string): string { return key in SessionStorageStub.store ? SessionStorageStub.store[key] : null; } static setItem(key: string, value: string) { SessionStorageStub.store[key] = `${value}`; } static removeItem(key: string) { delete SessionStorageStub.store[key]; } static clear() { SessionStorageStub.store = {}; } static spyOnStore() { spyOn(sessionStorage, "getItem").and.callFake(SessionStorageStub.getItem); spyOn(sessionStorage, "setItem").and.callFake(SessionStorageStub.setItem); spyOn(sessionStorage, "removeItem").and.callFake(SessionStorageStub.removeItem); spyOn(sessionStorage, "clear").and.callFake(SessionStorageStub.clear); } } // 使用方法 beforeEach(() => { SessionStorageStub.spyOnStore(); LocalStorageStub.spyOnStore(); TestBed.configureTestingModule({ imports: [ LoginModule, RouterTestingModule ], providers: [ HttpClient, HttpHandler, { provide: Router, useValue: routerSpy } ] }); service = TestBed.inject(AuthorizationService); loginService = TestBed.inject(LoginService); });
*同时,最好在每一个spect 运行完成后,在afterEach()里面把这些共享的内容reset;便于后面的ut 使用
-
spyOn,callThrough, callFake 的区别
spyOn(component, "initModalServiceSubscriptions")
这样写,component.initModalServiceSubscriptions 会被调用一次,但是initModalServiceSubscriptions 里面的所有内容都不会走。spyOn(component, "initModalServiceSubscriptions”).callThrough()
如果想要test case 走进真实的方法里面,我们就需要这样写spyOn(component, "initModalServiceSubscriptions”).callFake(() => {})
如果并不care spy的方法里面的内容,那么我们就直接callFake就可以了
mock 返回值是Observable - spyOn(component, "initModalServiceSubscriptions”).and.callFake(() => of())mock 返回值是Promise - spyOn(component, "initModalServiceSubscriptions”).and.callFake(() => Promise.resolve()/ Promise.reject())
mock 抛异常- spyOn(component, "initModalServiceSubscriptions”).and.throwError("test");
这种异常被谁捕获呢?,如下const result = concat(of(7), throwError(new Error('oops!'))); result.subscribe(x => console.log(x), e => console.error(e));
没有被spyOn的方法,即使在ngOnInit 里面被调用过,当spyOn之后,计算该方法的调用次数时,也是从0开始计算的
- 几种异常的测试方式
// 例子 - catchError exportEmployeeData(): Observable<any> { this.httpService.responseType = true; return this.httpService.get("employee/export").pipe(res => { this.httpService.responseType = false; return res; }, catchError(err => { this.httpService.responseType = false; throw err; })); } // 解决方案 fit("should invoke exportEmployeeData - exception", fakeAsync(() => { spyOn(httpService, "get").and.returnValue(from(new Promise((resolve, reject) => reject(new Error("test"))))); let result = null; service.exportEmployeeData().subscribe( () => null, err => { result = err; } ); tick(); expect(result).toEqual(new Error("test"), "catchError"); discardPeriodicTasks(); }));
// try - catch fileGenerate(data: BufferSource | Blob | string, fileName: string, type?: string) { try { let blob = new Blob([type === "text/csv; charset=UTF-8" ? "uFEFF" : "", data], { type: type ? type : "" }); let dwldLink = document.createElement("a"); let url = URL.createObjectURL(blob); let isSafariBrowser = navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1; if (isSafariBrowser) { //if Safari open in new window to save file with random filename. dwldLink.setAttribute("target", "_blank"); } dwldLink.setAttribute("href", url); dwldLink.setAttribute("download", fileName); dwldLink.style.visibility = "hidden"; document.body.appendChild(dwldLink); dwldLink.click(); document.body.removeChild(dwldLink); } catch (error) { console.log(error); } } // 解决方案 it("fileGenerate invoke # error", () => { const data = `First Name,Last Name,Email Address,Phone,Role,Registered Device rina,0821,rina0821@arasj.net,(201) 763-7488,Dashboard Administrator,Yes`; ConsoleStub.spyOnConsole(); spyOn(document, "createElement").and.throwError("testError"); service.fileGenerate(data, "test", "text/csv; charset=UTF-8"); expect(console.log).toHaveBeenCalledWith(new Error("testError")); });
- fakeAsync 什么时候用
fakeAsync 主要是用来解决 setTimeOut 的问题;要求case 等多久截取最终的结果;例如下面的例子: describe('this test', () => { it('looks async but is synchronous', <any>fakeAsync((): void => { let flag = false; setTimeout(() => { flag = true; }, 100); expect(flag).toBe(false); tick(50); expect(flag).toBe(false); tick(50); expect(flag).toBe(true); })); }); 一般情况,能不用fakeAsync就不用fakeAsync ; 否则可能会遇到下面的错误 Error: 1 periodic timer(s) still in the queue.
- 对于订阅的代码,测试方式 - 核心思想就是发布事件给订阅者
this.modalService.onHidden.subscribe(async (reason: string) => { if (reason) { const reasons = reason.split(","); switch (reasons[0]) { case "CHECK_UNASSIGN_DEVICE": if (reasons[1] === "YES") { const individualName = reasons[3]; const sessionId = parseInt(reasons[2], 10); await this.unassignedDevice(sessionId, individualName); } break; case "UNASSIGN_DEVICE": if (reasons[1] === "YES") { this.refreshDeviceTableData(); } break; case "ASSIGN_DEVICE": if (reasons[1] === "YES") { this.refreshDeviceTableData(); } break; case "DELETE_USER": if (reasons[1] === "YES") { const userId = parseInt(reasons[2], 10); await this.temporaryUserService.removeUser(userId).toPromise(); this.refreshDeviceTableData(); } break; default: // do nothing } } }) // 解决方案 it("handleModal with CHECK_UNASSIGN_DEVICE,YES", () => { modalService.onHidden.next("CHECK_UNASSIGN_DEVICE,YES"); spyOn(modalService.onHidden, "subscribe"); fixture.detectChanges(); expect(modalService.onHidden.subscribe).toHaveBeenCalled(); });
- Router 的测试方法
// 示例 routerCheck() { this.router.events.pipe( filter((event: RouterEvent) => event instanceof NavigationEnd) ).subscribe(() => { let isCaseSearch = false; if (history.state?.openNewCase) { isCaseSearch = true; } this.checkActiveMenu(isCaseSearch); }); } // 解决方案 let router: Router; const eventSubject = new ReplaySubject<RouterEvent>(1); const routerSpy = { navigate: jasmine.createSpy('navigate'), events: eventSubject.asObservable(), get url() { return "test" }, set url(v) { this.url = v } }; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [LeftNavigatorComponent], providers: [ AuthorizationService, EmitService, { provide: Router, useValue: routerSpy } ], imports: [ HttpClientTestingModule, RouterTestingModule ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(AppComponent); component = fixture.componentInstance; router = TestBed.inject(Router); fixture.detectChanges(); }); // routerCheck it("routerCheck run as expect", () => { eventSubject.next(new NavigationEnd(1, "regular", "redirectUrl")); spyOn(component, "checkActiveMenu").and.callFake(() => { }); spyOnProperty(history, "state").and.returnValue({ openNewCase: true }); component.routerCheck(); expect(component.checkActiveMenu).toHaveBeenCalledTimes(1); });
- 基本测试方法
// mock 一个假的service, 并使用 const PendoServiceStub = jasmine.createSpyObj("PendoService", [ "installPendoSnippet", "updatePendoSettings", "resetPendoSettings", "checkPendo", ]); beforeEach(async(() => { TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, RouterTestingModule, ModalModule.forRoot(), ], declarations: [ DeviceUserComponent ], providers: [ BsModalRef, BsModalService, EmitService, EmployeeService, AuthorizationService, {provide: PendoService, useValue: PendoServiceStub}, PaginationService, UploadFileService, TemporaryUserService, { provide: Router, useValue: routerSpy }, ], schemas: [NO_ERRORS_SCHEMA] --- 加上schema,可以忽略依赖的error }) .compileComponents(); }));
// 尽量把组件的变量声明成共享变量,以减少mock 数据的次数
// 获取service实例的方法
inventoryService = TestBed.inject(InventoryService);
// 如果组件中用到了httpClient, 和 Router, 那么需要import两个angular 提供的测试modulebeforeEach(async(() => { console.log("before each async"); TestBed.configureTestingModule({ imports: [ HttpClientTestingModule, RouterTestingModule, ModalModule.forRoot() ], declarations: [ AssignDevicesComponent ], providers: [ BsModalService, BsModalRef, AuthorizationService, InventoryService, TemporaryUserService, EmitService, PaginationService, OfficeService, {provide: ApplicationInsightsService, useValue: ApplicationInsightsServiceStub} ], schemas: [NO_ERRORS_SCHEMA], }) .compileComponents(); }));
-
karma 配置例子
// Karma configuration file, see link for more information // https://karma-runner.github.io/1.0/config/configuration-file.html module.exports = function (config) { config.set({ files: ['src/app/testing/google-mock.js'], basePath: '', frameworks: ['jasmine', '@angular-devkit/build-angular'], plugins: [ require('karma-jasmine'), require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), require('@angular-devkit/build-angular/plugins/karma') ], client: { clearContext: false, // leave Jasmine Spec Runner output visible in browser, rjasmine: { random: false } }, coverageIstanbulReporter: { dir: require('path').join(__dirname, './coverage/ECT'), reports: ['html', 'lcovonly', 'text-summary'], fixWebpackSourcePaths: true, // thresholds: { // statements: 80, // lines: 80, // branches: 80, // functions: 80 // } }, reporters: ['progress', 'kjhtml'], port: 9876, colors: true, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false, restartOnFileChange: true, browserDisconnectTimeout: 300000, }); };