如何为Angular服务类和httpclient的依赖关系编写单元测试

123 阅读4分钟

在本教程中,你将学习如何通过示例测试案例对angular服务进行单元测试。它还包括以下内容

  • 如何设置单元测试的Angular服务
  • 单元测试服务功能
  • 用jasmine库模拟依赖关系
  • 用karma库断言预期和实际

单元测试是测试一段代码的运行是否符合预期。

在我之前的文章中,我们讨论了关于Angular服务的完整教程和实例

我们有一个服务,其中包含可重用的业务方法,这些方法是独立的

  • 非同步函数
  • 同步函数
  • 有依赖关系的服务方法

我们必须写一个单元测试代码来独立测试该服务。

我假设你已经创建了一个angular应用程序。

让我们用下面的命令创建一个Angular服务

ng g service employee

这里是一个输出

B:\angular-app-testing>ng g service employee
CREATE src/app/employee.service.spec.ts (347 bytes)
CREATE src/app/employee.service.ts (133 bytes)

这是一个服务代码,我们用它来编写测试案例employee.service.ts

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'
})
export class EmployeeService {

  constructor() { }
}

这里是服务规范文件,Angular service code employee.service.spec.ts

import { TestBed } from '@angular/core/testing';

import { EmployeeService } from './employee.service';

describe('EmployeeService', () => {
  let service: EmployeeService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(EmployeeService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

考虑到这是一个雇员服务,它有以下的服务方法

  • 创建一个雇员
  • 更新一个雇员
  • 获取所有雇员
  • 删除一个雇员

假设雇员存储在数据库中,所以我们必须使用一个http连接到数据库。所以Angular在HttpClient 类中提供了http连接包装器。该服务对类有依赖性,服务做了以下事情

  • 通过构造函数向服务注入HttpClient方法,如下例所示

这里是一个Angular Service example

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from "rxjs/operators";
import { catchError } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class EmployeeService {
  private employee_api_url: string = 'api.localhost.com';

  constructor(private httpClient: HttpClient) { }

  createEmployee(employee: any): Observable {
    return this.httpClient.post(this.employee_api_url + '/create', employee)
      .pipe(map((resp: any) => resp.json()),
        catchError(error => this.throwError(error))
      )

  }
  getEmployees(): Observable {
    return this.httpClient.get(this.employee_api_url + '/read')
      .pipe(map((resp: any) => resp.json()),
        catchError(error => this.throwError(error))
      )

  }
  updateEmployee(employee: any): Observable {
    return this.httpClient.get(this.employee_api_url + '/update')
      .pipe(map((resp: any) => resp.json()),
        catchError(error => this.throwError(error))
      )

  }
  deleteEmployee(id: number): Observable {
    return this.httpClient.delete(this.employee_api_url + '/delete/{id}')
      .pipe(map((resp: any) => resp.json()),
        catchError(error => this.throwError(error))
      )

  }
  throwError(error: any) {
    console.error(error);
    return Observable.throw(error.json().error || 'Server error');
  }

}

我们必须为一个类的每个方法独立编写一个测试用例。

当你为服务编写测试用例时,通常你需要做以下步骤

  • 获取一个类的对象
  • 调用一个函数方法,传递不同的值
  • 最后用预期值断言结果。

默认情况下,angular cli会创建一个单元测试类文件,也叫规格文件,如下所示

默认生成的代码为angular unit testing service example

import { TestBed } from '@angular/core/testing';

import { EmployeeService } from './employee.service';

describe('EmployeeService', () => {
  let service: EmployeeService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(EmployeeService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });
});

让我们来讨论一下规格文件的步骤

describe('EmployeeService', () => {});

describe 是Jasmine框架中的一个关键字,它是用来指定逻辑上的分离,以告诉测试案例的分组。它包含beforeeach和it 方法。

beforeEach 用于初始化和声明所有的依赖和实例,这些都是在it 方法中定义的测试用例。

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

TestBed是一个Angular测试模块,在测试容器中加载和运行Angular组件。

    TestBed.configureTestingModule({});

由于Angular服务对HttpClient有依赖性,所以要创建一个服务对象,以发送http请求和响应,所以你需要提供HttpClient对象的实例。

如何为Angular服务创建一个HttpClient实例?

一般来说,在单元测试中,我们不会创建一个真实的类的实例,我们会创建一个HttpClient的模拟实例,模拟是一个假的对象,而不是真实的对象。我们不发送真实的http请求和响应,而是发送假的请求和响应。

为了创建一个httpClient的模拟实例,Angular提供了HttpClientTestingModule 首先,将HttpClientTestingModule导入到你的测试规范文件中,如下图所示 将EmployeeService 作为TestBed提供者的一个依赖项。

  TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [EmployeeService],
    });

让我们来讨论一下测试用例

  • 检查服务实例创建与否的测试用例
it('should be created', () => {
    const service: EmployeeService = TestBed.get(EmployeeService);
    expect(service).toBeTruthy();

  });
  • 角度服务方法的可观察性的测试用例

写一个雇员创建的步骤顺序

  • 首先从EmployeeService对象中获取TestBed

  • 使用Jasmine的spyOn 方法在createEmployee方法上创建一个模拟对象。

    spyon包含两个方法

    • withArgs用于输入模拟方法的值
    • returnValue包含返回值,是可观察的
  • 接下来,调用真正的服务方法并订阅它,在订阅中检查返回值与实际值和预期值的断言。

  • 最后使用toHaveBeenCalled 方法检查服务方法是否被调用。

 it('Create an Employee on Service method', () => {
    const service: EmployeeService = TestBed.get(EmployeeService);
    let employeeServiceMock = spyOn(service, 'createEmployee').withArgs({})
      .and.returnValue(of('mock result data'))

    service.createEmployee({}).subscribe((data) => {
      console.log("called")
      expect(data).toEqual(of('mock result data'));
    }); 
    expect(service.createEmployee).toHaveBeenCalled();
  
  });

NullInjectorError:在单元测试服务时没有HttpClient的提供者的错误

这看起来像是一个HttpClientModule在测试案例模块中没有被正确导入。

import { HttpClientTestingModule } from '@angular/common/http/testing';

在组件规格文件的@ngModule中。

你可以包括HttpClientModule ,如下所示

 //first testbed definition, move this into the beforeEach
TestBed.configureTestingModule({
  imports: [HttpClientTestingModule],
  providers: [EmployeeService]
});

总结

你学会了用httpclient和依赖关系以及模拟可观察方法对angular服务进行单元测试。