jayscript jayscript - 2 months ago 96
TypeScript Question

Angular 2 TestBed, Mocking Methods without Dependency Injection

Using TestBed, we are able to create mock classes for classes that are available with dependency injection. For example,

MyButtonClass
has access to
ElementRef
and
MyService
since they are implemented with dependency injection, and so we can override them. The problem I have is that, to write a Jasmine test, I have to create mock classes to override methods of classes that are not accessed with dependency injection.

In this case,
ScriptLoader.load
will load
ThirdPartyCheckout
in the global space. This means, it might not be available when Jasmine reads what is inside the subscribe operator. For this reason, I would like to mock the former first and then the latter after. Or maybe there is a different way to get around this.

It would be great if someone can suggest a way to create mock classes to override the
ScriptLoader.load
method and
ThirdPartyCheckout.configure
method.

The directive to be tested:

@Directive({
selector: '[myButton]'
})
export class MyButtonClass implements AfterViewInit {

private myKey: string;

constructor(private _el: ElementRef, private myService: MyService) {}

ngAfterViewInit() {
this.myService.getKey()
.then((myKey: string) => {
this.myKey = myKey;
ScriptLoader.load('https://thirdpartyurl.js', this._el.nativeElement)
.subscribe(
data => {
this.handeler = ThirdPartyCheckout.configure(<any>{
key: this.myKey
// etc
// and some methods go here
});
},
error => {
console.log(error);
}
);
});
}
}


Here is the test code:

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

class mockMyService {
getKey() {
return Promise.resolve('this is a key in real code');
}
}

describe('myButton', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComponent, MyButtonClass],
providers: [
{provide: MyService, useClass: mockMyService}
]
});
});

describe('ngAfterViewInit', fakeAsync(() => {
const template = '<div><div myButton></div></div>';
TestBed.overrideComponent(TestComponent, {set: {template: template}});
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
tick();
}));
});

Answer

Functions being first-class citizens, you can just assign a new function to it

let originalFn;

beforeEach(() => {
  originalFn = ScriptLoader.load;
});

afterEach(() => {
  ScriptLoader.load = originalFn;
});

it('...', fakeAsync(() => {
  ScriptLoader.load = (url, el: Element): Observable<string> => {
    return Observable.of('HelloSquirrel');
  };
  ...
}));

Other than this, you might want to just consider using DI. One of the main reasons for using DI is for better testability. For the ScriptLoader just make the method a non static method, and for the third party lib just create as abstraction service layer for it.