Angular Jasmine spy throws error randomly in unrelated test

Viewed 285

I'm trying to unit test my Angular component using Jasmine. I have a spec that tests whether an error gets thrown if the TrackService service returns an error (should throw if TrackService can't get track). Whenever I run these unit tests using ng test all tests usually pass, but sometimes the Error! string gets thrown in an unrelated test, for example:

Error thrown in unrelated test

To me, it seems like the trackServiceSpy object isn't properly 'reset' between tests. How do I make sure the trackServiceSpy.getTrack spy doesn't return the error in an unrelated test?

I've also noticed this only happens when the run tests in random order setting is enabled in the Jasmine GUI. Otherwise, the tests work fine.

This is my component:

import {
  ChangeDetectorRef,
  Component,
  ElementRef,
  EventEmitter,
  Input,
  OnInit,
  Output,
  ViewChild,
} from '@angular/core';
import { TrackService } from '../../../services/track/track.service';
import { Track } from '../../../models/Track';
import { HttpErrorResponse } from '@angular/common/http';

@Component({
  selector: 'app-track-list-row',
  templateUrl: './track-list-row.component.html',
  styleUrls: ['./track-list-row.component.scss'],
})
export class TrackListRowComponent implements OnInit {
  @Input() track: Track | undefined;
  @Output() trackChanged = new EventEmitter();
  @Output() trackCreationCancelled = new EventEmitter<Track>();
  editable = false;
  errors: Record<string, string> = {};
  @ViewChild('input') input: ElementRef | undefined;

  constructor(
    private readonly trackService: TrackService,
    private cdRef: ChangeDetectorRef
  ) {}

  ngOnInit(): void {
    if (this.track?.id) {
      this.getTrack();
    } else {
      this.editTrack();
    }
  }

  onEnter(): void {
    if (this.track) {
      this.saveTrack();
    }
  }

  saveTrack(): void {
    if (this.track) {
      if (this.track.id) {
        this.putTrack();
      } else {
        this.postTrack();
      }
    }
  }

  postTrack(): void {
    if (this.track) {
      this.trackService.addTrack(this.track).subscribe(
        () => {
          this.cancelEdit();
          this.trackChanged.emit();
        },
        (errorResponse: HttpErrorResponse) => {
          this.errors = errorResponse.error;
        }
      );
    }
  }

  putTrack(): void {
    if (this.track?.id) {
      this.trackService.putTrack(this.track.id, this.track).subscribe(
        () => {
          this.cancelEdit();
          this.trackChanged.emit();
        },
        (errorResponse: HttpErrorResponse) => {
          this.errors = errorResponse.error;
        }
      );
    }
  }

  cancelSaveTrack(): void {
    if (this.track?.id) {
      this.cancelEdit();
    } else {
      this.trackCreationCancelled.emit();
    }
  }

  editTrack(): void {
    this.editable = true;
    this.cdRef.detectChanges();
    if (this.input) {
      this.input.nativeElement.focus();
    }
  }

  cancelEdit(): void {
    this.editable = false;
    this.getTrack();
  }

  deleteTrack(): void {
    if (this.track?.id && confirm('Are you sure?')) {
      this.trackService.deleteTrack(this.track.id).subscribe(
        () => {
          this.trackChanged.emit();
        },
        (error) => {
          throw `Cannot delete this track. Server response: ${error}`;
        }
      );
    }
  }

  getTrack(): void {
    if (this.track?.id) {
      this.trackService.getTrack(this.track.id).subscribe(
        (track) => {
          this.track = track;
        },
        (error) => {
          throw `Cannot fetch this track. Server response: ${error}`;
        }
      );
    }
  }
}

and here are the related unit tests so far:

import { ComponentFixture, TestBed } from '@angular/core/testing';

import { TrackListRowComponent } from './track-list-row.component';
import { TrackService } from '../../../services/track/track.service';
import { Track } from '../../../models/Track';
import { of, throwError } from 'rxjs';
import { FormsModule } from '@angular/forms';

describe('TrackListRowComponent', () => {
  let component: TrackListRowComponent;
  let fixture: ComponentFixture<TrackListRowComponent>;
  let trackServiceSpy: jasmine.SpyObj<TrackService>;

  beforeEach(() => {
    trackServiceSpy = jasmine.createSpyObj('TrackService', [
      'getTracks',
      'getTrack',
      'addTrack',
      'putTrack',
      'deleteTrack',
    ]);

    TestBed.configureTestingModule({
      declarations: [TrackListRowComponent],
      imports: [FormsModule],
      providers: [{ provide: TrackService, useValue: trackServiceSpy }],
    });
    fixture = TestBed.createComponent(TrackListRowComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    const track: Track = new Track(1);
    component.track = track;

    trackServiceSpy.getTrack.and.returnValue(of(track));
    expect(component).toBeTruthy();
  });

  it('should get track if track id has a value', () => {
    const track: Track = new Track(1);
    component.track = track;

    trackServiceSpy.getTrack.and.returnValue(of(track));

    component.getTrack();

    expect(trackServiceSpy.getTrack).toHaveBeenCalledWith(track.id!);
  });

  it("should throw if TrackService can't get track", () => {
    const track: Track = new Track(1);
    component.track = track;

    trackServiceSpy.getTrack.and.returnValue(throwError('Error!'));
    fixture.detectChanges();

    expect(component.getTrack).toThrow();

    component.getTrack();

    expect(trackServiceSpy.getTrack).toHaveBeenCalledWith(track.id!);
  });

  it('should call addTrack from TrackService if track is posted', () => {
    const track: Track = new Track(1);
    component.track = track;

    trackServiceSpy.getTrack.and.returnValue(of(track));
    trackServiceSpy.addTrack.and.returnValue(of(track));

    component.postTrack();

    expect(trackServiceSpy.getTrack).toHaveBeenCalledTimes(1);
  });
});
0 Answers
Related