You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

覆盖率结果截图

预期行为

代码覆盖率应准确反映实际测试情况。


问题分析

  1. 未测试选择器被误标记为已覆盖:你仅测试了selectCoffeeList和getSelectedCoffee两个选择器,但覆盖率显示语句、行均为100%。这是因为NgRx的createSelector在定义时就会执行创建语句,但内部的投影函数(提取状态的回调)只有在选择器被调用时才会真正执行。V8覆盖率默认将创建语句标记为已执行,却未正确追踪未被调用的投影函数,导致未测试的选择器代码被误判为已覆盖。

  2. 分支未覆盖的具体原因: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 13:24:57