1

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

我的规格文件看起来像这样:

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(); 
     }); 
    }); 
}); 

以上传球,但第二个不,我目前得到一个错误,指出第一个测试“无法读取属性空的‘nativeElement’”。

我的组件的代码如下所示:

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> 
+0

您是否尝试过采用[文档中显示的]设置(https://angular.io/docs/ts/latest/guide/testing.html#!#component-with-external-template)? – jonrsharpe

+0

我试着在beforeEach和测试本身之间来回移动代码,但是在那里推荐什么样的设置?我是否获得了null nativeElement,因为我在测试DOM之前测试了DOM并准备好进行测试?我是否需要模拟服务而不是在提供商中添加真实服务? – bschmitty

+0

好吧,阅读它们 - 他们推荐*两个*'beforeEach'部分,一个'async'不是。看起来像在你当前的设置中'createComponent'在你达到预期之前不会发生。 – jonrsharpe

回答

1

嗯,我看到这里有两件事情,可能是你的问题。只是要清楚,你的错误来到这里:

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

你试着得到de的nativeElement,它是null。让我们假设你已经麻烦了,没有理由不存在 - 你可以理智地检查你自己,并抓住你“知道”应该存在的其他元素,但是我真的认为这里的问题试图获得对某个东西的引用在它存在之前。在这种情况下,您会在尝试获取对nativeElement的引用之后检测到更改。如果您的表按照我认为的方式填充,则需要先检测更改(),然后获取传播到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. 

希望这有助于!

+0

是的,当我移动el.nativElement上方的fixture.detectChanges()时,我遇到的问题是我得到一个“无法读取属性”订阅“null”这是来自我的组件代码在ngOnInit里面,我实际上试图加载我的公司到页面,基本上说,fixture.detectChanges调用ngOnInit这是给我的未定义'stateObservable'的值,然后杀死测试。如果我拿出fixture.detectChanges()并且只是测试一个间谍是否被调用或者任何基本的测试通过都没问题。任何想法为什么'stateObservable'在我的组件将我空? – bschmitty

+0

我在这里添加了一些额外的代码 - 我添加了一个FirmStub并添加了一个overrideComponent来尝试并保持不在组件中,但似乎没有工作..为什么它会在ngOnInit中获取undefined? – bschmitty

+1

对不起,我应该更清楚。我试图说要移动你的变化检测。这可以让您的DOM更改传播,并且您可以获取对这些节点的引用。我会更新我的答案。 – Angelo