TypeScript - Angular 2 - Simulación de Propiedades Privadas
Aunque entiendo que la forma ideal de probar el código es consumirlo de la misma manera que lo hará en producción y, por lo tanto, no tratar directamente con propiedades y métodos privados, TypeScript me tiene un poco desconcertado.
Tengo un servicio de usuario.
// user.service.ts
import {Injectable} from '@angular/core';
import {AppHttpService} from '../app-http/app-http.service'
@Injectable()
export class UserService {
constructor(private appHttp: AppHttpService) {
}
}
Como se muestra, depende de un servicio appHttp que tiene propiedades y métodos privados y digamos que se ve así:
// 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);
}
}
Para ejecutar una prueba aislada en mi servicio de usuarios, me gustaría proporcionarle un mock simple de mi servicio appHttp. Desafortunadamente, si solo hago un mock de los métodos públicos y propiedades de appHttp y se lo proporciono a mi constructor así:
// user.service.spec.ts
describe('', () => {
let appHttpMock = {
post: jasmine.createSpy('post')
};
let service = new UserService(appHttpMock);
beforeEach(() => {
})
it('', () => {
})
})
Recibo un error que indica:
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; }'.
Si cambio mi simulación para simplemente agregar la propiedad, obtendré otro error que se queja de que no es privada. Si creo una clase simulada de verdad, como:
// app-http.mock.ts
export class AppHttpMockService {
private apiUrl = 'my domain';
constructor() {
}
post() {
}
}
Todavía obtendré otro error de TypeScript:
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'.
¿Cuál es una forma limpia de ejecutar pruebas aisladas (es decir, una que no requiera la creación que consume mucho tiempo de un banco de pruebas) sin que TypeScript se preocupe por las propiedades y métodos privados del mock?
Respuesta #1
TL;DR: Evita private si te está causando problemas de testeo, ya que aporta muy poco.
Como gran fanático de TypeScript y defensor del lenguaje, sin embargo, tengo poco uso para la palabra clave private.
Tiene algunos problemas.
No hace nada para limitar el acceso real al miembro en tiempo de ejecución, solo sirve para limitar la superficie de la API pública de una abstracción.
Obviamente, la mayoría de los beneficios de TypeScript son solo en tiempo de compilación (lo cual es algo excelente, por cierto), así que esto no es un punto directo en contra de la característica, pero siento que les da a los desarrolladores una falsa sensación de encapsulación y, más problemáticamente, los anima a evitar técnicas de JavaScript probadas y verdaderas que en realidad sí hacen cumplir la privacidad mediante cierres.
Ahora sé que si estás usando clases, la encapsulación mediante cierres varía de incómoda a imposible, pero si realmente quieres un campo privado, y ciertamente si quieres un método privado, puedes declarar una función externa a la clase y no exportarla desde el módulo contenedor. Esto te da verdadera privacidad.
También hay otro problema. ECMAScript tiene una propuesta en marcha para propiedades privadas reales y es prácticamente seguro que utilizará una sintaxis diferente y tendrá una semántica distinta también.
Básicamente, la noción de private en TypeScript tiene una semántica muy débil y se convertirá en un choque sintáctico y semántico en el futuro cuando las propiedades realmente privadas sean parte de ECMAScript, así que simplemente no la uses donde sea problemático.
No estoy diciendo que lo evites por completo, pero en este tipo de situación, eliminar el modificador es la solución más sencilla.
Además, mucho estado mutable, ya sea privado o de otro tipo, no es deseable. A veces es necesario para optimizar cosas, pero primero necesitarías medir para saber si realmente te ayuda en tu escenario específico.
Más a menudo que no, lo mejor con lo que se puede definir una propiedad de clase es un get sin un set correspondiente.
Respuesta #2
En este caso podrías usar interfaces:
export interface IAppHttpService {post(body):Observable<any>;}
export class AppHttpService implements IAppHttpService { ... }
export class UserService {
constructor(@Inject(AppHttpService) private appHttp: IAppHttpService) {
}
}
