美文网首页
Andular测试相关

Andular测试相关

作者: 桃子是水果 | 来源:发表于2019-12-19 15:09 被阅读0次

    Andular测试相关

    https://blog.51cto.com/7308310/2325925?source=dra

    常用断言以及方法

    • Jasmine 提供非常丰富的API,一些常用的Matchers:
    toBe() 等同 ===
    toNotBe() 等同 !==
    toBeDefined() 等同 !== undefined
    toBeUndefined() 等同 === undefined
    toBeNull() 等同 === null
    toBeTruthy() 等同 !!obj
    toBeFalsy() 等同 !obj
    toBeLessThan() 等同 <
    toBeGreaterThan() 等同 >
    toEqual() 相当于 ==
    toNotEqual() 相当于 !=
    toContain() 相当于 indexOf
    toBeCloseTo() 数值比较时定义精度,先四舍五入后再比较。
    toHaveBeenCalled() 检查function是否被调用过
    toHaveBeenCalledWith() 检查传入参数是否被作为参数调用过
    toMatch() 等同 new RegExp().test()
    toNotMatch() 等同 !new RegExp().test()
    toThrow() 检查function是否会抛出一个异常
    

    而这些API之前用 not 来表示负值的判断。

    expect(true).not.toBe(false);


    angular cli使用karma进行单元测试.

    • 使用 ng test进行测试。

      端对端测试的命令是 ng e2e

      常用参数:
      --browsers 指定使用的浏览器
      --code-coverage 输出覆盖率报告
      --code-coverage-exclude 排除文件或路径
      --karma-config 指定Karma配置文件
      --prod 启用production环境
      --progress 默认为true,将编译进度输出到控制台
      --watch 默认为true,代码修改后会重新运行测试
      
    • 默认的测试文件扩展名为.spec.ts。

    • import { TestBed, async } from '@angular/core/testing';
      import { RouterTestingModule } from '@angular/router/testing';
      import { AppComponent } from './app.component';
      import { StudyBarComponent } from './study-bar/study-bar.component';
      
      describe('AppComponent', () => {
        beforeEach(async(() => {
          TestBed.configureTestingModule({
            imports: [
              RouterTestingModule   // 有路由的测试项需要用到
            ],
            declarations: [
              AppComponent,
              StudyBarComponent
            ],
          }).compileComponents();
        }));
      
        it('should create the app', () => {
          const fixture = TestBed.createComponent(AppComponent);
          const app = fixture.debugElement.componentInstance;
          expect(app).toBeTruthy();
        });
      
        it(`should have as title 'my-app'`, () => {
          const fixture = TestBed.createComponent(AppComponent);
          const app = fixture.debugElement.componentInstance;
          expect(app.title).toEqual('my-app');
        });
      });
      
    • 测试结构

      • describe函数中包含了beforeEach和it两类函数。describe相当于Java测试中的suite,也就是测试组,其中可以包含多个测试用例it。
      • 一般一个测试文件含有一个describe,当然也可以有多个。
      • beforeEach相当于Java测试中的@Before方法,每个测试用例执行前调用一次。同样,还有afterEach、beforeAll、afterAll函数,afterEach在每个测试用例执行后调用一次,beforeAll、afterAll相当于Java测试中的@BeforeClass、@AfterClass方法,每个describe执行前后调用一次。
      • **describe和it的第一个参数是测试说明。一个it中可以包含一个或多个expect来执行测试验证。 **
    • TestBed

      • TestBed.configureTestingModule()方法动态构建TestingModule来模拟Angular @NgModule,支持@NgModule的大多数属性。
      • 测试中需导入测试的组件及依赖。例如在AppComponent页面中使用了router-outlet,因此我们导入了RouterTestingModule来模拟RouterModule。Test Module预配置了一些元素,比如BrowserModule,不需导入。
      • TestBed.createComponent()方法创建组件实例,返回ComponentFixture。ComponentFixture是一个测试工具(test harness),用于与创建的组件和相应元素进行交互。
    • nativeElement和DebugElement

    • 示例中使用了fixture.debugElement.nativeElement,也可以写成fixture.nativeElement。实际上,fixture.nativeElement是fixture.debugElement.nativeElement的一种简化写法。nativeElement依赖于运行时环境,Angular依赖DebugElement抽象来支持跨平台。Angular创建DebugElement tree来包装native element,nativeElement返回平台相关的元素对象。我们的测试样例仅运行在浏览器中,因此nativeElement总为HTMLElement,可以使用querySelector()、querySelectorAll()方法来查询元素。

      element.querySelector('p');
      element.querySelector('input');
      element.querySelector('.welcome');
      element.querySelectorAll('span');
      
    • detectChanges
    • createComponent() 函数不会绑定数据,必须调用fixture.detectChanges()来执行数据绑定,才能在组件元素中取得内容:

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

      当数据模型值改变后,也需调用fixture.detectChanges()方法:

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

      可以配置自动检测,增加ComponentFixtureAutoDetect provider:

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

      启用自动检测后仅需在数值改变后调用detectChanges():

      it('should display original title', () => {
        // Hooray! No `fixture.detectChanges()` needed
        expect(h1.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(h1.textContent).toContain(oldTitle);
      });
      
      it('should display updated title after detectChanges', () => {
        comp.title = 'Test Title';
        fixture.detectChanges(); // detect changes explicitly
        expect(h1.textContent).toContain(comp.title);
      });
      
    • 依赖注入

      • 对简单对象进行测试可以用new创建实例:

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

        不过大多数Service、Component等有多个依赖项,使用new很不方便。若用DI来创建测试对象,当依赖其他服务时,DI会找到或创建依赖的服务。要测试某个对象,在configureTestingModule中配置测试对象本身及依赖项,然后调用TestBed.get()注入测试对象:

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

        单元测试的原则之一:仅对要测试对象本身进行测试,而不对其依赖项进行测试,依赖项通过mock方式注入,而不使用实际的对象,否则测试不可控。

        Mock优先使用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

        同测试含其它依赖的对象一样,可以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测试

        • 仅测试组件类

      测试组件类就像测试服务那样简单:
      组件类

        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类

        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 }
            ]
          });
          // 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测试

      只涉及类的测试可以判断组件类的行为是否正常,但不能确定组件是否能正常渲染和交互。
      进行组件DOM测试,需要使用TestBed.createComponent()等方法,第一个测试即为组件DOM测试。

        TestBed.configureTestingModule({
          declarations: [ BannerComponent ]
        });
        const fixture = TestBed.createComponent(BannerComponent);
        const component = fixture.componentInstance;
        expect(component).toBeDefined();
        ```
      
      **dispatchEvent**
        为模拟用户输入,比如为input元素输入值,要找到input元素并设置它的 value 属性。Angular不知道你设置了input元素的value属性,需要调用 dispatchEvent() 触发输入框的 input 事件,再调用 detectChanges():
      
      ```typescript
        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');
        });
        ```
      
      - 嵌套组件
      
      组件中常常使用其他组件:
      
      ```html
      <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>
      

      对于无害的内嵌组件可以直接将其添加到declarations中,这是最简单的方式:

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

      也可为无关紧要的组件创建一些测试桩:

      @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编译器忽略那些不认识的元素和属性:

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

      NO_ERRORS_SCHEMA方法比较简单,但不要过度使用。NO_ERRORS_SCHEMA 会阻止编译器因疏忽或拼写错误而缺失的组件和属性,如人工找出这些 bug会很费时。
      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;
        }
      }
      
      • 属性指令测试

        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,如只针对类测试不能证明指令的有效性。若通过组件来测试,单一的用例一般无法探索指令的全部能力。因此,更好的方法是创建一个能展示该指令所有用法的人造测试组件:

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

        测试程序:

        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 h2 without the HighlightDirective
          bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
        });
        
        // color tests
        it('should have three highlighted elements', () => {
          expect(des.length).toBe(3);
        });
        
        it('should color 1st <h2> background "yellow"', () => {
          const bgColor = des[0].nativeElement.style.backgroundColor;
          expect(bgColor).toBe('yellow');
        });
        
        it('should color 2nd <h2> 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 <h2> should not have a customProperty', () => {
          expect(bareH2.properties['customProperty']).toBeUndefined();
        });
        
      • Pipe测试

        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
        在前面的测试中我们使用了测试桩RouterOutletStubComponent,与Router有关的测试还可以使用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();
          });
        
            ...
        });
        
    • 在Mock的时候,优先推荐使用Spy

      使用方式简介:

      spyOn(obj, 'functionName').and.returnValue(returnValue);
      
      spyOn(storeStub, 'select').and.callFake(func => {
              func();
              return returnValue;
      });
      

      在需要模拟返回值的地方使用spy监视对象以及其调用的函数,按上述两种方式可以自定义返回值。

      更详细的用法参照

      https://jasmine.github.io/2.5/introduction#section-Spies

    相关文章

      网友评论

          本文标题:Andular测试相关

          本文链接:https://www.haomeiwen.com/subject/rwwmnctx.html