zoukankan      html  css  js  c++  java
  • Angular 单元测试方法总结

    • 每一个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 提供的测试module
       beforeEach(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,
        });
      };
    每天一点点
  • 相关阅读:
    jwt
    初入爬虫(java)
    SQL SERVER 查询第20行到30之间的数据
    JS 禁用按钮10秒方法
    My97DatePicker(js日期插件) v4.8
    IOS系统兼容input 监听事件
    SQLServer将一个表的数据导入到另一个表
    JS 截取地址栏指定字符后的内容
    C# EPPlus 导出Excel
    将解压后的文件复制到指定文件夹
  • 原文地址:https://www.cnblogs.com/juliazhang/p/14012910.html
Copyright © 2011-2022 走看看