在开发Angular应用时,网络通信是不可避免的一部分。为了确保网络通信服务的稳定性和可靠性,进行单元测试显得尤为重要。本文将结合master-data.service.ts
和master-data.service.spec.ts
两个文件,详细阐述如何对Angular中的网络通信服务进行单元测试。
网络通信服务设计
在master-data.service.ts
中,我们定义了一个名为MasterDataService
的服务,它负责与后端API进行交互,获取主数据。该服务依赖于Angular的HttpClient
模块来发送HTTP请求。服务提供了两个主要的方法:getAllMasterData
用于获取所有主数据,getMasterDataById
用于根据ID获取特定的主数据。
单元测试设计
在master-data.service.spec.ts
中,我们通过Angular的HttpClientTestingModule
和HttpTestingController
来模拟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
的单元测试,我们验证了服务在不同网络条件下的行为是否符合预期。这些测试用例不仅确保了服务的稳定性,还提高了代码的可维护性和可测试性。使用HttpClientTestingModule
和HttpTestingController
是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(); // 这个是确保接口被正确调用了
})
})