TDD: ¿Cómo escribir código testeable?

Post on 27-Jan-2015

127 views 0 download

description

 

Transcript of TDD: ¿Cómo escribir código testeable?

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