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

Angular中含ngOnInit的Mat-Dialog组件单元测试覆盖率问题

Angular Mat-Dialog组件单元测试:ngOnInit及方法未被代码覆盖

问题描述

我正在尝试对带有ngOnInit方法的Angular Mat-Dialog组件进行单元测试,目前仅构造函数代码被覆盖,其余方法无法调用。我已模拟Mat-Dialog、FormsModule及相关服务,以下是待测试的组件TS文件和测试Spec文件:

组件TS文件

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialog,MatDialogRef } from '@angular/material/dialog';
import { PieceManagementService } from 'src/app/shared/services/piece-management/piece-management.service';
import { CommonService } from 'src/app/shared/services/common.service';
import { commonConstants } from 'src/app/shared/utils/constant';

@Component({
  selector: "app-add-pieces",
  templateUrl: "./add-pieces.component.html",
  styleUrls: ["./add-pieces.component.scss"]
})
export class AddPiecesComponent implements OnInit {

  selectedPieces:any = [];
  pieces:any = [];
  errorPiece : string;
  empForm: FormGroup;
  pieceName:any;
  nom:any;
  errorMessage:string;
  public pieceForm: FormGroup;
  public pieceValidationMessages = commonConstants.pieceValidationMessages;

  constructor(private _fb: FormBuilder, private pieceManagement:PieceManagementService,
    public dialogRef: MatDialogRef<AddPiecesComponent>, private commonService:CommonService) { 
    }

  ngOnInit(): void {
    this.createPiecesForm();
    this.fetchAllZones();
  }

  public fetchAllZones(){
    this.pieceManagement.fetchAllZones().subscribe((response: any) => {
        response.forEach((item)=>{
          return item.active = false;
        })
        this.pieces = response;
        console.log(this.pieces);
    });
  }

  createPiecesForm(){
    this.pieceForm = this._fb.group({
      nom: ['',Validators.required]
    });
  }

  activateClass(subModule){
    subModule.active = !subModule.active;  
    subModule.activeli = !subModule.activeli; 
    if (this.selectedPieces.length === 0 && subModule.active === true){
      this.selectedPieces.push(subModule);
    } else {
      let index = this.selectedPieces.findIndex((piece) => piece.zoneId === subModule.zoneId);
      if (subModule.active === true && index === -1) {
        this.selectedPieces.push(subModule);
      }
      if (subModule.active === false && index >= 0) {
        this.selectedPieces.splice(index, 1);
      }
    }

  }
  
  createTest(){
    if(this.selectedPieces.length == 0){
      this.errorMessage = "Please select zone";
    }
    else if (this.pieceForm.valid){
      const param ={
        "pieceName":this.pieceForm.controls.nom.value,
        "selectedZoneList":this.selectedPieces
      }
    this.pieceManagement.saveZones(param).subscribe((response: any) => {
     this.commonService.openSnackBar('Piece have been added successfully!')
      this.dialogRef.close({event:'add'});
     });
  }
  }
  close(){
    this.dialogRef.close({event:'cancel'})
  }
}

测试Spec文件

import { ComponentFixture, TestBed } from '@angular/core/testing'; import { AddPiecesComponent } from './add-pieces.component'; import { NO_ERRORS_SCHEMA } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { MatDialog, MatDialogRef } from "@angular/material/dialog"; import { FormsModule } from "@angular/forms"; import { CommonService } from 'src/app/shared/services/common.service'; import { PieceManagementService } from 'src/app/shared/services/piece-management/piece-management.service';
    
describe('AddPiecesComponent', () => {   let component: AddPiecesComponent;   let fixture: ComponentFixture<AddPiecesComponent>;   beforeEach(() => {
            const formBuilderStub = () => ({ group: object => ({}) });
            const matDialogStub = () => ({});
            const matDialogRefStub = () => ({ close: object => ({}) });
            const pieceManagementServiceStub = () => ({
              fetchAllZones: () => ({ subscribe: f => f({}) }),
              saveZones: param => ({ subscribe: f => f({}) })
            });
            const commonServiceStub = () => ({ openSnackBar: string => ({}) });
            TestBed.configureTestingModule({
              imports: [FormsModule],
              schemas: [NO_ERRORS_SCHEMA],
              declarations: [AddPiecesComponent],
              providers: [
                { provide: FormBuilder, useFactory: formBuilderStub },
                { provide: MatDialog, useFactory: matDialogStub },
                { provide: MatDialogRef, useFactory: matDialogRefStub },
                {
                  provide: PieceManagementService,
                  useFactory: pieceManagementServiceStub
                },
                { provide: CommonService, useFactory: commonServiceStub }
              ]
            });
            fixture = TestBed.createComponent(AddPiecesComponent);
            component = fixture.componentInstance;   });
    
          it('can load instance', () => {
            expect(component).toBeTruthy();   });
    
          it(`selectedPieces has default value`, () => {
            expect(component.selectedPieces).toEqual([]);   });
    
          it(`pieces has default value`, () => {
            expect(component.pieces).toEqual([]);   });
    
          describe('ngOnInit', () => {
            it('makes expected calls', () => {
              spyOn(component, 'createPiecesForm').and.callThrough();
              spyOn(component, 'fetchAllZones').and.callThrough();
              component.ngOnInit();
              expect(component.createPiecesForm).toHaveBeenCalled();
              expect(component.fetchAllZones).toHaveBeenCalled();
            });   }); });

解决方案

要覆盖组件中所有方法的代码,需要针对每个方法编写独立测试用例,同时完善依赖服务的模拟逻辑,以下是具体步骤:

1. 完善FormBuilder模拟

当前的FormBuilder stub仅返回空对象,无法正确初始化表单,修改模拟逻辑:

// 在测试文件顶部导入FormGroup
import { FormGroup, Validators } from '@angular/forms';

// 修改FormBuilder stub
const formBuilderStub = () => ({
  group: (controls) => new FormGroup(controls)
});

2. 测试fetchAllZones方法

模拟服务返回真实数组,验证组件对响应的处理逻辑:

// 导入rxjs的of操作符
import { of } from 'rxjs';

describe('fetchAllZones', () => {
  it('should populate pieces array and set active to false for each item', () => {
    const mockZones = [{ zoneId: 1 }, { zoneId: 2 }];
    const service = TestBed.inject(PieceManagementService);
    spyOn(service, 'fetchAllZones').and.returnValue(of(mockZones));
    
    component.fetchAllZones();
    
    expect(component.pieces.length).toBe(2);
    expect(component.pieces.every(item => item.active === false)).toBe(true);
  });
});

3. 测试activateClass方法的选择逻辑

覆盖选中、取消选中的分支场景:

describe('activateClass', () => {
  it('should add subModule to selectedPieces on first selection', () => {
    const subModule = { zoneId: 1, active: false, activeli: false };
    
    component.activateClass(subModule);
    
    expect(subModule.active).toBe(true);
    expect(component.selectedPieces.length).toBe(1);
  });

  it('should remove subModule from selectedPieces when deselected', () => {
    const subModule = { zoneId: 1, active: true, activeli: true };
    component.selectedPieces.push(subModule);
    
    component.activateClass(subModule);
    
    expect(subModule.active).toBe(false);
    expect(component.selectedPieces.length).toBe(0);
  });
});

4. 测试createTest方法的分支逻辑

覆盖未选区域、表单无效、表单有效三种场景:

describe('createTest', () => {
  let service: PieceManagementService;
  let commonService: CommonService;
  let dialogRef: MatDialogRef<AddPiecesComponent>;

  beforeEach(() => {
    service = TestBed.inject(PieceManagementService);
    commonService = TestBed.inject(CommonService);
    dialogRef = TestBed.inject(MatDialogRef);
    component.createPiecesForm(); // 初始化表单
  });

  it('should set errorMessage when no zones are selected', () => {
    component.createTest();
    expect(component.errorMessage).toBe('Please select zone');
  });

  it('should not call saveZones when form is invalid', () => {
    component.selectedPieces.push({ zoneId: 1 });
    component.pieceForm.controls['nom'].setValue('');
    
    const spy = spyOn(service, 'saveZones');
    component.createTest();
    
    expect(spy).not.toHaveBeenCalled();
  });

  it('should call saveZones and close dialog when form is valid', () => {
    const mockParam = { pieceName: 'Test', selectedZoneList: [{ zoneId: 1 }] };
    component.selectedPieces.push(mockParam.selectedZoneList[0]);
    component.pieceForm.controls['nom'].setValue(mockParam.pieceName);
    
    const saveSpy = spyOn(service, 'saveZones').and.returnValue(of({}));
    const snackSpy = spyOn(commonService, 'openSnackBar');
    const closeSpy = spyOn(dialogRef, 'close');
    
    component.createTest();
    
    expect(saveSpy).toHaveBeenCalledWith(mockParam);
    expect(snackSpy).toHaveBeenCalledWith('Piece have been added successfully!');
    expect(closeSpy).toHaveBeenCalledWith({ event: 'add' });
  });
});

5. 测试close方法

验证对话框关闭逻辑:

describe('close', () => {
  it('should close dialog with cancel event', () => {
    const dialogRef = TestBed.inject(MatDialogRef<AddPiecesComponent>);
    const spy = spyOn(dialogRef, 'close');
    
    component.close();
    
    expect(spy).toHaveBeenCalledWith({ event: 'cancel' });
  });
});

6. 触发真实生命周期钩子

用fixture.detectChanges()触发ngOnInit,贴近真实组件初始化场景:

it('should trigger ngOnInit on component init', () => {
  spyOn(component, 'createPiecesForm');
  spyOn(component, 'fetchAllZones');
  
  fixture.detectChanges();
  
  expect(component.createPiecesForm).toHaveBeenCalled();
  expect(component.fetchAllZones).toHaveBeenCalled();
});

完成以上修改后,单元测试即可覆盖组件所有方法和逻辑分支。


内容的提问来源于stack exchange,提问作者tripti tiwari

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 19:12:13