Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular 2 - Mocking Private Properties

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?

like image 749
Roy Reiss Avatar asked Dec 15 '22 00:12

Roy Reiss


1 Answers

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.

like image 186
Aluan Haddad Avatar answered Dec 28 '22 10:12

Aluan Haddad