无法使用服务测试组件
Unable to test a component with a service
读完这篇 guide 我决定测试我的简单登录页面,它只包含 2 个输入框和一个提交按钮。然后该组件使用 LoginService
将这些数据传递到后端。
( 另请注意,我是单元测试的新手,所以我不确定这是否是测试此类组件的好方法。 )
对于初学者,我只想检查 #username
输入元素的初始值是否为空。但由于以下报告的问题,我什至无法使规范生效:
Chrome 55.0.2883 (Windows 7 0.0.0) LoginComponent Username field should be empty FAILED
Failed: Unexpected value 'Http' imported by the module 'DynamicTestModule'
Error: Unexpected value 'Http' imported by the module 'DynamicTestModule'
TypeError: Cannot read property 'detectChanges' of undefined
Chrome 55.0.2883 (Windows 7 0.0.0): Executed 4 of 4 (1 FAILED) (0 secs / 0.348 secs)
当我尝试删除 Http 模块时,出现此错误:
Chrome 55.0.2883 (Windows 7 0.0.0) LoginComponent Username field should be empty FAILED
Error: DI Error
Error: Uncaught (in promise): Error: No provider for Http!
TypeError: Cannot read property 'detectChanges' of undefined
Chrome 55.0.2883 (Windows 7 0.0.0): Executed 4 of 4 (1 FAILED) (0 secs / 0.456 secs)
login.component.html
<div class="login jumbotron center-block">
<h1>Login</h1>
<form (ngSubmit)="onSubmit($event)" #loginForm="ngForm">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" [(ngModel)]="model.username" name="username"
placeholder="Username" #username="ngModel" required>
<div [hidden]="username.valid || username.pristine" class="alert alert-danger"> Username is required </div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" [(ngModel)]="model.password" name="password" placeholder="Password" #password="ngModel" required>
<div [hidden]="password.valid || password.pristine" class="alert alert-danger"> Password is required </div>
</div>
<button type="submit" class="btn btn-default" [disabled]="!loginForm.form.valid" >Submit</button>
<a [routerLink]="['/signup']">Click here to Signup</a>
</form>
</div>
login.component.ts
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { LoginService } from '../services/login.service';
import { User } from '../extensions/user.class';
@Component({
moduleId: module.id,
selector: 'login',
templateUrl: '../templates/login.component.html',
styleUrls: [ '../styles/login.component.css' ],
providers: [ LoginService ]
})
export class LoginComponent {
private submitted = false;
private model = new User();
constructor(
private router: Router,
private loginService: LoginService
) {}
public onSubmit(event: any): void {
event.preventDefault();
if ( ! this.submitted ) {
this.submitted = true;
if ( this.model.username && this.model.password ) {
this.loginService.login(this.model).then( (token) => {
localStorage.setItem('id_token', token.id);
this.router.navigate(['home']);
}).catch( (error) => this.onLoginFailed(error) );
} else {
console.warn('No username or password provided');
}
}
}
private onLoginFailed( error: any ): void {
//// errors are already handled in login-service ////
console.error(error);
this.submitted = false; /// reset form submit funcitonality ///
}
public signup(event: any): void {
event.preventDefault();
this.router.navigate(['signup']);
}
}
login.component.spec.ts
import { async } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { Location } from '@angular/common';
import { LoginComponent } from './login.component';
import { LoginService } from '../services/login.service';
import { Http } from '@angular/http';
import { User } from '../extensions/user.class';
@Component({
template: ''
})
class DummyComponent{}
class LoginServiceStub {
login( user: User ){
return true;
}
}
describe('LoginComponent', () => {
let comp: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let de: DebugElement;
let el: HTMLElement;
let location: Location;
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent, DummyComponent ], // declare the test component
providers: [
{ provide: LoginService, useClass: LoginServiceStub }
],
imports: [
FormsModule ,
RouterTestingModule.withRoutes([
{ path: 'singup', component: DummyComponent }
])
]
}).compileComponents() // compile template and css
.then( () => {
fixture = TestBed.createComponent(LoginComponent);
comp = fixture.componentInstance; // LoginComponent test instance
de = fixture.debugElement.query(By.css('input[name="username"]'));
el = de.nativeElement;
});
}));
it('Username field should be empty', () => {
fixture.detectChanges();
expect(el.textContent).toContain('');
});
});
亚历克斯,
您是否尝试过将 Http 模块导入您的测试组件并将其添加到 "providers" 数组?我认为在这种情况下您必须指定所有依赖项。我假设您的 LoginService 需要 {Http} 作为一项规定,但您的测试组件没有注册 {Http},所以它找不到要使用的实例。
编辑:
TestBed.configureTestingModule({
declarations: [ LoginComponent, DummyComponent ], // declare the test component
providers: [
{ provide: LoginService, useClass: LoginServiceStub },
Http,
],
imports: [
FormsModule ,
RouterTestingModule.withRoutes([
{ path: 'singup', component: DummyComponent }
])
]
双重编辑!:
此外,您可能想要模拟 Http 模块,因为您实际上并不想在单元测试期间发送请求。来自 @angular/http/testing 的 "MockBackend" 就足够了——在这种情况下,您可能希望使用与登录服务一起使用的 "provide" 语法来提供一个使用 MockBackend 的 Http 模块来生成响应。
问题是 LoginService
是在 组件 级别声明的
@Component({
providers: [ LoginService ]
})
这将取代在 模块 级别声明的任何相同服务,这是您在测试中声明模拟的地方。您可以做几件事:
不要在组件级别声明服务。如果没有充分的理由将其限定在组件范围内,则只需在 @NgModule.providers
处声明它并将其设为单例。
覆盖测试中的@Component.providers
。
TestBed.configureTestingModule({})
TestBed.overrideComponent(LoginComponent, {
set: {
providers: [
{ provide: LoginService, useClass: LoginServiceStub }
]
}
});
读完这篇 guide 我决定测试我的简单登录页面,它只包含 2 个输入框和一个提交按钮。然后该组件使用 LoginService
将这些数据传递到后端。
( 另请注意,我是单元测试的新手,所以我不确定这是否是测试此类组件的好方法。 )
对于初学者,我只想检查 #username
输入元素的初始值是否为空。但由于以下报告的问题,我什至无法使规范生效:
Chrome 55.0.2883 (Windows 7 0.0.0) LoginComponent Username field should be empty FAILED
Failed: Unexpected value 'Http' imported by the module 'DynamicTestModule'
Error: Unexpected value 'Http' imported by the module 'DynamicTestModule'
TypeError: Cannot read property 'detectChanges' of undefined
Chrome 55.0.2883 (Windows 7 0.0.0): Executed 4 of 4 (1 FAILED) (0 secs / 0.348 secs)
当我尝试删除 Http 模块时,出现此错误:
Chrome 55.0.2883 (Windows 7 0.0.0) LoginComponent Username field should be empty FAILED
Error: DI Error
Error: Uncaught (in promise): Error: No provider for Http!
TypeError: Cannot read property 'detectChanges' of undefined
Chrome 55.0.2883 (Windows 7 0.0.0): Executed 4 of 4 (1 FAILED) (0 secs / 0.456 secs)
login.component.html
<div class="login jumbotron center-block">
<h1>Login</h1>
<form (ngSubmit)="onSubmit($event)" #loginForm="ngForm">
<div class="form-group">
<label for="username">Username</label>
<input type="text" class="form-control" [(ngModel)]="model.username" name="username"
placeholder="Username" #username="ngModel" required>
<div [hidden]="username.valid || username.pristine" class="alert alert-danger"> Username is required </div>
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" [(ngModel)]="model.password" name="password" placeholder="Password" #password="ngModel" required>
<div [hidden]="password.valid || password.pristine" class="alert alert-danger"> Password is required </div>
</div>
<button type="submit" class="btn btn-default" [disabled]="!loginForm.form.valid" >Submit</button>
<a [routerLink]="['/signup']">Click here to Signup</a>
</form>
</div>
login.component.ts
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { LoginService } from '../services/login.service';
import { User } from '../extensions/user.class';
@Component({
moduleId: module.id,
selector: 'login',
templateUrl: '../templates/login.component.html',
styleUrls: [ '../styles/login.component.css' ],
providers: [ LoginService ]
})
export class LoginComponent {
private submitted = false;
private model = new User();
constructor(
private router: Router,
private loginService: LoginService
) {}
public onSubmit(event: any): void {
event.preventDefault();
if ( ! this.submitted ) {
this.submitted = true;
if ( this.model.username && this.model.password ) {
this.loginService.login(this.model).then( (token) => {
localStorage.setItem('id_token', token.id);
this.router.navigate(['home']);
}).catch( (error) => this.onLoginFailed(error) );
} else {
console.warn('No username or password provided');
}
}
}
private onLoginFailed( error: any ): void {
//// errors are already handled in login-service ////
console.error(error);
this.submitted = false; /// reset form submit funcitonality ///
}
public signup(event: any): void {
event.preventDefault();
this.router.navigate(['signup']);
}
}
login.component.spec.ts
import { async } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { Location } from '@angular/common';
import { LoginComponent } from './login.component';
import { LoginService } from '../services/login.service';
import { Http } from '@angular/http';
import { User } from '../extensions/user.class';
@Component({
template: ''
})
class DummyComponent{}
class LoginServiceStub {
login( user: User ){
return true;
}
}
describe('LoginComponent', () => {
let comp: LoginComponent;
let fixture: ComponentFixture<LoginComponent>;
let de: DebugElement;
let el: HTMLElement;
let location: Location;
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ LoginComponent, DummyComponent ], // declare the test component
providers: [
{ provide: LoginService, useClass: LoginServiceStub }
],
imports: [
FormsModule ,
RouterTestingModule.withRoutes([
{ path: 'singup', component: DummyComponent }
])
]
}).compileComponents() // compile template and css
.then( () => {
fixture = TestBed.createComponent(LoginComponent);
comp = fixture.componentInstance; // LoginComponent test instance
de = fixture.debugElement.query(By.css('input[name="username"]'));
el = de.nativeElement;
});
}));
it('Username field should be empty', () => {
fixture.detectChanges();
expect(el.textContent).toContain('');
});
});
亚历克斯,
您是否尝试过将 Http 模块导入您的测试组件并将其添加到 "providers" 数组?我认为在这种情况下您必须指定所有依赖项。我假设您的 LoginService 需要 {Http} 作为一项规定,但您的测试组件没有注册 {Http},所以它找不到要使用的实例。
编辑:
TestBed.configureTestingModule({
declarations: [ LoginComponent, DummyComponent ], // declare the test component
providers: [
{ provide: LoginService, useClass: LoginServiceStub },
Http,
],
imports: [
FormsModule ,
RouterTestingModule.withRoutes([
{ path: 'singup', component: DummyComponent }
])
]
双重编辑!:
此外,您可能想要模拟 Http 模块,因为您实际上并不想在单元测试期间发送请求。来自 @angular/http/testing 的 "MockBackend" 就足够了——在这种情况下,您可能希望使用与登录服务一起使用的 "provide" 语法来提供一个使用 MockBackend 的 Http 模块来生成响应。
问题是 LoginService
是在 组件 级别声明的
@Component({
providers: [ LoginService ]
})
这将取代在 模块 级别声明的任何相同服务,这是您在测试中声明模拟的地方。您可以做几件事:
不要在组件级别声明服务。如果没有充分的理由将其限定在组件范围内,则只需在
@NgModule.providers
处声明它并将其设为单例。覆盖测试中的
@Component.providers
。TestBed.configureTestingModule({}) TestBed.overrideComponent(LoginComponent, { set: { providers: [ { provide: LoginService, useClass: LoginServiceStub } ] } });