Jasmine - 使用 HttpClientTestingModule 进行测试和 API 调用
Jasmine - Test and API call with HttpClientTestingModuel
我正尝试在我的 Ionic 应用程序中开始使用单元测试,使用 Angular 6、Jasmine 和 Karma。
我有一个 loginPage,它调用一个 AuthService,后者又调用一个 HttpService,下面我将向您展示代码。
我遇到的问题是,如果我调用 doLogin 函数(在 loginPage 中)或者如果我调用登录函数(在 authService 中),我永远无法得到响应。我尝试使用订阅并执行 toPromise() 将 Observable 转换为 Promise,但我也没有成功。
你能帮帮我吗?
非常感谢。
LOGINPAGE.TS
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Credentials } from '../models/credentials';
import { AuthService } from '../services/auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
})
export class LoginPage implements OnInit {
ionicForm: FormGroup;
isSubmitted: boolean = false;
credential: Credentials;
constructor(public formBuilder: FormBuilder, private _authService: AuthService) { }
ngOnInit() {
this.ionicForm = this.formBuilder.group({
email: ['eve.holt@reqres.in', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
}
doLogin1() {
this.isSubmitted = true;
if (!this.ionicForm.valid) {
return false;
} else {
return true
}
}
async doLogin(): Promise<Credentials | false> {
this.isSubmitted = true;
if (!this.ionicForm.valid) {
return false;
} else {
this.credential = await this._authService.login(this.ionicForm.value);
return this.credential;
}
}
get errorControl() {
return this.ionicForm.controls;
}
}
AUTHSERVICE.TS
import { Injectable } from '@angular/core';
import { HttpService } from './http.service';
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor(private _httpService: HttpService) { }
async login(body){
return await this._httpService.post('https://reqres.in/api/login',body).toPromise();
}
}
HTTPSERVICE.TS
import { Injectable } from '@angular/core';
import { Observable, Subject, throwError } from 'rxjs';
import { HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class HttpService {
errorLogin = new Subject<boolean>();
constructor(private httpClient: HttpClient) {
}
public get(path: string, options?: any): Observable<any> {
return this.httpClient.get(path,options).pipe(catchError(this.formatErrors));
}
public put(path: string, body: object = {},headers?:any): Observable<any> {
let options = {
headers: headers
}
return this.httpClient.put(path, JSON.stringify(body),options).pipe(catchError(this.formatErrors));
}
public post(path: string, body: object = {},options?: any): Observable<any> {
return this.httpClient.post(path,body,options).pipe(catchError(this.formatErrors));
}
public delete(path: string, headers?:any): Observable<any> {
return this.httpClient.delete(path, {headers,responseType: 'text' }).pipe(catchError(this.formatErrors));
}
public appendHeaders(header:HttpHeaders){
if (header != undefined){
return header.set('Content-Type','application/json');
}
return new HttpHeaders().set('Content-Type','application/json');
}
public formatErrors(error: HttpErrorResponse): Observable<any> {
if (error.status === 401 && error.error.message == 'Authentication failed'){
try {
this.errorLogin.next(true);
} catch(error){
}
}
return throwError(error);
}
showCredError(){
this.errorLogin.next(true);
}
hideCredError(){
this.errorLogin.next(false);
}
/*
Metodo de prueba, eliminar al pasar a producción
*/
public testRest(){
return this.httpClient.get('https://jsonplaceholder.typicode.com/todos/1');
}
}
LOGINPAGE.SPEC.TS
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { IonicModule } from '@ionic/angular';
import { HttpClientTestingModule, } from '@angular/common/http/testing';
import { LoginPage } from './login.page';
import { AuthService } from '../services/auth.service';
import { HttpService } from '../services/http.service';
import { of } from 'rxjs';
import { Credentials } from '../models/credentials';
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
let authService: AuthService;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot(),
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule],
providers: [AuthService, HttpService]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
fixture.detectChanges();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
}));
it('A) should create', () => {
expect(component).toBeTruthy();
});
it('B) send form without data', () =>{
component.ionicForm = component.formBuilder.group({
email: ['', [Validators.required, Validators.minLength(8)]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorUsernameRequired'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.errorPasswordRequired'))).not.toBeNull();
});
it('C) send form without password', () =>{
component.ionicForm = component.formBuilder.group({
email: ['UsuarioPrueba', [Validators.required, Validators.minLength(8)]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorUsernameRequired'))).toBeNull();
expect(fixture.debugElement.query(By.css('.errorPasswordRequired'))).not.toBeNull();
});
it('D) send form without username', () =>{
component.ionicForm = component.formBuilder.group({
email: ['', [Validators.required, Validators.minLength(8)]],
password: ['ContraseñaPrueba', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorUsernameRequired'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.errorPasswordRequired'))).toBeNull();
});
it('E) send form with minor lenght in both inputs', () =>{
component.ionicForm = component.formBuilder.group({
email: ['ABCDEF', [Validators.required, Validators.minLength(8)]],
password: ['ABCDEF', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorPasswordMinLength'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.errorUsernameMinLength'))).not.toBeNull();
});
it('F) send form successfully', (()=>{
component.ionicForm = component.formBuilder.group({
email: ['eve.holt@reqres.in', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
console.log(data);
// THIS CONSOLE LOG NOT APPEAR IN THE CHROME CONSOLE
})
}));
});
我会放弃 HttpClientTestingModule
并直接模拟 AuthService
。
在行后面加上 // !!:
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
// !! change this line to mockAuthService
let mockAuthService: jasmine.SpyObj<AuthService>;
beforeEach(waitForAsync(() => {
// !! assign mockAuthService to mock here
// !! first string argument is optional, 2nd array of strings are public methods you would like to mock
mockAuthService = jasmine.createSpyObj<AuthService>('AuthService', ['login']);
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot(),
FormsModule,
ReactiveFormsModule],
// !! provide mock for the real AuthService
providers: [{ provide: AuthService, useValue: mockAuthService }]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
fixture.detectChanges();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
}));
// !! you have an extra bracket after the ', and I think this could be a problem
it('F) send form successfully', ()=>{
component.ionicForm = component.formBuilder.group({
email: ['eve.holt@reqres.in', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
mockAuthService.login.and.returnValue(Promise.resolve({/* mock login respond here */}));
// !! below should hopefully work now
component.doLogin().then((data)=>{
console.log(data);
// THIS CONSOLE LOG NOT APPEAR IN THE CHROME CONSOLE
})
})
以上是单元测试。单元测试的心态应该是假设其他一切正常,我的 component/class/etc 是否如此。做它应该做的事?阅读 this 以了解模拟外部依赖项。
您的目标是集成测试(提供实际的 AuthService),我认为我们也可以做到。我们必须查看队列中的 http 请求并响应它。
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
let authService: AuthService;
// !! get a handle on httpTestingController
let httpTestingController: HttpTestingController;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot(),
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule],
providers: [AuthService, HttpService]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
// !! get handle, if you're using Angular 9+, get should be inject
httpTestingController = TestBed.get(HttpTestingController);
fixture.detectChanges();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
}));
// !! remove the ( after ',
it('F) send form successfully', () => {
component.ionicForm = component.formBuilder.group({
email: ['eve.holt@reqres.in', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
console.log(data);
// THIS CONSOLE LOG NOT APPEAR IN THE CHROME CONSOLE
});
// !! get a handle on request
const request = httpTestingController.expectOne(request => request.url.includes('login'));
request.flush({/* flush what you would like for data to be logged */});
});
我正尝试在我的 Ionic 应用程序中开始使用单元测试,使用 Angular 6、Jasmine 和 Karma。
我有一个 loginPage,它调用一个 AuthService,后者又调用一个 HttpService,下面我将向您展示代码。
我遇到的问题是,如果我调用 doLogin 函数(在 loginPage 中)或者如果我调用登录函数(在 authService 中),我永远无法得到响应。我尝试使用订阅并执行 toPromise() 将 Observable 转换为 Promise,但我也没有成功。
你能帮帮我吗? 非常感谢。
LOGINPAGE.TS
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Credentials } from '../models/credentials';
import { AuthService } from '../services/auth.service';
@Component({
selector: 'app-login',
templateUrl: './login.page.html',
styleUrls: ['./login.page.scss'],
})
export class LoginPage implements OnInit {
ionicForm: FormGroup;
isSubmitted: boolean = false;
credential: Credentials;
constructor(public formBuilder: FormBuilder, private _authService: AuthService) { }
ngOnInit() {
this.ionicForm = this.formBuilder.group({
email: ['eve.holt@reqres.in', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
}
doLogin1() {
this.isSubmitted = true;
if (!this.ionicForm.valid) {
return false;
} else {
return true
}
}
async doLogin(): Promise<Credentials | false> {
this.isSubmitted = true;
if (!this.ionicForm.valid) {
return false;
} else {
this.credential = await this._authService.login(this.ionicForm.value);
return this.credential;
}
}
get errorControl() {
return this.ionicForm.controls;
}
}
AUTHSERVICE.TS
import { Injectable } from '@angular/core';
import { HttpService } from './http.service';
@Injectable({
providedIn: 'root'
})
export class AuthService {
constructor(private _httpService: HttpService) { }
async login(body){
return await this._httpService.post('https://reqres.in/api/login',body).toPromise();
}
}
HTTPSERVICE.TS
import { Injectable } from '@angular/core';
import { Observable, Subject, throwError } from 'rxjs';
import { HttpClient, HttpErrorResponse, HttpHeaders} from '@angular/common/http';
import { catchError } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
})
export class HttpService {
errorLogin = new Subject<boolean>();
constructor(private httpClient: HttpClient) {
}
public get(path: string, options?: any): Observable<any> {
return this.httpClient.get(path,options).pipe(catchError(this.formatErrors));
}
public put(path: string, body: object = {},headers?:any): Observable<any> {
let options = {
headers: headers
}
return this.httpClient.put(path, JSON.stringify(body),options).pipe(catchError(this.formatErrors));
}
public post(path: string, body: object = {},options?: any): Observable<any> {
return this.httpClient.post(path,body,options).pipe(catchError(this.formatErrors));
}
public delete(path: string, headers?:any): Observable<any> {
return this.httpClient.delete(path, {headers,responseType: 'text' }).pipe(catchError(this.formatErrors));
}
public appendHeaders(header:HttpHeaders){
if (header != undefined){
return header.set('Content-Type','application/json');
}
return new HttpHeaders().set('Content-Type','application/json');
}
public formatErrors(error: HttpErrorResponse): Observable<any> {
if (error.status === 401 && error.error.message == 'Authentication failed'){
try {
this.errorLogin.next(true);
} catch(error){
}
}
return throwError(error);
}
showCredError(){
this.errorLogin.next(true);
}
hideCredError(){
this.errorLogin.next(false);
}
/*
Metodo de prueba, eliminar al pasar a producción
*/
public testRest(){
return this.httpClient.get('https://jsonplaceholder.typicode.com/todos/1');
}
}
LOGINPAGE.SPEC.TS
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { IonicModule } from '@ionic/angular';
import { HttpClientTestingModule, } from '@angular/common/http/testing';
import { LoginPage } from './login.page';
import { AuthService } from '../services/auth.service';
import { HttpService } from '../services/http.service';
import { of } from 'rxjs';
import { Credentials } from '../models/credentials';
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
let authService: AuthService;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot(),
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule],
providers: [AuthService, HttpService]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
fixture.detectChanges();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
}));
it('A) should create', () => {
expect(component).toBeTruthy();
});
it('B) send form without data', () =>{
component.ionicForm = component.formBuilder.group({
email: ['', [Validators.required, Validators.minLength(8)]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorUsernameRequired'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.errorPasswordRequired'))).not.toBeNull();
});
it('C) send form without password', () =>{
component.ionicForm = component.formBuilder.group({
email: ['UsuarioPrueba', [Validators.required, Validators.minLength(8)]],
password: ['', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorUsernameRequired'))).toBeNull();
expect(fixture.debugElement.query(By.css('.errorPasswordRequired'))).not.toBeNull();
});
it('D) send form without username', () =>{
component.ionicForm = component.formBuilder.group({
email: ['', [Validators.required, Validators.minLength(8)]],
password: ['ContraseñaPrueba', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorUsernameRequired'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.errorPasswordRequired'))).toBeNull();
});
it('E) send form with minor lenght in both inputs', () =>{
component.ionicForm = component.formBuilder.group({
email: ['ABCDEF', [Validators.required, Validators.minLength(8)]],
password: ['ABCDEF', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
expect(data).toBeFalse();
});
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('.errorPasswordMinLength'))).not.toBeNull();
expect(fixture.debugElement.query(By.css('.errorUsernameMinLength'))).not.toBeNull();
});
it('F) send form successfully', (()=>{
component.ionicForm = component.formBuilder.group({
email: ['eve.holt@reqres.in', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
console.log(data);
// THIS CONSOLE LOG NOT APPEAR IN THE CHROME CONSOLE
})
}));
});
我会放弃 HttpClientTestingModule
并直接模拟 AuthService
。
在行后面加上 // !!:
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
// !! change this line to mockAuthService
let mockAuthService: jasmine.SpyObj<AuthService>;
beforeEach(waitForAsync(() => {
// !! assign mockAuthService to mock here
// !! first string argument is optional, 2nd array of strings are public methods you would like to mock
mockAuthService = jasmine.createSpyObj<AuthService>('AuthService', ['login']);
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot(),
FormsModule,
ReactiveFormsModule],
// !! provide mock for the real AuthService
providers: [{ provide: AuthService, useValue: mockAuthService }]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
fixture.detectChanges();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
}));
// !! you have an extra bracket after the ', and I think this could be a problem
it('F) send form successfully', ()=>{
component.ionicForm = component.formBuilder.group({
email: ['eve.holt@reqres.in', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
mockAuthService.login.and.returnValue(Promise.resolve({/* mock login respond here */}));
// !! below should hopefully work now
component.doLogin().then((data)=>{
console.log(data);
// THIS CONSOLE LOG NOT APPEAR IN THE CHROME CONSOLE
})
})
以上是单元测试。单元测试的心态应该是假设其他一切正常,我的 component/class/etc 是否如此。做它应该做的事?阅读 this 以了解模拟外部依赖项。
您的目标是集成测试(提供实际的 AuthService),我认为我们也可以做到。我们必须查看队列中的 http 请求并响应它。
describe('LoginPage', () => {
let component: LoginPage;
let fixture: ComponentFixture<LoginPage>;
let authService: AuthService;
// !! get a handle on httpTestingController
let httpTestingController: HttpTestingController;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
declarations: [ LoginPage ],
imports: [IonicModule.forRoot(),
FormsModule,
ReactiveFormsModule,
HttpClientTestingModule],
providers: [AuthService, HttpService]
}).compileComponents();
fixture = TestBed.createComponent(LoginPage);
component = fixture.componentInstance;
// !! get handle, if you're using Angular 9+, get should be inject
httpTestingController = TestBed.get(HttpTestingController);
fixture.detectChanges();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000;
}));
// !! remove the ( after ',
it('F) send form successfully', () => {
component.ionicForm = component.formBuilder.group({
email: ['eve.holt@reqres.in', [Validators.required, Validators.minLength(8)]],
password: ['cityslicka', [Validators.required, Validators.minLength(8)]],
});
component.doLogin().then((data)=>{
console.log(data);
// THIS CONSOLE LOG NOT APPEAR IN THE CHROME CONSOLE
});
// !! get a handle on request
const request = httpTestingController.expectOne(request => request.url.includes('login'));
request.flush({/* flush what you would like for data to be logged */});
});