angular 2 тестирует компонент с помощью forkJoin

#unit-testing #angular

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

Вопрос:

В моем приложении есть LogComponent. Внутри ngOnInit я извлекаю журналы с сервера через LogService и отображаю их в виде сетки.

Вот 2 способа, как это сделать:

1-й:

 import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';

import { Log } from './log';
import { LogService } from './log.service';
import * as globals from '../../globals';
import { DataTransformService, HttpHandlerService, StateManagerService } from '../../shared';

@Component({
  selector: 'app-log',
  templateUrl: 'log.component.html',
  styleUrls: ['log.component.css'],
  providers: [ DataTransformService, HttpHandlerService, LogService, StateManagerService]
})
export class LogComponent implements OnInit {

  localGlobals: any = globals;

  logs: Log[];

  currentPage: number;
  totalRecords: number = 0;
  firstRowIndex: number = 0;

  constructor(
    private stateManagerService: StateManagerService,
    private httpHandlerService: HttpHandlerService,
    private logService: LogService,
    private dataTransformService: DataTransformService,
    private router: Router
  ) {}

  ngOnInit() {

    //get page number from local storage
    this.currentPage = this.stateManagerService.getParamFromState(this.router.url, 'page');
    this.firstRowIndex = this.currentPage * this.localGlobals.ROWS_PER_PAGE - this.localGlobals.ROWS_PER_PAGE;

    //get total count 
    let respHandler = (res: any) => {
      this.totalRecords = res.headers.get('X-Pagination-Total-Count');
      return this.httpHandlerService.extractData(res);
    };

    this.logService.search(this.currentPage, respHandler).subscribe(
      logs => {
        this.logs = logs;
      },
      err => console.error(err) 
    );

  }

}
  

Первый подход использует эту функцию для извлечения журналов:

 this.logService.search(this.currentPage, respHandler).subscribe(
  logs => {
    this.logs = logs;
  },
  err => console.error(err) 
);
  

Второй способ получения журналов — через Observable.forkJoin:

 import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';

import { Log } from './log';
import { LogService } from './log.service';
import * as globals from '../../globals';
import { DataTransformService, HttpHandlerService, StateManagerService } from '../../shared';

@Component({
  selector: 'app-log',
  templateUrl: 'log.component.html',
  styleUrls: ['log.component.css'],
  providers: [ DataTransformService, HttpHandlerService, LogService, StateManagerService]
})
export class LogComponent implements OnInit {

  localGlobals: any = globals;

  logs: Log[];

  currentPage: number;
  totalRecords: number = 0;
  firstRowIndex: number = 0;

  constructor(
    private stateManagerService: StateManagerService,
    private httpHandlerService: HttpHandlerService,
    private logService: LogService,
    private dataTransformService: DataTransformService,
    private router: Router
  ) {}

  ngOnInit() {

    //get page number from local storage
    this.currentPage = this.stateManagerService.getParamFromState(this.router.url, 'page');
    this.firstRowIndex = this.currentPage * this.localGlobals.ROWS_PER_PAGE - this.localGlobals.ROWS_PER_PAGE;

    //get total count 
    let respHandler = (res: any) => {
      this.totalRecords = res.headers.get('X-Pagination-Total-Count');
      return this.httpHandlerService.extractData(res);
    };

    Observable.forkJoin(
        this.logService.search(this.currentPage, respHandler)
    ).subscribe(
      data => {
        this.logs = data[0];
      },
      err => console.error(err) 
    );

  } 

}
  

Второй подход использует эту функцию для извлечения журналов:

 Observable.forkJoin(
        this.logService.search(this.currentPage, respHandler)
    ).subscribe(
      data => {
        this.logs = data[0];
      },
      err => console.error(err) 
    );
  

У меня есть тест для этого компонента, который проверяет, инициализируются ли свойства после инициализации:

 import { DebugElement, NO_ERRORS_SCHEMA }    from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
import { ReplaySubject } from 'rxjs/Rx';

import { Log } from './log';
import { LogComponent } from './log.component';
import { LogService } from './log.service';
import { DataTransformService, HttpHandlerService, LocalStorageService, StateManagerService } from '../../shared';

let comp:    LogComponent;
let fixture: ComponentFixture<LogComponent>;
let de:      DebugElement;

describe('LogComponent', () => {

  // async compile html and css
  beforeEach(async(() => {

    let project = new ReplaySubject(1);
    let logServiceStub = {
        search: (pageNum: number, respHandler: any) => {
            let logs = [
                { id: 1, ip: '127.0.0.1', user_id: 1, notification_event_id: 1, created_at: 1 },
                { id: 2, ip: '127.0.0.2', user_id: 2, notification_event_id: 2, created_at: 2 }
            ];
            project.next(logs);
            return project;
        }
    };

    let routerStub = {
        url: 'log_url'
    };

    TestBed.configureTestingModule({
        declarations: [ LogComponent ],
        imports: [ FormsModule ],
        providers: [
            DataTransformService,
            HttpHandlerService,
            LocalStorageService,
            { provide: Router, useValue: routerStub },
            StateManagerService
        ],
        schemas: [ NO_ERRORS_SCHEMA ]
    })
    .overrideComponent(LogComponent, {
      set: {
        providers: [
          { provide: LogService, useValue: logServiceStub }
        ]
      }
    })
    .compileComponents();

  }));

  // synchronous 
  beforeEach(() => {

    fixture = TestBed.createComponent(LogComponent);

    // LogComponent test instance
    comp = fixture.componentInstance;

    de = fixture.debugElement;

  });

  it('initial variables are set after init', () => {

      fixture.detectChanges(); 

      console.log(comp.logs);

      //TODO: test not working when requests are in forkJoin
      expect(comp.currentPage).toBe(1);
      expect(comp.firstRowIndex).toBe(0);
      expect(comp.logs).not.toBeUndefined();
      expect(comp.logs.length).toBe(2);

  });

});
  

Проблема: когда я запускаю тест с использованием 1-го подхода (без Observable.forkJoin) работает нормально. НО когда я запускаю тест, используя 2-й подход (с наблюдаемым.forkJoin) завершается ошибкой:

Ожидаемый неопределенный не должен быть неопределенным.

Таким образом, журналы не инициализируются после fixture.DetectChanges() . Похоже, что запрос находится внутри наблюдаемого.forkJoin() тогда инициализация не выполняется. Я понимаю, что в Observable есть только 1 запрос.forkJoin() и я могу использовать 1-й подход, но у меня есть другие компоненты в системе, которые используют более 1 запроса, поэтому мне нужно решить эту проблему.

Надеюсь, вы поняли, что я имею в виду.

Заранее спасибо.

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

1. Я также сталкиваюсь с этой проблемой. Вы нашли какое-либо решение для этого? @ryzhak

2. Я также ищу ответ на этот вопрос

3. @ryzhak не могли бы вы принять мой ответ ниже?

Ответ №1:

Хорошо, ребята, после некоторых усилий я обнаружил, что observable необходимо завершить forkJoin , чтобы его перехватить.
Поэтому, если вы вызываете
project.next(something)
этого недостаточно.
Вам также необходимо вызвать:
project.complete()
чтобы заставить его работать!

Имейте в виду, что вы всегда можете использовать zip вместо forkJoin для обработки .next распространения, а также .complete

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

1. Это тоже решило мою проблему, хотя я должен добавить небольшое замечание: complete следует вызывать сразу после next для каждого предмета, иначе это не сработает.

Ответ №2:

У меня была аналогичная проблема, и ее удалось решить после включения утверждения в функцию setTimeout.

 setTimeout(() => { expect(mockService.functionName).toHaveBeenCalledTimes(1); }, 10);