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 */});
  });