【问题标题】:angular2 unit test: cannot read property of componentInstance.method() undefinedangular2单元测试:无法读取componentInstance.method()的属性未定义
【发布时间】:2017-10-13 20:47:11
【问题描述】:

mycomponent.spec.ts 类:

这会引发错误:无法读取未定义的属性“ngOnInit”。

let myComponent: MyComponent;
let myService: MyService;

describe('myComponent', () => {
   beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [MyComponent],
    providers: [
      {provide: MyService, useClass: MockMyService} // **--passing Mock service**
  ]
}).compileComponents()
  .then(() => {
    myComponent = TestBed.createComponent(MyComponent).componentInstance;
    myService = TestBed.get(MyService);
    console.log(myService.getData());
  });
});

it('should get the mock data', () => {
   myComponent.ngOnInit(); //-----------> seems like myComponent is not defined at this place, how to resolve this error 
   expect(myComponent.data).toBe(DATA_OBJECT);
  });
});

下面是我的组件:

@Component({
  selector: 'pm-catalogs',
  templateUrl: './catalog-list.component.html'
})

export class MyComponent implements OnInit {

 public data: IData[];

 constructor(private _myService: MyService) {

}

public ngOnInit(): void {
this._myService.getData()
  .subscribe(
    data => this.data = data
  //  error => this.errorMessage = <any>error
  );
 }
}

下面是模拟服务

  export const DATA_OBJECT: IData[] = [
 {
   'Id': 1,
   'value': 'abc'
 },
 {
'Id': 2,
'value': 'xyz'
 }];

@Injectable()
export class MockMyService {
 public getData(): Observable<IData[]> {
    return Observable.of(DATA_OBJECT);
  }
}

我是 Angular2 测试的新手,我希望 myService.getData 在 myComponent.ngOnInit() 在我的规范类中调用 myService.getData() 方法时返回 DATA_OBJECT 请帮助我实现这一目标。

【问题讨论】:

  • describe 块中的代码缩进严重,不清楚那里发生了什么。

标签: angular unit-testing typescript jasmine karma-jasmine


【解决方案1】:

以防万一有人认为接受的答案没有帮助,当NgZone 未正确模拟时,此错误也可能会出现。我不了解底层机制,但 a 之前提供了模拟的 NgZone 作为对象文字,如下所示:

TestBed.configureTestingModule({
    providers: [{
        provide: NgZone,
        useValue: {
            runOutsideAngular: (fn: (...args: Array<any>) => T) => { return fn(); }
            ... other NgZone functions ...
        }
    }],
    declarations: [MyComponent]
})

由于某种原因,这在某些测试中有效,所以一开始我并没有怀疑它,但过了一段时间我创建了一个模拟的 NgZone 类,它扩展了实际的 NgZone

export class NgZoneMock extends NgZone {
    constructor() {
        super({ enableLongStackTrace: false });
    }

    public runOutsideAngular<T>(fn: (...args: Array<any>) => T) { return fn(); }
    public run<T>(fn: (...args: Array<any>) => T, applyThis?: any, applyArgs?: Array<any> | undefined) { return fn(); }
    public runTask<T>(fn: (...args: Array<any>) => T, applyThis?: any, applyArgs?: Array<any> | undefined) { return fn(); }
    public runGuarded<T>(fn: (...args: Array<any>) => T, applyThis?: any, applyArgs?: Array<any> | undefined) { return fn(); }
    public onUnstable = new EventEmitter<any>();
    public onStable = new EventEmitter<any>();
    public onMicrotaskEmpty = new EventEmitter<any>();
    public onError = new EventEmitter<any>();
}

然后就是TestBed配置中的类:

TestBed.configureTestingModule({
    providers: [{
        provide: NgZone,
        useClass: NgZoneMock
    }],
    declarations: [MyComponent]
})

值得一提的是,还有其他方法可以做到这一点(并且通常用于模拟任何服务)。这里有几个例子Running jasmine tests for a component with NgZone dependency。创建 Jasmine Spy 对象非常有用,但我个人更喜欢将模拟放在 DRY 实际服务文件旁边的单独文件中。当然你也可以把 Spy Object 放到 mock 文件中。

【讨论】:

    【解决方案2】:

    问题是异步beforeEach 实现不正确,这会导致竞争条件。

    beforeEach 块中执行.compileComponents().then(() =&gt; { ... }) 会导致then 回调中的代码执行延迟至少一滴。 it 块在有机会被分配之前从不等待和访问 myComponent 变量。

    当测试没有失败时,这种竞争条件会变得不那么明显并且更加危险。相反,当先前测试中的beforeEach 影响当前测试中的变量时,测试可能会被交叉污染。

    .compileComponents() 是同步的,除非有带有styleUrlstemplateUrl 的组件(如上例所示)。在这种情况下它变成异步的,应该使用async helper:

    // asynchronous block
    beforeEach(async(() => {    
      TestBed.configureTestingModule({ ... })
      .compileComponents();
    }));
    
    // synchronous block
    beforeEach(() => {    
      myComponent = ...
    });
    

    根据经验,如果块有可能是异步的,则应使用 asyncfakeAsync 助手包裹块。

    当使用TestBed 测试组件类时,它们遵循一个生命周期并且它们的钩子被自动调用。不需要手动调用ngOnInit()(正如另一个答案所解释的那样),这将导致调用钩子两次。

    【讨论】:

      【解决方案3】:

      您不必手动调用 ngOnInit() 来运行组件的 init()。

      将您的代码修改为以下代码

      let myComponent: MyComponent;
      let myService: MyService;
      let fixture;
      
      describe('myComponent', () => {
         beforeEach(() => {
          TestBed.configureTestingModule({
            declarations: [MyComponent],
          providers: [
            {provide: MyService, useClass: MockMyService} // **--passing Mock service**
        ]
      }).compileComponents()
        .then(() => {
         fixture = TestBed.createComponent(MyComponent);
          myComponent = TestBed.createComponent(MyComponent).componentInstance;
          myService = TestBed.get(MyService);
          console.log(myService.getData());
        });
      });
      
      it('should get the mock data', () => {
         fixture.detectChanges();  // this line will call components ngOnInit() method
         expect(myComponent.data).toBe(DATA_OBJECT);
        });
      })
      

      查看fixture.detectChanges(); 行,第一次发生更改检测时,将调用组件ngOnInit()

      【讨论】:

      • 这解决了我的问题,谢谢。这就是 angular/cli 原理图应该如何生成规范文件 (.compileComponents().then())
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-12-30
      • 2017-06-16
      • 2021-12-02
      • 1970-01-01
      • 2020-01-17
      • 2019-09-16
      • 2017-04-04
      相关资源
      最近更新 更多