Angular项目Jest代码覆盖率结果异常求助
代码覆盖率不符合预期问题排查
我创建了一个小项目用来检查代码覆盖率,但结果不符合预期。肯定是配置有问题,但我找不到要调整的地方。下面是能复现问题的最简文件,整个项目都存在这个问题。
依赖项
{"dependencies": {"@angular/animations": "^15.2.0","@angular/common": "^15.2.0","@angular/compiler": "^15.2.0","@angular/core": "^15.2.0","@angular/forms": "^15.2.0","@angular/platform-browser": "^15.2.0","@angular/platform-browser-dynamic": "^15.2.0","@angular/router": "^15.2.0","@ngrx/effects": "^15.3.0","@ngrx/entity": "^15.3.0","@ngrx/router-store": "^15.3.0","@ngrx/store": "^15.3.0","@ngrx/store-devtools": "^15.3.0","bootstrap": "^5.2.3","jasmine-marbles": "^0.9.1","rxjs": "~7.8.0","tslib": "^2.3.0","zone.js": "~0.12.0"},"devDependencies": {"@angular-builders/jest": "^15.0.0","@angular-devkit/build-angular": "^15.2.2","@angular-eslint/builder": "^15.2.1","@angular-eslint/eslint-plugin": "^15.2.1","@angular/cli": "~15.2.2","@angular/compiler-cli": "^15.2.0","@types/jest": "^29.4.1","@typescript-eslint/eslint-plugin": "^5.54.1","@typescript-eslint/parser": "^5.54.1","eslint": "^8.35.0","eslint-config-prettier": "^8.7.0","eslint-plugin-import": "^2.27.5","eslint-plugin-prettier": "^4.2.1","eslint-plugin-unused-imports": "^2.0.0","husky": "^4.2.5","jest": "^29.3.1","jest-environment-jsdom": "^29.5.0","jest-jasmine2": "^29.5.0","jest-junit": "^15.0.0","jest-preset-angular": "^13.0.0","lint-staged": "^13.2.0","prettier": "^2.8.4","ts-node": "10.9.1","typescript": "~4.9.4"}}
Jest配置
export default { displayName: 'coffee-project', testEnvironment: 'jsdom', extensionsToTreatAsEsm: ['.ts'], globals: { 'ts-jest': { tsconfig: '<rootDir>/tsconfig.spec.json', isolatedModules: true, }, }, transformIgnorePatterns: ['node_modules/(?!.*\.mjs$)'], resetMocks: true, coverageProvider: 'v8', }
测试文件
import {getSelectedCoffee, selectCoffeeList} from './coffee.selectors'; const coffeeDataMock = [ {"id": 9809,"uid": "e2bb804d-2f9c-4d4c-ac04-23d516c30e69","blend_name": "Express Bean","origin": "Nariño, Colombia","variety": "S795","notes": "balanced, smooth, graham cracker, leafy greens, medicinal","intensifier": "delicate"}, {"id": 9779,"uid": "545b2556-2249-4eee-bb93-16588ecfb446","blend_name": "Goodbye Cake","origin": "Estelí, Nicaragua","variety": "Gesha","notes": "wild, smooth, lemonade, tomato, cranberry","intensifier": "lingering"}, ]; export const CoffeeStateMock = { coffeeList: coffeeDataMock, selectedCoffee: coffeeDataMock[0], currentPage: 0, loader: false }; describe('Coffee selectors', () => { it('should select CoffeeList', () => { const result = selectCoffeeList({'coffeeState': CoffeeStateMock}); expect(result).toEqual(CoffeeStateMock.coffeeList) }); it('should select CoffeeList', () => { const result = getSelectedCoffee({'coffeeState': CoffeeStateMock}); expect(result).toEqual(CoffeeStateMock.selectedCoffee) }); })
被测文件
import {createFeatureSelector, createSelector} from '@ngrx/store'; import {CoffeeState} from './coffee.state'; import {CoffeeItem} from '../definitions/interface/coffee-item.interface'; export const getCoffeeState = createFeatureSelector<CoffeeState>('coffeeState'); export const selectCoffeeList = createSelector( getCoffeeState, (state: CoffeeState): CoffeeItem[] => state.coffeeList ); export const getSelectedCoffee = createSelector( getCoffeeState, (state: CoffeeState): CoffeeItem => state.selectedCoffee! ); export const selectCurrentPage = createSelector( getCoffeeState, (state: CoffeeState): number => state.currentPage ); export const getLoaderStatus = createSelector( getCoffeeState, (state: CoffeeState): boolean => state.loader ); export const getSlicedCoffeeList = createSelector( selectCoffeeList, selectCurrentPage, (coffeeList, currentPage) => { const pageItemsLimit = 10; const startIndex = currentPage * pageItemsLimit; const endIndex = startIndex + pageItemsLimit; return (coffeeList || [])?.slice(startIndex, endIndex); } );
文件覆盖率结果
100% Statements 36/36 83.33% Branches 5/6 100% Functions 0/0 100% Lines 36/36

预期行为
代码覆盖率应准确反映实际测试情况。
问题分析
未测试选择器被误标记为已覆盖:你仅测试了
selectCoffeeList和getSelectedCoffee两个选择器,但覆盖率显示语句、行均为100%。这是因为NgRx的createSelector在定义时就会执行创建语句,但内部的投影函数(提取状态的回调)只有在选择器被调用时才会真正执行。V8覆盖率默认将创建语句标记为已执行,却未正确追踪未被调用的投影函数,导致未测试的选择器代码被误判为已覆盖。分支未覆盖的具体原因:83.33%的分支覆盖率来自两个未覆盖场景中的一个:
getSelectedCoffee中的非空断言state.selectedCoffee!:你仅测试了selectedCoffee存在的情况,未覆盖其为null/undefined的场景;getSlicedCoffeeList中的coffeeList || []:你的测试数据中coffeeList始终存在,未覆盖其为null/undefined的分支。
解决方案
1. 补全所有选择器的测试用例
添加未测试选择器的测试,并覆盖分支场景:
import {getSelectedCoffee, selectCoffeeList, selectCurrentPage, getLoaderStatus, getSlicedCoffeeList} from './coffee.selectors'; // ... 已有常量和Mock数据 describe('Coffee selectors', () => { // ... 已有测试 it('should select current page', () => { const result = selectCurrentPage({'coffeeState': CoffeeStateMock}); expect(result).toEqual(CoffeeStateMock.currentPage); }); it('should get loader status', () => { const result = getLoaderStatus({'coffeeState': CoffeeStateMock}); expect(result).toEqual(CoffeeStateMock.loader); }); it('should get sliced coffee list when coffeeList exists', () => { const result = getSlicedCoffeeList({'coffeeState': CoffeeStateMock}); expect(result).toEqual(CoffeeStateMock.coffeeList); }); it('should return empty array when coffeeList is null', () => { const stateWithNullList = {...CoffeeStateMock, coffeeList: null}; const result = getSlicedCoffeeList({'coffeeState': stateWithNullList}); expect(result).toEqual([]); }); it('should throw error when selectedCoffee is null', () => { const stateWithNullSelected = {...CoffeeStateMock, selectedCoffee: null}; expect(() => getSelectedCoffee({'coffeeState': stateWithNullSelected})).toThrow(); }); });
2. 确认Jest覆盖率配置(可选)
确保jest.config.js中的collectCoverageFrom包含你的选择器文件,例如:
export default { // ... 已有配置 collectCoverageFrom: [ 'src/**/*.selectors.ts' ] }
验证效果
补全测试后重新运行覆盖率:
- 所有选择器的投影函数都会被调用,语句、行覆盖率将准确反映实际测试范围;
- 覆盖所有分支场景后,分支覆盖率将达到100%。
内容的提问来源于stack exchange,提问作者Kostas
相关产品推荐
相关产品推荐

