Ошибка модульного тестирования Angular — не удается прочитать свойство ‘subscribe’ неопределенного значения

#angular #typescript #unit-testing #karma-jasmine

#angular #typescript #модульное тестирование #karma-jasmine

Вопрос:

Я внес некоторые обновления в компонент Angular, и это нарушило некоторые модульные тесты. Все тестовые спецификации нарушены, поэтому мне кажется, что это как-то связано с инициализацией в beforeEach вызовах. Я пытался тщательно исследовать проблему, но не добился никакого успеха.

Ошибка, которую я получаю после запуска своих модульных тестов, заключается в: TypeError: Cannot read property 'subscribe' of undefined

Единственными вызовами, выполняемыми в subscribe , являются:

 this.ruleDataFieldOption$.subscribe
...
this.submitted$.subscribe
...
this.dataFieldControl.valueChanges.subscribe
  

Я попытался явно инициализировать, ruleDataFieldOption$ установив для него значение of(mockRuleDataFields) , но это не сработало. Я также попытался переместить блок кода из второго beforeEach вызова в обещание асинхронного beforeEach вызова (т.е. .compileComponents().then( () => {...} ) и это тоже не сработало.

client-rules-condition.component.spec.ts

 const mockDataFieldService = {
  getRuleDataFields: () => {}
} as RuleDataFieldService;

const mockStore = {
  select: td.function('.select'),
  dispatch: td.function('.dispatch'),
  pipe: td.function('.pipe'),
} as Store<any>;
let dispatch;
let pipe;

const mockConfigService = {
  getConfiguration: td.func(() => {
    return mockNotificationConfig
  })
} as ConfigService;

const mockNotificationConfig = {
  Env: null,
  apiBaseUrl: 'blah'
} as MclNotificationConfig;

const mockRuleDataFields: RuleDataFieldDefinition[] = [
  {name: 'data field 1', dataFieldId: 'mock data field guid', category: 'mock category'} as RuleDataFieldDefinition
];

fdescribe('ClientRulesConditionComponent', () => {
  let component: ClientRulesConditionComponent;
  let fixture: ComponentFixture<ClientRulesConditionComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ ClientRulesConditionComponent ],
      imports: [ReactiveFormsModule],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      providers: [
        {provide: Store, useFactory: () => mockStore},
        {provide: RuleDataFieldService, useValue: mockRuleDataFields},
        HttpClientTestingModule,

        // {provide: ConfigService, useFactory: () => mockConfigService},
        // {provide: MclNotificationConfig, useFactory: () => mockNotificationConfig},
        // HttpHandler, HttpClient
      ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    dispatch = spyOn(mockStore, 'dispatch');
    pipe = spyOn(mockStore, 'pipe').and.callThrough();
    fixture = TestBed.createComponent(ClientRulesConditionComponent);
    component = fixture.componentInstance;
    const fb: FormBuilder = new FormBuilder();
    component.conditionForm = fb.group({
      name: fb.control('', [Validators.required]),
      triggerWhen: fb.control('', [Validators.required]),
      conditions: fb.array([
        fb.group({
          dataField: fb.control('df1'),
          conditionToMatch: fb.control('ctm1'),
          value: fb.control('v1')
        }),
        fb.group({
          dataField: fb.control('df2'),
          conditionToMatch: fb.control('ctm2'),
          value: fb.control('v2')
        })
      ])
    });
    component.indexInConditionArray = 1;
    component.submitted$ = of(false);
    component.ruleDataFieldOption$ = of(mockRuleDataFields);
    fixture.detectChanges();
  });


  it('should create', () => {
    expect(component).toBeTruthy();

    // Confirm conditionToMatch and dataField get actions have been dispatched
    expect(dispatch).toHaveBeenCalledTimes(2);

    // Confirm that we load the correct set of form controls and they have the correct value associated with them
    const conditionsArray = component.conditionForm.controls['conditions'] as FormArray;
    const conditionGroup = conditionsArray.at(component.indexInConditionArray) as FormGroup;
    expect(component.dataFieldControl).toEqual(conditionGroup.controls['dataField'] as FormControl);
    expect(component.dataFieldControl.value).toEqual('df2');
    expect(component.matchingConditionControl).toEqual(conditionGroup.controls['conditionToMatch'] as FormControl);
    expect(component.matchingConditionControl.value).toEqual('ctm2');
    expect(component.valueControl).toEqual(conditionGroup.controls['value'] as FormControl);
    expect(component.valueControl.value).toEqual('v2');

    // Confirm the conditionToMatch and dataField selectors were wired up
    expect(pipe).toHaveBeenCalledTimes(2);

    // Confirm the expected value for the 'submitted' flag got emitted
    expect(component.submitted).toBeFalsy();
  });
});
.
.
.
  

client-rules-condition.component.ts

 @Component({
  selector: 'nd-client-rules-condition',
  templateUrl: './client-rules-condition.component.html',
  styleUrls: ['./client-rules-condition.component.scss']
})
export class ClientRulesConditionComponent implements OnInit {

  @Input() conditionForm: FormGroup;
  @Input() indexInConditionArray: number;
  @Input() submitted$: Observable<boolean>;
  @Output() removeCondition: EventEmitter<number> = new EventEmitter();

  conditionToMatchOption$: Observable<ConditionToMatch[]>;

  valueControl: FormControl;
  matchingConditionControl: FormControl;
  dataFieldControl: FormControl;

  private static NPI_DATA_FIELD_GUID: string;

  validValuePattern: string;
  invalidValueError: string;

  // Emits the candidate rule fields that eventually come back from the database.
  //    The 'data fields' combo box in the markup is wired up to this.
  ruleDataFieldOption$: Observable<RuleDataFieldDefinition[]>;

  // Tracks whether the parent form has been submitted.
  submitted: boolean;

  constructor(private _store: Store<state.AppState>) { }

  ngOnInit() {
    this._store.dispatch(new GetConditionToMatchOptions(''));

    // Dispatch Action that causes the rule-data-field options to get retrieved from the server and slammed into the global state.
    this._store.dispatch(new GetRuleDataFieldsOptions(''));

    this.conditionToMatchOption$ = this._store.pipe(select(getConditionToMatchOption));

    // Wire up the selector that cause the rule-data-field options to get pulled from the global state when they are set/updated
    this.ruleDataFieldOption$ = this._store.pipe(select(getRuleDataFieldsSelector));

    // Load GUID for NPI field to use to distinguish between different Data Field dropdown items to apply different validation in Value To Match
    this.ruleDataFieldOption$.subscribe(dataFields => {
      if (ClientRulesConditionComponent.NPI_DATA_FIELD_GUID == null) {
        for (let df of dataFields) {
          if (df.name.toLowerCase().includes('npi') || df.name.toLowerCase().includes('national provider identifier')) {
            ClientRulesConditionComponent.NPI_DATA_FIELD_GUID = df.dataFieldId.toUpperCase();
            break;
          }
        }
      }
    });
    .
    .
    .
  

Я уверен, что это что-то незначительное, и мне просто нужно, чтобы мои модульные тесты прошли.

Комментарии:

1. Слишком много кода, но первое, что появляется, spyOn(mockStore, 'dispatch') вероятно, должно возвращать Observable .

2. Я согласен с @The Head Rush, это довольно много кода, не имеющего отношения к реальной проблеме. Но я не думаю, что это шпион отправки, поскольку это просто вызываемый метод. Но, скорее всего, select spy не возвращает наблюдаемое. И вам нужно знать, что при первом fixture.DetectChanges вызывается ngOnInit. Итак, вы устанавливаете ruleDataField $ с фиксированным значением, а затем вызываете OnInit, и оно заменяется вашим select spy, который возвращает только null.

Ответ №1:

Попробуйте это, в одном из перед каждым declare новым observable .

 component.ruleDataFieldOption$ = new Observable<ConditionToMatch[]>;
  

Я не знаю, применяются ли те же правила к наблюдаемому it self, что и к объекту, но вы можете попробовать.