tdd: ¿cómo escribir código testeable?
DESCRIPTION
TRANSCRIPT
TDD: Cómo escribir código testeable.
¿Hacer tests es bueno?
¿Por qué no los hacemos?
Razones
Válidas
Inválidas
No hacer
pruebas
¡No se!
Código antiguo
El diseño es malo
No coge los bugs
Es len
to
UI
De eso se encarga QA
Demasiadas interfacesEs dificil de cambiar
Aburi
do…
.
Hacer tests es una habilidad
Entonces…
¿Cómo se escribe código difícil de probar?
¿Por qué?
Construcción de objetosTrabajo en el constructorEstados globalesViolaciones de la Ley de Deméter
Localización del “new”
API de Prueb
as
Clase bajo
prueba
Estímulo
Verificaciones
Localización del “new”
API de Prueb
as
Clase bajo
prueba
Estímulo
Verificaciones
Otra clase
Otra clase
Otra clase
Instanciado dentro de la clasePasado por referenciasEstados globales
Localización del “new”
API de Prueb
as
Clase bajo
prueba
Estímulo
Verificaciones
Otra clase
Otra clase
Otra clase
Otra clase
Otra clase
Otra clase
Otra clase
Localización del “new”
API de Prueb
as
Clase bajo
prueba
Estímulo
Verificaciones
Otra clase
Otra clase
Otra clase
Otra clase
Otra clase
Otra clase
Otra clase
Costu
ra
Localización del “new”
API de Prueb
as
Clase bajo
prueba
Estímulo
Verificaciones
Clase Falsa
Clase Falsa
Clase Falsa
Costu
ra
Localización del “new”
Construcción del Grafo de Objetos y Búsqueda
Lógica de Negocio
Localización del “new”
Construcción del Grafo de Objetos y Búsqueda
Lógica de Negocio
Localización del “new”
API de Prueb
as
Clase bajo
prueba
Estímulo
Verificaciones
Clase Falsa
Clase Falsa
Clase Falsa
Instanciado dentro de la clasePasado por referenciasEstados globales
Ejemplo (House)
House
class House {Kitchen kitchen = new Kitchen();Bedroom bedroom;public House() {
this.bedroom = new Bedroom();
}}
House Test
class HouseTests {@Testpublic void impossibleTestOnIsolation() {
// Can’t replace anything!!!}
}
Análisis
Fácil de instanciar pero…No se puede remplazar nada.Puede ser costoso
computacionalmenteEl diseño está acoplado. Cerrado para extensión.
House (Refactoizado)
class House {Kitchen kitchen;Bedroom bedroom;@Injectpublic House(Kitchen kitchen, Bedroom br) {
this.bedroom = br;this.kitchen = kitchen;
}}
House Test (Refactorizado)
class HouseTests {@Testpublic void easierToTestOnIsolation() {DummyKitchen dk = new DummyKitchen();DummyBedroom db = new DummyBedroom();House h = new House(dk,db);// …}
}
Ejemplo (Gardener)
Gardener
class Garden {Gardener joe;public Garden(Gardener joe) {
joe.setWorkHours(new TwelveHours());
joe.setBoots(new ExpensiveBoots());
this.joe = joe;}
}
Análisis
Mejor pero… No se pueden remplazar las botas
o el horario.La prueba puede tardar (horario
de 12h).Necesita el jardinero pero se
encarga de configurarlo.
Gardener (Refactored)
class Garden {Gardener joe;@Injectpublic Garden(Gardener joe) {
this.joe = joe;}
}
class GardenerProvider {@ProvidesGardener getGardener(ExpensiveBoots boots, TwelveHours schedule) {
Gardener joe = new Gardener();joe.setWorkHours(schedule);joe.setBoots(boots);return joe;
}}
¿Por qué?
Construcción de objetosTrabajo en el constructorEstados globalesViolaciones de la Ley de Deméter
Costo de la construcción
Para testear, primero hay que instanciar pero…
El trabajo dentro del constructor no tiene “costuras”
No se puede sobre escribir.La prueba tiene que saber
navegar lo que haya dentro
Ejemplo (Super Car)
Super Car
class Car {Engine engine;public Car(File file) {
String m = readModelFromCfg(file);engine = new
EngineFactory().create(m);}
}
Super Car Test
class CarTest {@Testpublic void hardToSetupTest() {
File file = new File(“custom.conf”);Car car = new Car(file);//Asserts...
}}
Análisis
Pasamos un File cuando lo que se necesita es un Engine.Toda prueba que necesite Car requiere eta parafernaria.Accede al sistema de ficheros
Lento.No es una prueba unitaria
realmente.
Super Car (Refactorizado)
class Car {Engine engine;@Injectpublic Car(Engine engine) {
this.engine = engine;}
}
Super Car (Provider)
class CarEngineProvider {@ProvidesEngine getEngine(EngineFactory factory, @EngineModel String model) {
return factory.create(model);}
}
Super Car Test (Refactorizado)
class CarTest {@Testpublic void easyToSetupTest() {
Engine engine = new FakeEngine();
Car car = new Car(engine);//Asserts...
}}
¿Por qué?
Construcción de objetosTrabajo en el constructorEstados globalesViolaciones de la Ley de Deméter
Locura
Locura
Definición:Repetir una misma acción una y otra vez y esperar obtener un resultado diferente.
Albert Einstein
O mejor dicho… Estados Globales
class X {public X() {}public int doSomething() {
//… Something…}
}
Estados Globales
int a = new X().doSomething();
int b = new X().doSomething();
Estados Globales
¿ a == b ó a != b ?
Estados Globales
X x1 = new X(); X
Y
Z
Q
Estados Globales
X x1 = new X();
X x2 = new X();
X
Y
Z
Q
X
Y
Z
Q
Estados Globales
X x1 = new X();x1.doSomething();
A == B ó A != B
X x2 = new X();x2.doSomething();
X
Y
Z
Q
X
Y
Z
Q
GS
Consecuencias
Múltiples ejecucionesResultados inesperadosOrden importaNo se pueden ejecutar en paralelo
Localización del estado desacotado.
Estados Globales Ocultos
System.currentTime()new Date()Math.random()
Consecuencias
La API miente:A cerca de lo que necesitaHace cosas que dan miedo detrás
de la fachada
Ejemplo (Credit Card)
Credit Card
@Testpublic void creditCardChargeTest() {
CreditCard cc = new CreditCard(“1234567890”);cc.charge(200);
}
Credit Card
@Testpublic void creditCardChargeTest() {
CreditCard cc = new CreditCard(“1234567890”);cc.charge(200);
}
java.lang.NullPointerException at talk2.CreditCard.charge(CreditCard.java:48)
Credit Card
@Testpublic void creditCardChargeTest() {
CreditCardProcessor.init(…);CreditCard cc = new CreditCard(“1234567890”);cc.charge(200);
}
Credit Card
@Testpublic void creditCardChargeTest() {
CreditCardProcessor.init(…);CreditCard cc = new CreditCard(“1234567890”);cc.charge(200);
}
java.lang.NullPointerException at talk2.CreditCardProcessor.init(CreditCardProcessor.java:134)
Credit Card
@Testpublic void creditCardChargeTest() {
OfflineQueue.start();CreditCardProcessor.init(…);CreditCard cc = new CreditCard(“1234567890”);cc.charge(200);
}
Credit Card
@Testpublic void creditCardChargeTest() {
OfflineQueue.start();CreditCardProcessor.init(…);CreditCard cc = new CreditCard(“1234567890”);cc.charge(200);
}
java.lang.NullPointerException at talk2.OffileQueue.start (OfflineQueue.java:203)
Credit Card
@Testpublic void creditCardChargeTest() {
Database.connect(…);OfflineQueue.start();CreditCardProcessor.init(…);CreditCard cc = new CreditCard(“1234567890”);cc.charge(200);
}
Credit Card
¿Se ve un patrón?
Credit Card
Miente sobre sus dependencias.El orden de las inicializaciones no es
explicito.
Credit Card
¿Qué podemos hacer?
Credit Card (Refactorizado)
@Testpublic void creditCardChargeTest() {
Database db = Database(“connectionStr”);OfflineQueue queue = new OfflineQueue(db);CreditCardProcessor ccProcessor = new CreditCardProcessor(queue);CreditCard cc = new CreditCard(“1234567890”, ccProcesor);cc.charge(200);
}
Credit Card
¡Tenemos opciones!
Credit Card
Pero…
¿Y si termino con 20 parámetros?
Es porque los tenias… solo que…
Accounting 101
class AccountView {User user;public AccountView() {
user = RPCClient.getInstance().getUser();}
}
Análisis
Uso de Estados GlobalesIncorpora las dependencias del “singleton”Miente a cerca de las dependencias.
Accounting 101
class AccountView {User user;public AccountView(User user) {
user = user;}
}
¿Por qué?
Construcción de objetosTrabajo en el constructorEstados globalesViolaciones de la Ley de Deméter
La ley de Deméter
Cada unidad debería tener conocimiento limitado sobre otras unidades: solo unidades “relacionadas de cerca” a la unidad actual.
Cada unidad debe de hablar solamente con sus amigos; No hablar con extraños.
Habla solamente con tus amigos inmediatos.
Service Locator
Aka. Context, aka. Registry… etc.
Mejor que un singletonAl menos el problema está en un solo lugar.Es testeable… pero no muy bonito.
Esconde las dependencias reales.Las dependencias hacen el código poco
reusable.
Service Locator
Class House {//…
public House(Locator locator) {//… Qué tengo que mockear???
}}
House
Class House {//…Window window;Door door;Roof roof;public House(Locator locator) {
this.window = locator.getWindow();this.door = locator.getDoor();this.roof = locator.getRoof();
}}
House (Refactorizado)
class House {//…Window window;Door door;Roof roof;public House(Window w, Door d, Roof, r) {
this.window = w;this.door = d;this.roof = r;
}}
Service Locator
¿Qué otro problema tiene?Mezcla responsabilidades.
Buscar y encontrar.Creación
Se necesita tener una interface para testearSi dependes de ServiceLocator, dependes de todo lo demás.
Making a Mockery…
class LoginPage {RPCClient client;HttpRequest request;public LoginPage(RPCClient client, HttpRequest request) {
this.client = client;this.request = request;
}
public boolean login() {String cookie = request.getCookie();return client.getAuthenticator().authenticate(cookie);
}}
Análisis
Para testear se hace complicadoLas dependencias no son las declaradas.Crear rpc client, entrenarlo para devolver un authenticator, crear un authenticator… Etc.
Making a Mockery…
class LoginPage {Authenticator authenticator;String cookie;public LoginPage(Authenticator auth, @Cookie String cookie) {
this.cookie= cookie;this.authenticator = auth;
}
public boolean login() {return authenticator.authenticate(cookie);
}}
Making a Mockery
Cosas a tener en cuenta.Tiempo de vida de los objetos que se pasan.
Ej. Cookie puede ser distinto para cada vez