How to write unit testing for Angular / TypeScript for private methods with Jasmine
I'm with you, even though it's a good goal to "only unit test the public API" there are times when it doesn't seem that simple and you feel you are choosing between compromising either the API or the unit-tests. You know this already, since that's exactly what you're asking to do, so I won't get into it. :)
In TypeScript I've discovered a few ways you can access private members for the sake of unit-testing. Consider this class:
class MyThing {
private _name:string;
private _count:number;
constructor() {
this.init("Test", 123);
}
private init(name:string, count:number){
this._name = name;
this._count = count;
}
public get name(){ return this._name; }
public get count(){ return this._count; }
}
Even though TS restricts access to class members using private
, protected
, public
, the compiled JS has no private members, since this isn't a thing in JS. It's purely used for the TS compiler. Therefor:
You can assert to
any
and escape the compiler from warning you about access restrictions:(thing as any)._name = "Unit Test"; (thing as any)._count = 123; (thing as any).init("Unit Test", 123);
The problem with this approach is that the compiler simply has no idea what you are doing right of the
any
, so you don't get desired type errors:(thing as any)._name = 123; // wrong, but no error (thing as any)._count = "Unit Test"; // wrong, but no error (thing as any).init(0, "123"); // wrong, but no error
This will obviously make refactoring more difficult.
You can use array access (
[]
) to get at the private members:thing["_name"] = "Unit Test"; thing["_count"] = 123; thing["init"]("Unit Test", 123);
While it looks funky, TSC will actually validate the types as if you accessed them directly:
thing["_name"] = 123; // type error thing["_count"] = "Unit Test"; // type error thing["init"](0, "123"); // argument error
To be honest I don't know why this works.This is apparently an intentional "escape hatch" to give you access to private members without losing type safety. This is exactly what I think you want for your unit-testing.
Here is a working example in the TypeScript Playground.
Edit for TypeScript 2.6
Another option that some like is to use // @ts-ignore
(added in TS 2.6) which simply suppresses all errors on the following line:
// @ts-ignore
thing._name = "Unit Test";
The problem with this is, well, it suppresses all errors on the following line:
// @ts-ignore
thing._name(123).this.should.NOT.beAllowed("but it is") = window / {};
I personally consider @ts-ignore
a code-smell, and as the docs say:
we recommend you use this comments very sparingly. [emphasis original]
You can call private methods.
If you encountered the following error:
expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);
// TS2341: Property 'initFooBar' is private and only accessible within class 'FooBar'
just use // @ts-ignore
:
// @ts-ignore
expect(new FooBar(/*...*/).initFooBar()).toEqual(/*...*/);
As most of the developers don't recommend testing private function, Why not test it?.
Eg.
YourClass.ts
export class FooBar {
private _status: number;
constructor( private foo : Bar ) {
this.initFooBar({});
}
private initFooBar(data){
this.foo.bar( data );
this._status = this.foo.foo();
}
}
TestYourClass.spec.ts
describe("Testing foo bar for status being set", function() {
...
//Variable with type any
let fooBar;
fooBar = new FooBar();
...
//Method 1
//Now this will be visible
fooBar.initFooBar();
//Method 2
//This doesn't require variable with any type
fooBar['initFooBar']();
...
}
Thanks to @Aaron, @Thierry Templier.