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,
      });
    };
每天一点点
原文地址:https://www.cnblogs.com/juliazhang/p/14012910.html