Angular unit test calling readonly BehaviorSubject

810 views Asked by At

I have a angular class implemented ControlValueAccessor interface too.

I need to have 100% coverage. Please help me to cover the remain. Line 20, 35, 36 need to be cover. Tried my best seams I have missed somewhere.

Unit test code

import { ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
import { TestSharedModule } from 'src/app/test/test-shared.module';

import { CommentEditorComponent } from './comment-editor.component';

describe('CommentEditorComponent', () => {
  let component: CommentEditorComponent;
  let fixture: ComponentFixture<CommentEditorComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [
        NgbPopoverModule,
        TestSharedModule,
      ],
      declarations: [CommentEditorComponent]
    })
      .compileComponents();
  });

  beforeEach(() => {
    fixture = TestBed.createComponent(CommentEditorComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

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

  it('should call writeValue', fakeAsync(() => {
    const savedValue = spyOn(component.savedValue$, 'next');
    component.writeValue('A');
    expect(savedValue).toHaveBeenCalled();
  }));

  it('should call registerOnChange', () => {
    let isCalledOnChange = false;
    const onChange = () => {
      isCalledOnChange = true;
    };
    component.registerOnChange(onChange);
    expect(isCalledOnChange).toBeFalsy();
  });

  it('should call registerOnTouched', () => {
    let isOnTouched = false;
    const onChange = () => {
      isOnTouched = true;
    };
    component.registerOnTouched(onChange);
    expect(isOnTouched).toBeFalsy();
  });

  it('should call setDisabledState', () => {
    const disable = spyOn(component.ctrl, 'disable');
    component.setDisabledState(true);
    expect(disable).toHaveBeenCalled();
    component.setDisabledState(false);
  });

});

Here is the Component class

import { EventEmitter, forwardRef } from '@angular/core';
import { Component, Output } from '@angular/core';
import {
  ControlValueAccessor,
  FormControl,
  NG_VALUE_ACCESSOR,
  Validators,
} from '@angular/forms';
import { invoke } from 'lodash-es';
import { BehaviorSubject } from 'rxjs';
import { distinctUntilChanged, map, skip } from 'rxjs/operators';

@Component({
  selector: 'app-comment-editor',
  templateUrl: './comment-editor.component.html',
  styleUrls: ['./comment-editor.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CommentEditorComponent),
      multi: true,
    },
  ],
})
export class CommentEditorComponent implements ControlValueAccessor {
  onTouchedFn: () => void;
  readonly savedValue$ = new BehaviorSubject<string>(null);

  readonly ctrl = new FormControl(null, {
    validators: Validators.required,
  });

  constructor() {
    this.savedValue$.pipe(skip(1)).subscribe((value) => {
      this.ctrl.setValue(value);
      this.setDisabledState(Boolean(value));
    });
  }

  writeValue(val: string): void {
    this.savedValue$.next(val);
  }

  registerOnChange(fn: any): void {
    this.savedValue$.pipe(skip(1), distinctUntilChanged()).subscribe(fn);
  }

  registerOnTouched(fn: any): void {
    this.onTouchedFn = fn;
  }

  setDisabledState?(isDisabled: boolean): void {
    invoke(this.ctrl, isDisabled ? 'disable' : 'enable');
  }
}

And here is the coverage result

enter image description here

1

There are 1 answers

2
AliF50 On BEST ANSWER

When you spy on a method, you lose implementation details but gain access to when it was called, how it was called and how many times it was called. To get the best of both world, you have to add .and.callThrough().

Try the following:

it('should call writeValue', fakeAsync(() => {
    // add .and.callThrough() here so next time savedValue$.next() is called,
    // the actual method is called
    const savedValue = spyOn(component.savedValue$, 'next').and.callThrough();
    component.writeValue('A');
    expect(savedValue).toHaveBeenCalled();
  }));