Andular测试相关
常用断言以及方法
- 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会很费时。
RouterLinkDirectiveStubimport { 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监视对象以及其调用的函数,按上述两种方式可以自定义返回值。
更详细的用法参照
网友评论