5

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?

5
  • 1
    I suggest you look at the official Angular 2 testing guide, since you're not testing the 'angular way'. This problem is solved by using Testbed and configureTestingModule. angular.io/docs/ts/latest/guide/testing.html#!#testbed Commented Feb 15, 2017 at 22:21
  • Use a provider to inject your mock at instantiation. This is the purpose of dependancy injection. Their is good documentation at angular.io on how to do this. Commented Feb 15, 2017 at 22:28
  • You can just specify the type of appHttpMock as any, to force TypeScript to relax the type check. AFAIK, you can also use jasmine.createSpyObj() to create the whole mock object, and pretend that the result is an AppHttpMockService: let appHttpMock = jasmine.createSpyObj(...) as AppHttpService;. Commented Feb 15, 2017 at 22:32
  • And it seems there is even a generic method declared in the jasmine d.ts file to do that: jasmine.createSpyObj<AppHttpService>(...) Commented Feb 15, 2017 at 22:40
  • 1
    @Adam thanks, I actually write my integrated tests using the 'angular way' with a test bed (to be clear I mean integrated in terms of Angular, I use mocks for dependencies in those tests too). Unfortunately, those tests can take 500-1000ms to run even with mocks and that doesn't scale well if you're using TDD, BDD. I saw this as a faster way of testing logic that doesn't touch Angular. Commented Feb 15, 2017 at 23:25

3 Answers 3

9

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Wow! That explanation. 👌🏻
2

In this case you could use interfaces:

export interface IAppHttpService  {post(body):Observable<any>;}

export class AppHttpService implements IAppHttpService { ... } 

export class UserService {
  constructor(@Inject(AppHttpService) private appHttp: IAppHttpService) {
  }
}

3 Comments

Observable<any> is dangerous because it is compatible with Observable<void>, consider using Observable<{}>.
On the whole this is a nice solution in that, necessary or not, it illustrates a useful technique. The only real downside besides the types (easy to fix em :)) is that you use the concrete type as the DI token for injecting the implementation. Obviously that does not matter in this case, but using classes as interfaces is a bad pattern and the great thing about this answer is it shows how to avoid that, but then fails to do so. Consider using this. export interface IAppHttpService {post(body):Observable<{}>;} export namespace IAppHttpService {} and then @Inject(IAppHttpService).
that would require @NgModule({ providers: [ { provide: IAppHttpService, useClass: AppHttpService } ] }) somewhere.
2

I had similar problem and I have found this solution (without the usage of jasmine.createSpy):

import { Service } from 'my-service';

class ServiceMock {
    service(param: string): void { // do mocking things }
}

function factoryServiceMock(): any {
    return new ServiceMock();
}

// user.service.spec.ts
describe('', () => {
    const serviceMocked = factoryServiceMock() as Service;
    const userService = new UserService(serviceMocked);
    beforeEach(() => {
    });
    it('', () => {
    });
});

I don't know if it is the best solution, but it works for mocking strategy and obviously doesn't use Testbed

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.