在 Angular2 TestBed 配置方面需要帮助。 DecoratorFactory 错误

Need assistance with Angular2 TestBed Configuration. DecoratorFactory errors

我正在为名为 'DashboardComponent' 的组件创建测试。事实证明很难创建测试规范。 Karma 抛出错误 "Unexpected value 'DecoratorFactory' imported by the module 'DynamicTestModule'".

我曾尝试删除代码直到错误消失,然后重新添加代码直到错误再次出现以确定来源。但是,这并没有带来丰硕的成果,因为代码在使用几种不同的配置时会中断,而且我无法判断哪个配置导致错误存在,哪个导致错误被抛出。我怀疑在我所有未失败的尝试中都存在错误的配置,并且某些代码行(例如添加 it() 调用)导致错误在之前不可见时突然出现。

希望比我更有经验的人看到我的错误并提供一些建议。其他人在 Whosebug 上遇到了同样的问题,适用于他们的解决方案似乎不适用于这种情况。

代码图

为了使代码库更容易理解,我创建了一个 UML 图来显示我试图在 TestBed 配置中复制的依赖关系。

绿色元素是被测试的元素。包元素被导入到模块中,每个服务都有一个 mock class,我们在它们的位置提供 mock classes。我们必须声明三个组件,因为DashboardComponent的模板引用了PatientListComponent,而PatientListComponent的模板又依赖于PatientListItemComponent

代码和文件

我希望不要在这个问题上写小说,所以如果需要的话,我会添加更多的文件和代码。在他们被要求之前,我将提供那些看起来与确定问题最相关的文件。

app/dashboard/dashboard.component.spec.ts

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

import { DashboardComponent }        from './dashboard.component';
import { PatientListComponent }      from '../patient-list/patient-list.component';
import { PatientListItemComponent }  from '../patient-list-item/patient-list-item.component';
import { AuthenticationService }     from '../authentication.service';
import { PatientSummaryService }     from '../patient-summary/patient-summary.service';

import { MockAuthService, MockPatientSummaryService, RouterStub } from './dashboard.mocks';

/*
 * DebugElement and By are currently not used. I have left them in the import statements above,
 * because any test suite WILL use them when it is fully developed.
 */
describe( "DashboardComponent", ()=>{
    var component : DashboardComponent                   = null;
    var fixture   : ComponentFixture<DashboardComponent> = null;
    beforeEach( 
        async(  
            ()=>{
                TestBed.configureTestingModule(
                    {
                    imports: [ NgModule, RouterModule, FormsModule ],
                    declarations: [ DashboardComponent, PatientListComponent, PatientListItemComponent ],
                    providers: [
                        { provide: AuthenticationService, useClass: MockAuthService },
                        { provide: PatientSummaryService, useClass: MockPatientSummaryService },
                        { provide: Router, useClass: RouterStub }
                               ]
                    }
                ).compileComponents();
    } ));
    beforeEach(()=>{
        fixture = TestBed.createComponent( DashboardComponent );
    });
    it( "has a test", ()=>{ expect(1).toBe(1);});
    /*
    describe( "filter section behavior", ()=>{} );
    describe( "list display behavior", async( ()=>{
        describe( "filtered-list behavior", async(()=>{
            //component.filterList = true;
            fixture.detectChanges();
            fixture.whenStable().then((done:any)=>{
                debugger;
            });
        }) );
        describe( "unfiltered-list behavior", ()=>{
            //component.filterList = false;
        } );
    }) );
    */
} );

app/dashboard/dashboard.mocks.ts

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

import { DashboardComponent }        from './dashboard.component';
import { PatientListComponent }      from '../patient-list/patient-list.component';
import { PatientListItemComponent }  from '../patient-list-item/patient-list-item.component';
import { AuthenticationService }     from '../authentication.service';
import { PatientSummaryService }     from '../patient-summary/patient-summary.service';

import { MockAuthService, MockPatientSummaryService, RouterStub } from './dashboard.mocks';

/*
 * DebugElement and By are currently not used. I have left them in the import statements above,
 * because any test suite WILL use them when it is fully developed.
 */
describe( "DashboardComponent", ()=>{
    var component : DashboardComponent                   = null;
    var fixture   : ComponentFixture<DashboardComponent> = null;
    beforeEach( 
        async(  
            ()=>{
                TestBed.configureTestingModule(
                    {
                    imports: [ NgModule, RouterModule, FormsModule ],
                    declarations: [ DashboardComponent, PatientListComponent, PatientListItemComponent ],
                    providers: [
                        { provide: AuthenticationService, useClass: MockAuthService },
                        { provide: PatientSummaryService, useClass: MockPatientSummaryService },
                        { provide: Router, useClass: RouterStub }
                               ]
                    }
                ).compileComponents();
    } ));
    beforeEach(()=>{
        fixture = TestBed.createComponent( DashboardComponent );
    });
    it( "has a test", ()=>{ expect(1).toBe(1);});
    /*
    describe( "filter section behavior", ()=>{} );
    describe( "list display behavior", async( ()=>{
        describe( "filtered-list behavior", async(()=>{
            //component.filterList = true;
            fixture.detectChanges();
            fixture.whenStable().then((done:any)=>{
                debugger;
            });
        }) );
        describe( "unfiltered-list behavior", ()=>{
            //component.filterList = false;
        } );
    }) );
    */
} );

app/dashboard/dashboard.组件

import { Component, Input }        from '@angular/core';
import { Router }                  from '@angular/router';

import { Authentication }          from '../authentication';
import { AuthenticationService }   from '../authentication.service';
import { PatientSummaryService }   from '../patient-summary/patient-summary.service';
import { PatientSummary }          from '../patient-summary/patient-summary';

@Component(
    {
        moduleId: module.id,
        selector: 'dashboard',
        template: `
        <div class="container" *ngIf="credentials.valid">
            <div class="col-xs-12 filterOptions">
                <span class="col-xs-12">
                    <button class="btn btn-small btn-default pull-right"  (click)="toggleFilterView()">Toggle Filters</button>
                    <h4>Filter Options</h4>
                </span>
                <span *ngIf="viewFilters">
                    <label>
                    <input type='checkbox' [(ngModel)]="filterList" />
                    Filter the list for <strong>only</strong> patients linked to your account.
                    </label>
                    <div class="form-group">
                        <label>Filter By Patient Name</label>
                        <input class="form-control" [(ngModel)]="nameFilter" placeholder="Patient name in full or in part." />
                    </div>
                </span>
            </div>
            <h1>Priority Patients</h1>
            <patient-list [sourceData]="todaysPatientList | staffFilter : acceptableStaff" (clickPatient)="selectPatient($event)"></patient-list>
            <h1>Patients Records <small>(Not Yet Complete)</small></h1>
            <patient-list [sourceData]="nonActivePatientList | staffFilter : acceptableStaff" (clickPatient)="selectPatient($event)"></patient-list>
        </div>`,
        styles: [
                `.filterOptions {
                    background-color: hsla( 187, 55%, 90%, 0.5 );
                    padding: 1em;
                    border: solid 3px black;
                    border-radius: 1em;
                    margin-bottom: 1em;
              }`
            ]
    }
)
export class DashboardComponent {
    credentials : Authentication = new Authentication(null,null,null);
    viewFilters: boolean = false;
    nameFilter: string = "";
    filterList: boolean = true;
    patientSummary: PatientSummary[];

    constructor( private patientSummaryService : PatientSummaryService, 
    private authService : AuthenticationService, 
    private router : Router ){}
    ngOnInit(){
        var app = this;
        this.patientSummaryService.updatedList.subscribe(
            (list : PatientSummary[] ) => {app.setPatientSummaryList(list);}
        );
        this.authService.newCreds.subscribe(
            (creds : Authentication) => this.credentials = creds
        );
        this.authService.invalidate.subscribe(
            (obj : any) => this.credentials = new Authentication(null,null,null)
        );
    }
    setPatientSummaryList(list: PatientSummary[]) {
        var app = this;
        list.sort((a: PatientSummary, b: PatientSummary) => {
            var dateA = app.extractDate(a);
            var dateB = app.extractDate(b);
            if (dateA > dateB) return 1;
            if (dateA < dateB) return -1;
            return 0;
        });
        this.patientSummary = list;
    }
    extractDate(item: PatientSummary) {
        var date = item.arrivalTime;
        if (date === null || date < item.visit.date) {
            date = item.visit.date;
        }
        return date;
    }
    nameFilterFunction(item: PatientSummary) {
        if (this.nameFilter == "") return true;
        if (typeof item == "object" && typeof item.name != "undefined") {
            var index = item.name.indexOf(this.nameFilter);
            return (index !== -1);
        }
        return false;
    }
    toggleFilterView() {
        this.viewFilters = !this.viewFilters;
    }


    /**
     * Returns a list of patients in ascending order (oldest first) of items 
     * that are today and are assigned to a room.
     */
    get todaysPatientList() {
        var app = this;
        if (!Array.isArray(this.patientSummary)) return [];
        var list = this.patientSummary.filter(
            (item: PatientSummary) => {
                var date = app.extractDate(item);
                var now = new Date();
                var today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
                var tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1);
                return date >= today && date <= tomorrow;
            }).filter((item: PatientSummary) => {
                if (typeof item == "object" && typeof item.location == "object" && typeof item.location.room !== null) {
                    return item.location.room != "No Room Assignment";
                } else {
                    return true;
                }
            });
        return list.filter((item) => {return app.nameFilterFunction(item);});
    }
    /**
     * Returns a list of patients in descending order (most recent first) of items 
     * that do not appear in the todaysPatientList attribute;
     */
    get nonActivePatientList() {
        if (!Array.isArray(this.patientSummary)) return [];
        var app = this;
        var list = this.todaysPatientList;
        var nonActiveList = this.patientSummary.filter((obj: PatientSummary) => {
            var index = list.indexOf(obj);
            return (index == -1);
        });
        nonActiveList.reverse();
        return nonActiveList.filter((item) => {return app.nameFilterFunction(item);});;
    }
    get acceptableStaff() {
        if (!this.filterList) {
            return "any";
        } else {
            var user = "any";
            if (this.credentials instanceof Authentication) {
                user = this.credentials.username;
            }
            if (user === null) user = "any";
            return user;
        }
    };
    selectPatient( patient : PatientSummary ){
        var id = patient.medfaceId;
        this.router.navigate(['/detail',id]);
    }
}

更新:2017 年 2 月 9 日 3:10pmPCT

我认为问题出在我的配置或代码库中。出于这个原因,我今天尝试使用 Spies 而不是 MockClasses。通过使用 Spies,我希望消除因供应商有问题而造成的复杂情况。在浏览器中测试时系统加载,所以我知道我创建的常规提供程序运行良好并且没有加载错误。

使用 Spies 没有解决问题。我仍然收到 DecoratorFactory 错误。我将继续尝试新的解决方案,直到找到答案。感谢任何帮助。

看来问题很可能是这条线引起的。即使不是,也可能是错误

imports: [ NgModule, RouterModule, FormsModule ],

请注意 NgModule 而不是 一个 Angular 2 模块而是一个 returns 一个已配置的装饰器,它又将目标 class 表示为 Angular 2 模块。将其作为 Angular 2 模块导入可能会引发此错误。