Angular方法特定分支测试覆盖率无法覆盖问题求助
解决Angular服务类测试覆盖率未覆盖特定分支的问题
问题描述
编写了Angular服务类AccountsChartService,通过HttpClient获取数据并处理后以Observable形式返回,但测试覆盖率始终无法覆盖loadAccountByChartService方法中以下分支:
list => { this.groupService.getGroupedAccounts(cT, bT, list).subscribe(response => { aCBS.next(response); }); }
尝试将aCBS、aSAD改为类实例变量后问题依旧,需找到可行的测试覆盖方案。
服务类代码
import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, Subject } from 'rxjs'; import { AccountDetail } from '../../models/accounts'; import { environment } from '../../../../environments/environment'; import { AccountDescriptionHelper } from '../../utils/account-subaccount'; import { GroupService } from '../group-structure'; import { TreeNode } from '../../models/treeNode'; @Injectable({ providedIn: 'root' }) export class AccountsChartService { private readonly ENDPOINT = `${environment}/ac`; constructor( private http: HttpClient, private groupService: GroupService, private accountDescriptionHelper: AccountDescriptionHelper ) {} loadAccountByChartService(bT: string, cT: string): Observable<Node[]> { const accountSubaccountDescEP = `${this.ENDPOINT}/cT/${cT}/ad`; let aCBS = new Subject<Node[]>(); let aSAD = new Subject<AccountDetail[]>(); this.http .get<AccountDetail[]>(accountSubaccountDescEP) .subscribe(response => { this.accountDescriptionHelper.storeMapFromDescriptionList(response); aSAD.next(response); }); aSAD.subscribe(list => { this.groupService.getGroupedAccounts(cT, bT, list).subscribe(response => { aCBS.next(response); }); }); return aCBS.asObservable(); } }
现有测试代码
it('should be created', () => { expect(service).toBeTruthy(); }); it('test', () => { httpClient = createHttpClient(); service = new AccountsChartService(httpClient as HttpClient, groupService, asdHelper); const a = spyOn(groupService, 'getGroupedAccounts') const result = service.testMethod('1', 'Test'); expect(result).toBeDefined(); result.subscribe(() => { expect(a).toHaveBeenCalled(); }); });
解决方案
1. 重构服务类(推荐)
原代码使用嵌套subscribe是RxJS反模式,不仅难以测试,还可能导致内存泄漏。改用RxJS操作符重构,消除内部Subject:
loadAccountByChartService(bT: string, cT: string): Observable<Node[]> { const accountSubaccountDescEP = `${this.ENDPOINT}/cT/${cT}/ad`; return this.http.get<AccountDetail[]>(accountSubaccountDescEP).pipe( // 执行存储操作,不修改流数据 tap(response => this.accountDescriptionHelper.storeMapFromDescriptionList(response)), // 切换到groupService的流,直接返回最终结果 switchMap(list => this.groupService.getGroupedAccounts(cT, bT, list)) ); }
2. 编写正确的测试用例
使用Angular官方测试工具HttpClientTestingModule和HttpTestingController模拟HTTP请求,同时依赖注入模拟的GroupService:
import { TestBed } from '@angular/core/testing'; import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; import { AccountsChartService } from './accounts-chart.service'; import { GroupService } from '../group-structure'; import { AccountDescriptionHelper } from '../../utils/account-subaccount'; import { of } from 'rxjs'; import { environment } from '../../../../environments/environment'; import { AccountDetail } from '../../models/accounts'; describe('AccountsChartService', () => { let service: AccountsChartService; let httpTestingController: HttpTestingController; let groupService: jasmine.SpyObj<GroupService>; let accountHelper: jasmine.SpyObj<AccountDescriptionHelper>; beforeEach(() => { // 创建依赖的模拟对象 const groupSpy = jasmine.createSpyObj('GroupService', ['getGroupedAccounts']); const helperSpy = jasmine.createSpyObj('AccountDescriptionHelper', ['storeMapFromDescriptionList']); TestBed.configureTestingModule({ imports: [HttpClientTestingModule], providers: [ AccountsChartService, { provide: GroupService, useValue: groupSpy }, { provide: AccountDescriptionHelper, useValue: helperSpy } ] }); // 获取实例 service = TestBed.inject(AccountsChartService); httpTestingController = TestBed.inject(HttpTestingController); groupService = TestBed.inject(GroupService) as jasmine.SpyObj<GroupService>; accountHelper = TestBed.inject(AccountDescriptionHelper) as jasmine.SpyObj<AccountDescriptionHelper>; }); afterEach(() => { // 验证所有HTTP请求都被处理 httpTestingController.verify(); }); it('should cover all branches in loadAccountByChartService', () => { const mockAccountDetails: AccountDetail[] = [{ id: '1', name: 'Test Account' }]; const mockGroupedNodes: Node[] = [{ id: 'group1', label: 'Test Group' }]; const bT = '1'; const cT = 'Test'; const expectedEndpoint = `${environment}/ac/cT/${cT}/ad`; // 模拟groupService的返回值 groupService.getGroupedAccounts.and.returnValue(of(mockGroupedNodes)); // 调用服务方法并订阅结果 service.loadAccountByChartService(bT, cT).subscribe(result => { // 验证最终返回结果正确 expect(result).toEqual(mockGroupedNodes); // 验证存储方法被调用 expect(accountHelper.storeMapFromDescriptionList).toHaveBeenCalledWith(mockAccountDetails); // 验证分组方法被调用,参数正确 expect(groupService.getGroupedAccounts).toHaveBeenCalledWith(cT, bT, mockAccountDetails); }); // 拦截HTTP请求并返回模拟数据 const req = httpTestingController.expectOne(expectedEndpoint); expect(req.request.method).toBe('GET'); req.flush(mockAccountDetails); }); });
3. 若不重构原代码的临时方案
如果暂时无法重构,需将aSAD改为类实例变量,然后在测试中手动触发流:
// 服务类修改:将aSAD改为实例变量 export class AccountsChartService { private aSAD = new Subject<AccountDetail[]>(); // ...其他代码不变 loadAccountByChartService(bT: string, cT: string): Observable<Node[]> { // ...其他代码不变,去掉let aSAD = new Subject<AccountDetail[]>(); } } // 测试代码修改 it('should cover the target branch', () => { const mockList: AccountDetail[] = [{ id: '1' }]; const mockGrouped = [{ id: 'group1' }]; spyOn(groupService, 'getGroupedAccounts').and.returnValue(of(mockGrouped)); const result = service.loadAccountByChartService('1', 'Test'); // 手动触发aSAD的next,触发目标分支 service['aSAD'].next(mockList); result.subscribe(res => { expect(res).toEqual(mockGrouped); expect(groupService.getGroupedAccounts).toHaveBeenCalled(); }); });
方案说明
- 重构方案通过RxJS操作符消除嵌套订阅,代码更简洁易维护,同时测试时能自然触发所有分支,完全覆盖代码。
- 临时方案仅适用于无法重构的场景,需通过访问私有变量触发流,不推荐长期使用。
内容的提问来源于stack exchange,提问作者Kashif999
相关产品推荐
相关产品推荐

