问题
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);
});
}
}
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);
}));
});
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();
}
}
And it logs it. So, it should keep the element in the DOM, but I can't find a way to test it.
回答1:
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.
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);
}));
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;
}
}
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.
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.
See Also:
- My post in Testing promise in Angular2 ngOnInit for an example of mocking
ActivatedRoute
to work synchronously when testing a component.
来源:https://stackoverflow.com/questions/39436230/angular-2-rc5-testing-promises-in-ngoninit-not-working