问题描述
我正在尝试使用 Jasmine 测试名为 MyDirective 的结构指令.使用的 Angular 版本是 RC5.
I am trying to test a structural directive named MyDirective with Jasmine. The Angular version used is 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);
});
}
}
getData 方法在 MockService 类中被覆盖,而 isValid 方法(MyService 的静态方法)被直接调用,用于检查数据的有效性.
The getData method is overwritten in the MockService class whereas the isValid method (a static method of MyService) is called directly, which checks the validity of the data.
// 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 语句中的第一个条件,如下所示:
The second it
is supposed to create a case in which the span element is in the DOM, but it does not. I checked to see if it goes to the first condition in the if statement like this:
if (!MyService.isValid(data)) {
console.log('the first if condition is read.');
this.viewContainer.createEmbeddedView(this.templateRef);
} else {
this.viewContainer.clear();
}
}
它会记录下来.因此,它应该将元素保留在 DOM 中,但我找不到测试它的方法.
And it logs it. So, it should keep the element in the DOM, but I can't find a way to test it.
推荐答案
这是因为 Promise
(从 getData
返回的那个)是异步的.因此,所有同步活动都在 Promise
活动之前得到处理.即使调用了 ngOnInit
,Promise
也是异步解析的.
It because a Promise
(the one returned from getData
) is asynchronous. So all the synchronous activity gets handled ahead of the Promise
activity. Even though ngOnInit
is called, the Promise
is resolved asynchronously.
我通常使用几个选项来处理这种类型的事情.
There are a couple options that I usually use for this type thing.
一种选择是使用 fakeAsync
而不是 async
.这允许您调用 tick
以允许异步操作同步完成
One option is to use fakeAsync
instead of async
. This allows you to call tick
to allow for asynchronous actions to complete synchronously
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()
返回服务本身,并向服务添加 then
和 catch
方法来轻松做到这一点.例如
Another options is to make the mocked service synchronous. You can easily do that by making the call to getData()
return the service itself, and add a then
and catch
method to the service. For example
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 调用不能在 fakeAsync
中进行,所以使用它不是一种选择.这就是使用同步模拟服务的地方.
One advantage of this approach is that it gives you more control over the service during the test execution. This is also very useful when testing components that use templateUrl
. XHR calls can't be made in a fakeAsync
, so using that is not an option. This is where the synchronous mock service comes in use.
您可以将服务注入到您的 it
测试用例中,也可以在测试中保留一个变量并设置类似
You can either inject the service to your it
test cases or you can just keep a variable in you test and set it up something like
let mockMyService: MockMyService;
beforeEach(() => {
mockMyService = new MockMyService();
TestBed.configureTestingModule({
providers: [
{ provide: MyService, useValue: mockMyService }
]
});
});
注意:您还需要修正您的通过测试,因为您当前的测试由于上述原因无效.
Note: You'll also want to fix your passing test, as your current test is not valid for reasons mentioned above.
另请参阅:
- 我在 Testing Promise in Angular2 ngOnInit 中的帖子中的一个模拟
ActivatedRoute
工作的示例在测试组件时同步.
- My post in Testing promise in Angular2 ngOnInit for an example of mocking
ActivatedRoute
to work synchronously when testing a component.
这篇关于ngOnInit 中的 Angular 2 RC5 测试承诺不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!