While I understand that the ideal way to test code is to consume it the same way it will be in production and thus don't directly deal with private properties and methods TypeScript has me a little flummoxed.
I have a user service.
// user.service.ts
import {Injectable} from '@angular/core';
import {AppHttpService} from '../app-http/app-http.service'
@Injectable()
export class UserService {
constructor(private appHttp: AppHttpService) {
}
}
As shown it depends on an appHttp service which has private properties and methods and let's say looks like this:
// app-http.service.ts
@Injectable()
export class AppHttpService {
private apiUrl = 'my domain';
constructor(private http: Http, private authHttp: AuthHttp) {
}
post(body): Observable<any> {
return this.http.post(this.apiUrl, body)
.map((res)=>res)
.catch((err)=>null);
}
}
In order to run an isolated test on my user service I would like to hand it a simple mock of my appHttp service. Unfortunately, if I just mock the public methods, properties of appHttp and provide it to my constructor like so:
// user.service.spec.ts
describe('', () => {
let appHttpMock = {
post: jasmine.createSpy('post')
};
let service = new UserService(appHttpMock);
beforeEach(() => {
})
it('', () => {
})
})
I get an error stating:
Error:(11, 33) TS2345:Argument of type '{ post: Spy; }' is not assignable to parameter of type 'AppHttpService'. Property 'apiUrl' is missing in type '{ post: Spy; }'.
If I change my mock to simply add the property I'll get another error complaining that it's not private. If I create a bonafide mock class such as:
// app-http.mock.ts
export class AppHttpMockService {
private apiUrl = 'my domain';
constructor() {
}
post() {
}
}
I'll still get another TypeScript error:
Error:(8, 33) TS2345:Argument of type 'AppHttpMockService' is not assignable to parameter of type 'AppHttpService'. Types have separate declarations of a private property 'apiUrl'.
What is a clean way to run isolated tests (i.e., one that doesn't require the time consuming creation of a testbed) without TypeScript fussing over the private properties and methods of the mock?
TL;DR: Avoid private
if it is causing you testability problems since it does precious little.
As a huge fan of TypeScript and an advocate of the language, I nevertheless have little use for the private
keyword.
It has a few problems.
It does nothing to limit actual access to the member at runtime, it only serves to limit the public API surface of an abstraction.
Obviously most of TypeScript's benefits are compile time only (which is a great thing by the way) so this is not a direct point against the feature, but I feel that it gives developers a false sense of encapsulation, and more problematically, encourages them to eschew tried and true JavaScript techniques that actually do enforce privacy via closures.
Now I know that if you are using classes, encapsulation via closures ranges from awkward to impossible, but if you actually want a private field, and certainly if you want a private method, you can declare a function external to the class and not export it from the enclosing module. This gives you true privacy.
There is also another issue. ECMAScript has a proposal in the works for real private properties and it is pretty much guaranteed to use a different syntax and have different semantics as well.
Basically TypeScript's notion of private
has very weak semantics and will become a syntactic and semantic clash in the future when true private properties are part of ECMAScript, so just don't use it where it is problematic.
I am not saying to avoid it completely, but in this kind of situation, dropping the modifier is the simplest solution.
Also lots of mutable state, private or otherwise, is not desirable. It is sometimes necessary to optimize things, but you would need to measure first to know if it even helps you in your specific scenario.
More often than not, the best thing a class property can be defined by is a get
with no corresponding set
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With