编写使用Observables的Angular 2组件的Jasmine测试

时间:2017-06-05 19:22:07

标签: angular unit-testing karma-jasmine angular2-services angular2-directives

我正在尝试测试使用服务调用和可观察调用的角度2组件来获取数据列表。我已将主应用程序模块导入此spec文件。

我的spec文件如下所示:

import { ComponentFixture, TestBed, fakeAsync, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { MaterialModule } from '@angular/material';
import { FormsModule } from '@angular/forms';
import { AppModule } from '../../../src/app/app.module';
import { Observable } from 'rxjs/Observable';
import { Store } from '@ngrx/store';
import { } from 'jasmine';

import { FirmService } from '../../../src/app/containers/dashboard/services/firm.service';
import { FirmListComponent } from '../../../src/app/containers/dashboard/firm-list/firm-list.component';
import { mockFirm1, mockFirm2, mockFirms } from './firm-list.mocks';
import { Firm } from '../../../src/app/containers/dashboard/models/firm.model';
import { FirmState } from '../../../src/app/containers/dashboard/services/firm.state';

describe('Firm List Component', () => {
    let fixture: ComponentFixture<FirmListComponent>;
    let component: FirmListComponent;
    let element: HTMLElement;
    let debugEl: DebugElement;
    let firmService: FirmService;
    let mockHttp;
    let stateObservable: Observable<FirmState>;
    let store: Store<FirmState>;
    let getFirmsSpy;
    let getObservableSpy;

    // utilizes zone.js in order to mkae function execute syncrhonously although it is asynchrounous
    beforeEach(async(() => {
        TestBed.configureTestingModule({
            imports: [MaterialModule, FormsModule, AppModule],
            declarations: [FirmListComponent],
            providers: [FirmService]
        })
            .compileComponents() // compiles the directives template or any external css calls
            .then(() => {
                fixture = TestBed.createComponent(FirmListComponent); // allows us to get change detection, injector
                component = fixture.componentInstance;
                debugEl = fixture.debugElement;
                element = fixture.nativeElement;
                firmService = fixture.debugElement.injector.get(FirmService);

                getObservableSpy = spyOn(firmService, 'stateObservable')
                    .and.returnValue(new FirmState());

                getFirmsSpy = spyOn(firmService, 'getFirms')
                    .and.returnValue(Observable.of(mockFirms));
            });
    }));

    it('should be defined', () => {
        expect(component).toBeDefined();
    });

    describe('initial display', () => {
        it('should not show firms before OnInit', () => {
            debugEl = fixture.debugElement.query(By.css('.animate-repeat'));
            expect(debugEl).toBeNull();
            expect(getObservableSpy.calls.any()).toBe(false, 'ngOnInit not yet called');
            expect(getFirmsSpy.calls.any()).toBe(false, 'getFirms not yet called');
        });

        it('should still not show firms after component initialized', () => {
            fixture.detectChanges();
            debugEl = fixture.debugElement.query(By.css('.animate-repeat'));
            expect(debugEl).toBeNull();
            expect(getFirmsSpy.calls.any()).toBe(true, 'getFirms called');
        });

        it('should show firms after getFirms observable', async(() => {
            fixture.detectChanges();

            fixture.whenStable().then(() => {
                fixture.detectChanges();

                // **I get the correct value here, this is the table headers for the table data below that is showing 0**
                var rowHeaderLength = element.querySelectorAll('th').length;
                expect(rowHeaderLength).toBe(8);

                // **I get 0 for rowDataLength here, test fails**
                var rowDataLength = element.querySelectorAll('.animate-repeat').length;
                console.log(rowDataLength);
            });
        }));

        it('should show the input for searching', () => {
            expect(element.querySelector('input')).toBeDefined();
        });
    });
});

上面的第一个测试通过,但第二个测试没有通过,我目前收到的错误是&#34;无法读取属性&#39; nativeElement&#39; of null&#34;。

我的组件代码如下所示:

 import { NgModule, Component, Input, OnInit, OnChanges } from '@angular/core';
 import { MaterialModule } from '@angular/material';
 import { FlexLayoutModule } from '@angular/flex-layout';
 import { CommonModule } from '@angular/common';
 import { FormsModule } from '@angular/forms';
 import { Firm } from '../models/firm.model';
 import { FirmService } from '../services/firm.service';

 @Component({
   selector: 'firm-list',
   templateUrl: './firm-list.html',
   styles: []
 })

export class FirmListComponent implements OnInit {
   public selectAll: boolean;
   public firms: Array<Firm>;
   public filteredFirms: any;
   public loading: boolean;
   public searchText: string;
   private componetDestroyed = false;

   // @Input() public search: string;

   constructor(public firmService: FirmService) { }

     public ngOnInit() {
       this.firmService.stateObservable.subscribe((state) => {
         this.firms = state.firms;
         this.filteredFirms = this.firms;
       });

      this.getFirms();
   }

    public getFirms(value?: string) {
      this.loading = true;
      this.firmService.getFirms(value).subscribe((response: any) => {
         this.loading = false;
      });
    }
}

 @NgModule({
   declarations: [FirmListComponent],
   exports: [FirmListComponent],
   providers: [FirmService],
   imports: [
       MaterialModule,
       FlexLayoutModule,
       CommonModule,
       FormsModule
     ]
 })

export class FirmListModule { }

我不确定我是否在我的规范文件中遗漏了一些代码以说明可观察的内容,或者我是否遗漏了其他内容?任何帮助表示赞赏。

公司服务

import { Observable } from 'rxjs/Rx';
import { Injectable } from '@angular/core';
import { AuthHttp } from 'angular2-jwt';
import { Response } from '@angular/http';
import { Store } from '@ngrx/store';
import { firmActions } from './firm.reducer';
import { FirmState } from './firm.state';

@Injectable()
export class FirmService {
   public stateObservable: Observable<FirmState>;

   constructor(private $http: AuthHttp, private store: Store<FirmState>) {
    // whatever reducer is selected from the store (in line below) is what the "this.store" refers to in our functions below.
    // it calls that specific reducer function
    // how do I define this line in my unit tests?
      this.stateObservable = this.store.select('firmReducer');
  }

public getFirms(value?: string) {
    return this.$http.get('/api/firm').map((response: Response) => {
        this.store.dispatch({
            type: firmActions.GET_FIRMS,
            payload: response.json()
        });
        return;
    });
}

public firmSelected(firms) {
    // takes in an action, all below are actions - type and payload
    // dispatches to the reducer
    this.store.dispatch({
        type: firmActions.UPDATE_FIRMS,
        payload: firms
    });
}

public firmDeleted(firms) {
    this.store.dispatch({
        type: firmActions.DELETE_FIRMS,
        payload: firms
    });
  }
}

我的公司组件html模板:

<md-card class="padding-none margin">
  <div class="toolbar" fxLayout="row" fxLayoutAlign="start center">
    <div fxFlex class="padding-lr">
      <div *ngIf="anySelected()">
        <button color="warn" md-raised-button (click)="deleteSelected()">Delete</button>
      </div>
      <div *ngIf="!anySelected()">
        <md-input-container floatPlaceholder="never">
          <input mdInput [(ngModel)]="searchText" (ngModelChange)="onChange($event)" type="text" placeholder="Search" />
        </md-input-container>
      </div>
    </div>
    <div class="label-list" fxFlex fxLayoutAlign="end center">
      <label class="label bg-purple600"></label>
      <span>EDF Model</span>
      <label class="label bg-green600"></label>
      <span>EDF QO</span>
      <label class="label bg-pink800"></label>
      <span>LGD Model</span>
      <label class="label bg-orange300"></label>
      <span>LGD QO</span>
    </div>
  </div>
  <md-card-content>
    <div class="loading-container" fxLayoutAlign="center center" *ngIf="loading">
      <md-spinner></md-spinner>
    </div>
    <div *ngIf="!loading">
      <table class="table">
        <thead>
          <tr>
            <th class="checkbox-col">
              <md-checkbox [(ngModel)]="selectAll" (click)="selectAllChanged()" aria-label="Select All"></md-checkbox>
            </th>
            <th>
              Firm Name
            </th>
            <th>
              Country
            </th>
            <th>
              Industry
            </th>
            <th>
              EDF
            </th>
            <th>
              LGD
            </th>
            <th>
              Modified
            </th>
            <th>
              Modified By
            </th>
          </tr>
        </thead>
        <tbody>
          <tr *ngFor="let firm of filteredFirms; let i = index" class="animate-repeat" [ngClass]="{'active': firm.selected}">
            <td class="checkbox-col">
              <md-checkbox [(ngModel)]="firm.selected" aria-label="firm.name" (change)="selectFirm(i)"></md-checkbox>
            </td>
            <td>{{firm.name}}</td>
            <td>{{firm.country}}</td>
            <td>{{firm.industry}}</td>
            <td>
              <span class="label bg-purple600">US 4.0</span>
              <span class="label bg-green600">US 4.0</span>
            </td>
            <td>
              <span class="label bg-pink800">US 4.0</span>
              <span class="label bg-orange300">US 4.0</span>
            </td>
            <td>{{firm.modifiedOn}}</td>
            <td>{{firm.modifiedBy}}</td>
          </tr>
        </tbody>
      </table>
    </div>
  </md-card-content>
</md-card>

1 个答案:

答案 0 :(得分:1)

嗯,我看到这里有几件事可能是你的问题。为了清楚起见,你的错误就在这里:

de = fixture.debugElement.query(By.css('table'));

您尝试获取de的nativeElement,它为null。让我们假设你已经麻烦了,并且它没有理由不存在 - 你可以理智地检查自己并抓住你“知道”应该存在的其他元素,但我真的认为这里的问题是试图抓住对某事的引用在它存在之前。在这种情况下,您在尝试获取对nativeElement的引用后检测更改。如果你的表以我认为的方式填充,你需要首先检测changes(),然后获取对传播到DOM的内容的引用。确定你的ngOnInit还没有发生 - 当TestBed创建组件夹具时它不会触发,它发生在第一个detectChanges()上。

试试这个:

it('should have table headers', () => {
        fixture.detectChanges();
        de = fixture.debugElement.query(By.css('table'));
        el = de.nativeElement;        
        expect(el.textContent).toEqual('Firm Name');
    });

它更进一步 - 很多时候使用表格或任何使用角度动画的东西要求你导入BrowserAnimationsModule或NoopAnimationsModule。由于这是一个单元测试,我只需导入NoopAnimationsModule然后获取您的引用并根据需要执行测试。

好的,在你指出你在ngOnInit上的错误后,我看到你的问题是什么。

因此,此单元测试并非用于测试该服务。在这种思想中,你有几个选择。用间谍拦截对服务的调用,但由于它是属性,你必须使用spyOnProperty。或者,您可以使用您提供的存根。回顾你原来的帖子,我认为这就是你想要做的。我认为如果你这样改变它可能会有效:

beforeEach(async(() => {
     TestBed.configureTestingModule({
        imports: [MaterialModule, FormsModule, AppModule],
        declarations: [FirmListComponent],
        providers: [{provide: FirmService, useClass: FirmStub}]
    })
        .compileComponents()
        .then(() => {
            fixture = TestBed.createComponent(FirmListComponent);
            component = fixture.componentInstance;
            firmStub = fixture.debugElement.injector.get(FirmService);
        });
}));

在那个注释中,您还需要在FirmStub上提供一个stateObservable属性,因为它正在ngOninit中访问。你可以直截了当地说出来。

class FirmStub {
   public stateObservable: Observable<FirmState> = new Observable<FirmState>();
   public getFirms(value?: string): Observable<any> {
    return Observable.of(mockFirms);
    }
}

没有html文件,我不确定你是否确实需要以某种方式填充该属性以测试模板,但如果没有,那该存根应该可以工作。如果您确实需要它,请让FirmStub提供更强大的属性。

你也可以通过将其添加到测试中来一起拦截ngOnInit:

spyOn(component, 'ngOnInit');// this will basically stop anything from ngOnInit from actually running. 

希望这有帮助!