Search code examples
javascriptangulartestingjasmine

Testing a void async method in Angular, the spec is not waiting for the async calls to complete, which is making the test always fail


I have a method called loadIssues() in an Angular component called BurndownComponent that is void and inserts values into an array. This method uses some async methods from a service and from the component itself. After running those asyncs it fills in the array.

I want to write test code for loadIssues(), but I do not know how to make the test wait for the array to be filled. Because loadIssues is void I cannot use async await. I also tried using a setTimeout but it ran asynchronously and did not wait for the execution of loadIssues().

Does anyone have an idea on how I could write such a test?

The relevant codes are below:

loadIssues():

loadIssues(): void {
    this.selectedMilestone = this.milestone;
    this.issues = [];
    console.log(this.selectedMilestone.reviewDate);

    this.gitlabService.getIssues(this.selectedMilestone?.title).then(issues => {
      let allIssues = issues.filter(i => [RequirementLabel.StoryTask, RequirementLabel.Task, RequirementLabel.Story].includes(i.requirement));
      this.getIssueEvents(allIssues).then(issues => {
        allIssues = issues;
        console.log('allIssues ', allIssues.length);

        // issues could be moved out of the milestone towards the end of it
        // we consider a limit of 3 days before the review meeting date
        if (new Date().getTime() >= this.selectedMilestone.reviewDate.getTime() - (3 * MILLISECONDS_PER_DAY)) {
          this.getMilestoneRolledIssues().then(rolledIssues => {
            const issuesIds = allIssues.map(i => i.id);
            console.log(issuesIds);
            allIssues = allIssues.concat(...rolledIssues.filter(i => !issuesIds.includes(i.id))); // removes duplicated issues
            this.gitlabService.getDiscussions(allIssues).then(discussions => {
              allIssues.forEach((issue, index) => issue.discussions = discussions[index]);
              this.issues = allIssues;
            });
          });
        }
        else {
          this.gitlabService.getDiscussions(allIssues).then(discussions => {
            allIssues.forEach((issue, index) => issue.discussions = discussions[index]);
            this.issues = allIssues;  
          });
        }
      });
    });

Test attempt (BurndownComponent.spec.ts):


describe('BurndownComponent', () => {
  let component: BurndownComponent;
  let fixture: ComponentFixture<BurndownComponent>;
  const data: object = jsonData;
  let httpMock: object;
  let stubGitLabService: GitlabService;

  beforeEach(async () => { 
    httpMock = {
      'get': (url, headers): Observable<object[]> => {
        const endpoint = 'https://git.tecgraf.puc-rio.br/api/v4/';
        const discussions = data['discussions-test'][0]['discussions']
          .map(d => Discussion.getDiscussion(d));
        const urlDiscussions = [
          `${endpoint}projects/1710/issues/120/discussions`,
          `${endpoint}projects/1710/issues/97/discussions`,
          `${endpoint}projects/1210/issues/920/discussions`
        ];
        if(urlDiscussions.includes(url)) {
          return new Observable(subscriber => discussions[urlDiscussions.indexOf(url)]);
        }
        return new Observable(subscriber => null);
      }
    }
    stubGitLabService = new GitlabService(<any> httpMock);
    await TestBed.configureTestingModule({
      declarations: [ BurndownComponent ],
      providers: [
        { provide: GitlabService, useValue: stubGitLabService }
      ]
    })
    .compileComponents();
  });

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

it('loads all issues - loadIssues()', async () => {
    const milestoneData = Milestone.getMilestone(data['milestone-test'][0]);
    const milestoneEventsData = data['all-milestone-events'][0]['events']
    .map(me => MilestoneEvent.getMilestoneEvent(me));
    const labelEventsData = data['label-events-burndown-component-test'][0]['events']
    .map(le => LabelEvent.getLabelEvent(le));
    const issues = data['issues-test-passed-milestone']
    .map(i => Issue.getIssue(i));
    const discussions = data['discussions-test'][0]['discussions']
    .map(d => Discussion.getDiscussion(d));
    issues.forEach((issue, index) => {
      issue.labelEvents = labelEventsData.map(le => LabelEvent.copy(le));
      issue.milestoneEvents = milestoneEventsData.map(me => MilestoneEvent.copy(me));
    });
    component.milestone = milestoneData;
    stubGitLabService['getDiscussions'] = (issues: Issue[]): Promise<Discussion[][]> => {
      return new Promise<Discussion[][]>(resolve => resolve(discussions))
    };
    const spyMilestoneRolledIssues = spyOn(component, 'getMilestoneRolledIssues')
    .and
    .returnValue(Promise.resolve(issues));
    const spyIssueEvents = spyOn(component, 'getIssueEvents')
    .and
    .returnValue(Promise.resolve(issues));
    const getDiscussionsSpy = spyOn(stubGitLabService, 'getDiscussions')
    .and
    .returnValue(new Promise(
      resolve => {
        console.log('discussions');
        resolve(discussions)
      }
    ));
    await component.loadIssues();
    expect(component.issues.length).toBe(3);
    expect(spyMilestoneRolledIssues).toHaveBeenCalled();
    expect(getDiscussionsSpy).toHaveBeenCalled();
  });

Solution

  • You are awaiting loadIssues but it is not an async function. You can address this by returning the Promise returned by then which will complete at an appropriate time.

    loadIssues() {   // remove explicit void return type
        this.selectedMilestone = this.milestone;
        this.issues = [];
        console.log(this.selectedMilestone.reviewDate);
    
        // return the promise
        return this.gitlabService.getIssues(this.selectedMilestone?.title).then(issues => {
       ...