Angular 2 RC5 测试 Promise 在 ngOnInit 中不起作用

标签 angular jasmine karma-jasmine angular2-testing angular-rc5

我正在尝试使用 Jasmine 测试名为 MyDirective 的结构指令。使用的 Angular 版本是 RC5。

// Part of the MyDirective class

@Directive({selector: '[myDirective]'})
export class MyDirective {
    constructor(protected templateRef: TemplateRef<any>,
                protected viewContainer: ViewContainerRef,
                protected myService: MyService) {
    }

    ngOnInit() {
        this.myService.getData()
            .then((data) => {
                if (!MyService.isValid(data)) {
                    this.viewContainer.createEmbeddedView(this.templateRef);
                } else {
                    this.viewContainer.clear();
                }
            })
            .catch((error) => {
                console.log(error);
                this.viewContainer.createEmbeddedView(this.templateRef);
            });
    }
}

MockService类中重写了getData方法,而直接调用isValid方法(MyService的静态方法),该方法检查数据的有效性。

// Part of the Jasmine unit test class for the MyDirective class

@Component({
    selector: 'test-cmp', template: '', directives: [MyDirective]
})
class TestComponent {}

class MockService {
    mockResponse: MyResponse = {valid date goes here};
    mockInvalidResponse: MyResponse = {};

    getData() {
        if (booleanCondition) {
            return Promise.resolve(this.mockResponse);
        } else {
            return Promise.resolve(this.mockInvalidResponse);
        }
    }
}

describe('MyDirective', () => {
    beforeEach(() => {
        TestBed.configureTestingModule({
            declarations: [TestComponent],
            providers: [
                {provide: MyService, useClass: MockService},
                TemplateRef,
                ViewContainerRef
            ]
        });
    });

    it('should remove the target DOM element when the condition is true', async(() => {
        booleanCondition = true;
        const template =
             '<div><div *myDirective><span>Hi</span></div></div>';

        TestBed.overrideComponent(TestComponent, {set: {template: template}});
        let fixture = TestBed.createComponent(TestComponent);
        fixture.detectChanges();
        expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length).toEqual(0);
    }));

    it('should contain the target DOM element when the condition is false', async(() => {
        booleanCondition = false;
        const template =
             '<div><div *myDirective><span>Hi</span></div></div>';

        TestBed.overrideComponent(TestComponent, {set: {template: template}});
        let fixture = TestBed.createComponent(TestComponent);
        fixture.detectChanges();

        // The 'expect' bellow fails because the value is 0 for some reason
        expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length).toEqual(1);
    }));
});

第二个 it 应该创建一个 span 元素位于 DOM 中的情况,但事实并非如此。我检查了它是否符合 if 语句中的第一个条件,如下所示:

if (!MyService.isValid(data)) {
        console.log('the first if condition is read.');
        this.viewContainer.createEmbeddedView(this.templateRef);
    } else {
        this.viewContainer.clear();
    }
}

它会记录它。所以,它应该将元素保留在 DOM 中,但我找不到测试它的方法。

最佳答案

这是因为 Promise(从 getData 返回的 Promise)是异步的。因此,所有同步事件都会在 Promise 事件之前得到处理。即使调用 ngOnInitPromise 也会异步解析。

对于这种类型的事情,我通常使用几个选项。

一种选择是使用 fakeAsync而不是异步。这允许您调用 tick 以允许异步操作同步完成

import { fakeAsync, tick } from '@angular/core/testing';

it('... when the condition is false', fakeAsync(() => {
  const template = '<div><div *myDirective><span>Hi</span></div></div>';

  TestBed.overrideComponent(TestComponent, { set: { template: template } });
  let fixture = TestBed.createComponent(TestComponent);
  fixture.detectChanges();
  // tick can also be called with a millisecond delay argument `tick(1000)`
  tick();          
  expect(getDOM().querySelectorAll(fixture.debugElement.nativeElement, 'span').length)
    .toEqual(1);
}));

另一个选项是使模拟服务同步。您可以通过调用 getData() 返回服务本身,并向服务添加 thencatch 方法来轻松实现此目的。例如

class MockMyService {
  data;
  error;

  getData() {
    return this;
  }

  then(callback) {
    if (!this.error) {
      callback('mockData');
    }
    return this;
  }

  catch(callback) {
    if (this.error) {
      callback(this.error);
    }
  }

  setData(data) {
    this.data = data;
  }

  setError(error) {
    this.error = error;
  }
}

这种方法的一个优点是它可以让您在测试执行期间更好地控制服务。这在测试使用 templateUrl 的组件时也非常有用。 XHR calls can't be made in a fakeAsync ,所以使用它不是一个选择。这就是同步模拟服务的用武之地。

您可以将服务注入(inject)到您的 it 测试用例中,或者您可以在测试中保留一个变量并对其进行设置,例如

let mockMyService: MockMyService;

beforeEach(() => {
  mockMyService = new MockMyService();
  TestBed.configureTestingModule({
    providers: [
      { provide: MyService, useValue: mockMyService }
    ]
  });
});

注意:您还需要修正您的通过测试,因为您当前的测试由于上述原因无效。


另请参阅:

关于Angular 2 RC5 测试 Promise 在 ngOnInit 中不起作用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/39436230/

相关文章:

css - 更改 Clarity 拨动开关的颜色

javascript - 使用 Jasmine 捕获传递给不同 JavaScript 文件中函数的参数

javascript - 在 AngularJS Controller 规范中测试来自服务的回调

angular - karma 单元测试 : ERROR in Cannot read property 'length' of undefined

unit-testing - 我是否需要在单元测试中取消订阅?

javascript - 将使用 Javascript/Jquery 构建的 Web 应用程序的一部分转换为 Angular 2

angular - 使 Angular 对话框只能通过标题拖动

angularjs - Angular 应用程序中 Google Analytics 的 Jasmine 测试出错

Angular2 karma 测试说 "TypeError: this._subscribe is not a function"

angularjs - Angular UI 网格 onRegisterApi 函数的 Jasmine 测试用例