Angular 网络通信服务单元测试

在开发Angular应用时,网络通信是不可避免的一部分。为了确保网络通信服务的稳定性和可靠性,进行单元测试显得尤为重要。本文将结合master-data.service.tsmaster-data.service.spec.ts两个文件,详细阐述如何对Angular中的网络通信服务进行单元测试。

网络通信服务设计

master-data.service.ts中,我们定义了一个名为MasterDataService的服务,它负责与后端API进行交互,获取主数据。该服务依赖于Angular的HttpClient模块来发送HTTP请求。服务提供了两个主要的方法:getAllMasterData用于获取所有主数据,getMasterDataById用于根据ID获取特定的主数据。

单元测试设计

master-data.service.spec.ts中,我们通过Angular的HttpClientTestingModuleHttpTestingController来模拟HTTP请求和响应,从而对MasterDataService进行单元测试。以下是几个关键步骤和测试用例的详细解释。

1. 测试环境配置

beforeEach钩子中,我们使用TestBed来配置测试环境。这包括导入HttpClientTestingModule以模拟HttpClient,并将MasterDataService添加到providers中。通过TestBed.inject方法获取服务实例和HTTP测试控制器实例。

2. 测试用例

测试一:测试正常网络情况下能否获取所有主数据
typescript 复制代码
it('Should get all data when the network works', () => {
  service.getAllMasterData().subscribe(
    data => {
      expect(data).toEqual(defaultData);
    }
  );

  const req = httpMock.expectOne('/api/masterDatas');
  expect(req.request.method).toEqual('GET');
  req.flush(defaultData);
  httpMock.verify();
});

这个测试用例模拟了网络请求成功的情况,验证服务返回的数据是否与默认数据一致。

测试二:测试非正常网络情况下是否获得正常的错误码
typescript 复制代码
it('Should get code 10 when the network doesn"t work', () => {
  errorMsg = 'Internal Server Error';
  service.getAllMasterData().subscribe(
    data => {
      expect(data.code).toBe(10);
      expect(data.message).toBe(errorMsg);
    }
  );

  const req = httpMock.expectOne('/api/masterDatas');
  expect(req.request.method).toEqual('GET');
  req.flush('', { status: 500, statusText: errorMsg });
  httpMock.verify();
});

这个测试用例模拟了网络请求失败的情况,验证服务是否正确处理了错误,并返回了预期的错误信息。

测试三:测试正常网络情况下并且ID有效时能否获取所对应ID的数据
typescript 复制代码
it('Should get right data by id', () => {
  const id = "ENO#219481951";
  service.getMasterDataById(id).subscribe(
    data => {
      expect(data).toEqual(defaultData.find(v => v.id === id));
    }
  );

  const req = httpMock.expectOne('/api/masterDatas');
  expect(req.request.method).toEqual('POST');
  req.flush({
    success: true,
    data: defaultData.find(v => v.id === id),
  });
  httpMock.verify();
});

这个测试用例验证了当ID有效时,服务能否正确返回对应的数据。

测试四:测试正常网络情况下并且ID无效时能否获取正确的错误码
typescript 复制代码
it('Shouldn"t get right data by id', () => {
  const id = "ENO#219481952";
  service.getMasterDataById(id).subscribe(
    data => {
      expect(data.code).toBe(10);
    }
  );

  const req = httpMock.expectOne('/api/masterDatas');
  expect(req.request.method).toEqual('POST');
  req.flush({
    success: false,
    data: {},
  });
  httpMock.verify();
});

这个测试用例验证了当ID无效时,服务是否能正确处理错误并返回预期的错误码。

总结

通过对MasterDataService的单元测试,我们验证了服务在不同网络条件下的行为是否符合预期。这些测试用例不仅确保了服务的稳定性,还提高了代码的可维护性和可测试性。使用HttpClientTestingModuleHttpTestingController是Angular中进行网络通信服务单元测试的一种有效方式,值得在开发过程中广泛应用。

附录

master-data.service.ts

ts 复制代码
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable, of, throwError } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';

// 默认数据,用于模拟或初始化  
export const defaultData = [
  {
    id: "ENO#219481951",
    status: "Running",
    eui: "24E124723D4951091",
    description: 'AA',
    lowTmp: -12.5,
    highTmp: 35.5,
    ptoNum: "SD 20240101",
    propId: "PID#12345678",
    propOrg: "Proponent Org.A",
    vendorId: "VID#12345678",
    companyName: "Tamimi",
    location: "Midra Tower",
    floorNum: "1st floor",
    facilityDesp: "Room 302",
    detailedLoc: "Warmer#03",
    createdBy: "TOM CARVERS",
    createdTime: +new Date(),
    updatedBy: "TOM CARVERS",
    updatedTime: +new Date(),
    remark: "Some example text that's free-flowing within the dropdown menu.\n\nAnd this is more example text."
  },
];

@Injectable({
  providedIn: 'root' // 表示该服务将在应用的根模块中提供  
})
export class MasterDataService {
  private newData: any[] = defaultData; // 存储默认数据或新的数据  

  constructor(private http: HttpClient) { } // 依赖注入HttpClient  

  // 根据ID获取主数据  
  getMasterDataById(id: string): Observable<any> {
    if (!id || id == '-1') return of({} as any); // 如果没有ID或ID为'-1',则返回一个空的Observable  
    return this.http.post<any>('/api/masterDatas', { id }).pipe(
      tap(data => console.log(data)), // 在控制台打印数据  
      map((data: any) => { // 处理响应数据  
        const { success, data: masterData } = data;
        if (success) {
          return masterData;
        } else {
          throw 'Error'; // 如果不成功,抛出错误  
        }
      }),
      catchError(err => of(this.handleHttpError(err))) // 捕获错误并处理  
    );
  }

  // 获取所有主数据  
  getAllMasterData(): Observable<any> {
    return this.http.get<any>('/api/masterDatas')
      .pipe(
        catchError(err => of(this.handleHttpError(err)))
      )
  }

  // 处理HTTP错误  
  private handleHttpError(err: HttpErrorResponse) {
    let dataError = {} as any;
    dataError.code = 10;
    dataError.message = err.statusText; // 设置错误消息  
    dataError.friendlyMessage = 'An error occured retrieving data'; // 设置友好的错误消息  
    return dataError;
  }
}

master-data.service.spec.ts

ts 复制代码
/**
 * 本文件的代码旨在展示如何更加真实的对一个负责网络通信的服务进行测试
 * 难点在于,网络通信一定会用到 HttpClient 这个服务,所以在测试自定义个服务的时候,如何 mock HttpClient 是比较关键的
 * 第二个难点在于,我们如何测试我们的服务,这里当然不是用 new 的方法,而是有一套既定的流程
 * 还有一个难点,或者说比较容易混淆的地方,那就是我们不是真的使用 HttpClient 去请求数据,而是 mock 数据。所以怎么 mock 是一大难点,包括成功和失败的情况
 * 最后,如何检验呢?
 */
import { MasterDataService, defaultData } from "./master-data.service"; // 我们引入 MasterDataService 这个待测服务,并不是要 new 它
import { TestBed } from "@angular/core/testing"; // 需要记住的是,一旦涉及 HttpClient 的 mock 就必须使用 TestBed 来配置编译环境
import { HttpClientTestingModule, HttpTestingController } from "@angular/common/http/testing"; // 这是 mock HtpClient 雷打不动的两大支柱

describe('Master Data Service Testing', () => {
  let service: MasterDataService; // 待测服务实例,但非 new 获得
  let httpMock: HttpTestingController; // 从 controller 这个名字就可以看出来,它在此次测试中占据中枢作用
  let errorMsg: string; // 这是网络请求失败之后展示的错误信息

  beforeEach(() => {
    // 这里需要澄清一下:并非只有测试 component 的时候才需要 configureTestingModule, 很有可能在其它地方都需要用到
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule], // import 支柱一
      providers: [MasterDataService], // 在 providers 中声明服务,私以为 providedIn: 'root' 失效
    });

    service = TestBed.inject(MasterDataService); // 不是通过 new 的方式,而是通过 TestBed.inject() 的方式获取实例
    httpMock = TestBed.inject(HttpTestingController); // 不是通过 new 的方式,而是通过 TestBed.inject() 的方式获取实例
  })

  // 测试一:测试正常网络情况下能否获取所有 master data 的数据
  it('Should get all data when the network works', () => {
    // 测试实例执行方法并在 subscribe 中进行断言
    service.getAllMasterData().subscribe(
      data => {
        expect(data).toEqual(defaultData);
      }
    )

    // 使用控制器实例截取此次网络请求并拿到请求体
    const req = httpMock.expectOne('/api/masterDatas');
    // 测试请求的方法是否正确
    expect(req.request.method).toEqual('GET');
    // mock 返回值
    req.flush(defaultData); // 使用 flush 来模拟返回的数据  
    // 检查网络请求是否正确关闭
    httpMock.verify(); // 确保没有未完成的请求 
  })

  // 测试二:测试非正常网络情况下是否获得正常的错误码
  it('Should get code 10 when the network doesn"t work', () => {
    // 模拟错误的情况,设置错误信息
    errorMsg = 'Internal Server Error';
    service.getAllMasterData().subscribe(
      data => {
        expect(data.code).toBe(10);
        expect(data.message).toBe(errorMsg);
      }
    )

    const req = httpMock.expectOne('/api/masterDatas');
    expect(req.request.method).toEqual('GET');

    // 使用 flush 来模拟返回的错误  
    req.flush('', { status: 500, statusText: errorMsg });
    httpMock.verify(); // 确保没有未完成的请求 
  })

  // 测试三:测试正常网络情况下并且 id 有效时能否获取所对应 id 的数据
  it('Should get right data by id', () => {
    const id = "ENO#219481951";
    service.getMasterDataById(id).subscribe(
      data => {
        expect(data).toEqual(defaultData.find(v => v.id === id));
      }
    )

    const req = httpMock.expectOne('/api/masterDatas');
    expect(req.request.method).toEqual('POST');
    req.flush({
      success: true,
      data: defaultData.find(v => v.id === id),
    });
    httpMock.verify();
  })

  // 测试四:测试正常网络情况下并且 id 无效时能否获取正确的错误码
  it('Shouldn"t get right data by id', () => {
    const id = "ENO#219481952";
    service.getMasterDataById(id).subscribe(
      data => {
        expect(data.code).toBe(10);
      }
    )

    const req = httpMock.expectOne('/api/masterDatas');
    expect(req.request.method).toEqual('POST');
    req.flush({
      success: false,
      data: {},
    });
    httpMock.verify();
  })

  // 在每一个测试最后再次验证网络请求是否完成
  afterEach(() => {
    httpMock.verify(); // 这个是确保接口被正确调用了
  })
})
相关推荐
勘察加熊人3 天前
andrular输入框input监听值传递
angular.js
勘察加熊人3 天前
Angular解析本地json文件
javascript·json·angular.js
勘察加熊人4 天前
angular实现list列表和翻页效果
javascript·angular.js
界面开发小八哥4 天前
界面控件Kendo UI for Angular 2024 Q3亮点 - 全新的页面模板
前端·ui·界面控件·kendo ui·angular.js·ui开发
勘察加熊人6 天前
angular登录按钮输入框监听
angular.js
勘察加熊人6 天前
angular使用http实现get和post请求
前端·http·angular.js
勘察加熊人7 天前
angular实现dialog弹窗
javascript·ecmascript·angular.js
勘察加熊人7 天前
angular实现calculate计算器
angular.js
无敌喜之郎7 天前
Angular中ChangeDetectorRef.detectChanges是如何实现的,对比vue种的nextTick有何不同
前端·vue.js·angular.js·视图同步
界面开发小八哥7 天前
界面控件DevExpress JS & ASP.NET Core v24.1亮点 - 支持Angular 18
javascript·ui·asp.net·界面控件·angular.js·devexpress