17 febrero 2012
Setup de una aplicación web java en 3 lineas
Algunas alternativas para hacer rapidamente un proyecto web son Maven y sus "arquetypes", NetBeans, Eclipse o Idea con los plugins correctos y un Tomcat instalado.
En este post quería contarles una alternativa que me resulta muy sencilla y util, ahi va la receta:
1. Instale Groovy y Gradle (solo es necesario hacerlo una vez).
2. Cree un directorio para el projecto siguiendo la misma convención de Maven:
mkdir -p prueba/src/main/webapp
3. Agregue una pizca de Gradle
echo 'apply plugin: "jetty"' > prueba/build.gradle
4. Cocine:
cd prueba && gradle jettyRun
Ok, es una aplicación web vacía, pero podes agregar los HTML que necesites dentro de src/main/webapp (si usaste Maven la convención de directorios de Gradle es la misma).
Ahi va todo junto para que puedas hacer copy & paste (técnicamente son "4 lineas" pero el número 3 quedaba mejor para el titulo :) ):
mkdir -p prueba/src/main/webapp
echo 'apply plugin: "jetty"' > prueba/build.gradle
cd prueba && gradle jettyRun
... una cosita más...
Si necesitas usar Eclipse o Idea, agrega estas lineas a build.gradle:
apply plugin: 'eclipse'
apply plugin: 'idea'
Después ejecutas:
gradle eclipse idea
y listo!
22 diciembre 2011
"Developability"
¿Para que sirven estas clasificaciones?
Por un lado sirven para darle a los diseñadores de sistemas un punto de partida. Algo que permita dar respuesta a la pregunta: "entre las características que requiere nuestro sistema ¿nos estaremos olvidando de algo?". Por otro sirven para que los investigadores de arquitectura de software puedan definir métricas y estilos de solución.
Y por otro, sirven para que yo tenga una excusa por donde empezar este post :)
Estaba pensando que en estas clasificaciones hay un atributo de calidad perdido, que voy a bautizar como "Developability", y voy a definir así: la capacidad un sistema para reflejar los cambios introducidos durante el desarrollo.
Después de todo si en Java existen productos como JRebel y frameworks como Play anuncian sus bondades con frases como: "No need to compile, deploy or restart the server"; creo que existe una necesidad de tener la característica de "developability" en mente a la hora de pensar en el diseño de un sistema.
¿No es lo mismo que Maintainability?
Es parte de la mantenibilidad, en el sentido que forma parte de la facilidad de cambio de un sistema.
Pero muchas veces cuando se habla de mantenibilidad se hace referencia a cuan fácil es introducir nueva funcionalidad sin tener que hacer grandes cambios en el diseño.
Podemos tener, por ejemplo, un sistema muy fácil de extender, donde cada vez que un desarrollador introduzca un cambio tenga que esperar 2hs a que termine la compilación para poder verlo.
¿No es lo mismo que Testablity?
Testabilty se refiere a que tan fácil es verificar el software, por ejemplo: si es posible realizar test automatizados con facilidad o no.
Y lo que planteo es en cierto sentido similar, pero con objetivos levemente distintos: un sistema puede ser fácil de testear en el sentido que es posible automatizar tests u obtener información de estado relevante para el testing y aún así ser un tremendo "dolor" para desarrollar.
Un ejemplo: Mejorando el "developability" de Microwiki
Mi pequeño proyecto: microwiki; del que les conte hace un par de posts atras. Utiliza templates de html para renderizar las paginas web. Estos templates son resources y se cargan del classpath.
Por default Jetty no hace un reload del classpath cada vez que algo cambia -principalmente por que estar viendo el filesystem por cambios es una operación costosa.
Al principio tener que reiniciar microwiki para ver los cambios en un template no me molestaba: el proyecto es chico y reiniciar es rápido.
Sin embargo cuando empece a mejorar un poco el diseño del HTML esos segundos de presionar restart en el IDE y volver a cargar la página se volvieron una molestia.
Por suerte para no meterme con cuestiones de classpath reload y configuración de Jetty, algunos astros de "mantenibilidad" y "testeabilidad" se alinearon.
Para permitir que microwiki sea configurable mediante archivos de configuración, y que la interpretación de estos me sea fácil de mantener utilice un DSL de Groovy.
Es decir los archivos de configuración son código fuente Groovy, y en ellos es posible definir un template de la siguiente manera:
templates {
display = '/dir/disp.html'
edit = '/other/edit.html'
}
Por otro lado para facilitar el testing los templates implementan la interfaz: ViewTemplate. Esto me facilita la creación de mocks en los tests.
Juntando estas dos cosas, el cambio para facilitar el "developability" fue muy sencillo:
Agregue un método al builder que interpreta el DSL del archivo de configuración (ConfigBuilder):
ViewTemplate debug(source, Map context = [:]) {
{ Map ctx -> template(source, context).applyWith(ctx) } as ViewTemplate
}
Con este método es posible definir la configuración de esta manera:
templates {
File prjDir = new File('/Users/diegof/Projects/microwiki/src/main/resources')
edit = debug(new File(prjDir, 'microwiki/templates/edit.html'))
search = debug(new File(prjDir, 'microwiki/templates/search.html'))
}
13 diciembre 2011
Configuración basada en Java con Spring 3.x
La buena noticia es que a partir de Spring 3.x se puede configurar el application context casi 100% en Java (hay una pequeña parte que todavia hay que hacer con el XML). Me entere de esto leyendo el anuncio del release 3.1 en InfoQ (y para mi sorpresa esta posibilidad ya existía en el release 3.0).
Si planean usar Spring 3.x es una buena noticia por que pueden tirar ese applicationContext.xml al tacho y hacerlo directamente en Java que es más fácil de mantener y refactorizar :)
La implementación se basa mucho en annotations, pero es lo suficientemente flexible como para permitir refactorings (para ejemplos ver la sección 3.11.3 del manual de Spring 3.0). Aunque hubiese preferido que lo hagan como en Grails, donde hicieron un builder usando Groovy (que dicho sea de paso, si están en un proyecto Java/Groovy pueden usar el builder sin necesidad de depender de otras librerías de Grails).
09 diciembre 2011
Mapeos Objeto/Relacional
- No es conveniente jugar con las jerarquias de clases por que no se pueden mapear de una forma buena.
- No se puede refactorizar algo para cambiar su composición por que es engorroso modificar los mapeos.
- No se pueden usar objetos más "dinamicos", como objetos que tengan closures (en un lenguaje que lo permita), implemetaciones de interfaces definidas inline o simplemente un decorator como el del ejemplo.
09 noviembre 2011
Microwiki: Primeros pasos de diseño
El secreto es que incluso con más experiencia, uno tambien se encuentra algo perdido cuando se empieza con un problema nuevo.
Creo que un buen consejo para encarar un diseño es tener una "mente de principiante" y no dejar de preguntarse "¿Por qué?".
Claro que para hacerse las preguntas y poder responderlas se necesita un conocimiento previo.
Ambas cosas -hacerse las preguntas y construir ese conocimiento- van de la mano.
Esta introducción, viene a cuento que me dieron ganas de contarles las preguntas que me fui haciendo en la construcción de microwiki.
Para quienes no hayan visto mi post anterior, microwiki, es un pequeño servidor wiki que comencé a programar en mis ratos libres. Como objetivos de diseño, microwiki tiene las siguientes características:
- Se utiliza localmente: no hay usuarios, ni permisos, ni historial de versiones en las paginas.
- Iniciar el servidor debe ser tan simple como ejecutar un comando.
- Las paginas se guardan en el file system, y pueden editarse tanto dentro como fuera de la aplicación web.
- La búsqueda de contenido debe ser rápida.
Las cuestiones técnicas
En un mundo teórico ideal, uno debería evaluar la funcionalidad, los atributos de calidad, ver las herramientas disponibles, un largo etcéra y luego elegir la solución técnica más adecuada. La realidad es mucho más simple, venia jugando un poco con Groovy y Gradle así que esas fueron las herramientas que elegí para trabajar.
Al principio pensé en hacerlo con Scala para practicar un poco este lenguaje, pero la comodidad de IntelliJ IDEA para usar Groovy me compro -me estoy volviendo viejo, ya no tengo ganas de ponerme a configurar plugins en versión beta.
El resto de las opciones fueron más simples. Conocía la sintaxis Markdown de usar GitHub y Stackoverflow, y PegDown fue el primer parser que encontré para Java.
Y si voy a hacer un pequeño web server tampoco iba a empezar de cero, Jetty es muy conocido por proveer un API simple para crear web servers en Java sin meterse con todo el lío de JEE (otra opción en Grizzly, pero es mucho más nuevo y no tiene tanta documentación).
Primeros pasos
Partiendo de que microwiki es una aplicación web, y que voy a utilizar servlets con Jetty, el primer paso fue pensar en un servlet que mostrara una pagina.
Consejo: Empezar a diseñar siempre por un caso particular y simple.Entonces tenemos nuestro servlet que muestra la pagina. ¿Implementamos en el servlet la funcionalidad de abrir el archivo e invocar al parser? Respuesta rápida: no.
¿Por qué? El servlet se encarga de manejar el request y response de http, si ponemos todo junto no hay forma de testear la funcionalidad de obtener y parsear una pagina por separado.
Probablemente en alguna clase sobre diseño orientado a objetos escuchaste que las clases deberían tener alta cohesión, se referían justamente a este tipo de casos: hacer que la clase servlet implemente dos funcionalidades distintas es contraproducente a la hora de introducir cambios.
Los tests de unidad son útiles para detectar este tipo de problemas: si dejamos todo junto para testear la responsabilidad de brindar una pagina vamos a tener que crear un mock HttpServletRequest y un mock HttpServletResponse.
Consejo: Los mock objects son útiles, pero si tus tests necesitan muchos, probablemente le estés pifiando en la separación de responsabilidades.Entonces separando responsabilidades termine con algo así:
Para los "Templates" no hubo mucho análisis de mi parte: implementar la generación de HTML dentro del servlet es engorroso e inmantenible (en este caso la necesidad de separar responsabilidades es bien clara). Para implementar los templates use los GStrings de Groovy. Me pareció bueno mantener las cosas bien concretas: el template por ahora se utiliza para visualizar una pagina.
Nota: En el código actual en GitHub van a ver que el uso de los templates evoluciono hacia algo más generalizado, en otros posts les cuento el por que.¿Por que Writable?
Writable es una interfaz de Groovy que simplemente describe el método "writeTo(Writer)". Podría haber usado String, pero usar Writable permite expresar solo lo que necesito y optimizar las cosas si fuese necesario.
Si te estas preguntando a que me refiero con "optimizar": si uno tiene una pagina grande es preferible hacer un "streaming" que guardar toda la pagina en un gran String. Lamentablemente el parser de Markdown que estoy usando no permite hacerlo, pero como están planteadas las cosas podría usar otro parser sin afectar al resto de la aplicación.
¿Por que un objeto Page y no retornar directamente el String con el contenido?
Esta claro que para mi sistema una pagina no es simplemente un String.
Un fanático de TDD y del "paso a paso" me diría que para el test de mostrar la página un String alcanza. Sin embargo sé que voy a querer modelar más cosas de una pagina: una pagina tiene un titulo, una representación HTML y una representación en formato wiki.
¿Por qué una interface PageProvider?
Bueno yo tambien tengo la misma duda :)
Por ahora solo tengo una implementación de PageProvider y tampoco tengo intenciones de tener una distinta a futuro. En este punto hay dos cuestiones basadas en la experiencia que me llevaron a esto:
- Una interfaz PageProvider me facilitaría la creación de mocks en el caso que quisiera testear otros componentes que dependan de un PageProvider (si esta es una de las "malas" costumbres adquiridas de la experiencia en Java).
- Si quisiera agregar un cache podría usar un decorator que implemente esta interfaz.
Otra vez me estoy adelantando -son las manias que uno adquiere de la experiencia previa- en estos casos es importante tomar nota mental de que uno se esta adelantando. A veces por adelantarse, uno le puede errar fiero (de hecho me paso con la búsqueda, pero eso se los voy a contar en otro post). En este caso decidí seguir adelante: le veo mas ventajas que contras, pero si les pasa algo asi en un diseño y les queda la duda... paso a paso como diría mostaza.
Espero no haberlos aburrido mucho, la próxima les cuento algunos pasos más.
02 septiembre 2011
Some Java "dependency injection" bad practices
But when DI frameworks started to take advantage of annotations introduced in Java 5, the term "non-invasive" became relative, why? Here are some reasons:
@Autowire and @Inject annotation in private fields/private constructors
Spring, CDI and Guice allows you to annotate private fields, to let the DI framework inject the appropriate instance. But this feature has two problematic consequences, lets see them in a example taken from the Jboss CDI documentation:
public class Checkout {
private @Inject ShoppingCart cart;
}
- Is impossible to create a valid Checkout instance without using a framework that supports the @Inject annotation.
- You would not see the dependency directly by looking into the Checkout public interface or JavaDocs.
There are annotations and test classes to make tests for Spring beans, but in my experience (after having to maintain application-context.xml files created and used by multiple tests) is an unnecessary complexity that could be removed if the code is not coupled with the DI framework.
Some people argue that (2) is good (for example: this Guice "best practice"). The idea behind that is that developers will do "bad things" if they can: instead of obtaining the required instance by using dependency injection, they will use the constructor.
But that has more to do with a communication problem, and should be resolved by using code reviews, pair programming and walkthroughs.
@Required
In the same tone as the @Inject for private fields comes the @Required annotation supported by Spring. That annotation allows you to write things like:
public class Shape {
private Color color;
@Required
public void setColor(Color c) {
color = c;
}
....
}
Then the DI framework will give you an exception if the dependency is missing.
But what happens if you instantiate a Shape outside the DI framework? You don't know if you have to set the color or not.
A constructor is simple: no dependencies with the DI framework and comes free with the Java language :) (This part from the "famous" Martin Fowler article on DI, also talks about constructors and setters in DI frameworks)
Note for the reader: I wrote the last articles in English because I wanted to practice that language. But I think that is better to keep the blog posts in one language, and Spanish is my native language, that's why this will be my last post in English (at least in this blog).
26 julio 2011
Using embedded databases in unit tests
Using mock objects to simulate the database connection is impractical. A better option is to start and shutdown a embed database. For that matter pure Java SQL databases like HSQL DB are really lightweight, for example a unit test that reads resources from disk usually takes more time than one that uses HSQL in memory.
At the time of that blog post I wrote a small code snippet to start and shutdown HSQL. I used that code snippet many times, but never took the time to create a reusable library for it. Fortunately the Spring guys made it for me :)
Since Spring Jdbc 3 there is an interface to handle embed databases for testing.
If you use Maven, add the dependency to Spring Jdbc (you don't have to use the whole Spring framework if you don't want to) and to HSQLDB:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>3.0.5.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>hsqldb</groupId>
Start the embed database before your tests:
public static void setUpDatabase() {
sharedDatabase = new EmbeddedDatabaseBuilder()
.build()
}
hibernateSessionFactory.openSession(sharedDatabase.getConnection());
Shutdown the database after the tests:
@AfterClass
public static void tearDownDatabase() {
if (sharedDatabase != null) {
sharedDatabase.shutdown();
}
}
Usually starting the HSQL server is fast, so you can use @Before and @After (instead of @BeforeClass / @AfterClass).
That's all.
02 mayo 2011
Using Spring dependency injection without XML
Having said that, let’s go with the blog post…
It’s difficult to define what the Spring Framework provides, because is not a framework but a collection of them. The included frameworks covers pretty much everything from dependency injection (DI) to the creation of web applications. All of them designed to work seamlessly with the DI framework, and in particular with the XML way of configuring DI.
By using dependency injection you don’t need to modify your model to use Spring: you depend only on the DI framework to get the root objects of your application. At “lower levels” you can use some abstractions provided by the Spring Framework like JdbcTemplate or JmsTemplate, but you can use them without the DI framework too: they are part of the “Spring framework collection”, but they can work as independent frameworks.
Sadly in my experience, it seems that most of the developers are not aware of that: a lot of projects are too coupled with the Spring XMLs (which is only one convenient way of configuring DI), to the point that there are also dependencies to the “application-context.xml” in the unit tests. This is really bad. Spring XML is easy to write, but is a pain to maintain: it lacks abstraction mechanisms, you can’t debug it, is difficult to refactor and to find where the things are.
An alternative is to use Google Guice for the dependency injection part: It contains a nice API to define instance dependencies in code. You can use it with other Spring things, but I never feel comfortable to do that, because I think that mixing both frameworks could be a little bit confusing.
Spring DI can be configured by code, but to do it you have to pass some barriers:
- All the documentation refers to the XML configuration.
- The API to do the configuration directly in Java is awful.
Understanding the Spring API
The base interface to get instances from the DI injection framework is BeanFactory. The “Bean” prefix is there for historical reasons, but not be confused by it: your object classes doesn’t need to comply with the JavaBeans specification.
But to use a BeanFactory instance you need to configure it in some way, that’s why exists other interface called BeanDefinitionRegistry. This interface defines methods to register bean definitions (instances of BeanDefinition) that indicates how to create an instance.
To summarize:
- BeanFactory: provides bean instances by name (getBean)
- BeanDefintionRegistry: associates bean names with bean definitions (registerBeanDefinition).
- BeanDefinition: provides information on how to create an object instance.
Design side note: To me Spring is unnecessary complex in this part. The BeanDefinition provides information but it doesn’t do the instantiation, it means that each BeanFactory has to do that work (see AbstractBeanFactory.doGetBean). I think that if Spring delegates the instantiation to the definition, a lot of things could be simplified and the API becomes easier to use directly from Java.
If you used Spring before, you probably heard the name “ApplicationContext”. This is because the usual ways to get an implementation of BeanFactory is by using a ClassPathXmlApplicationContext or a XmlWebApplicationContext. That classes also implements other interfaces (i.e. ApplicationContext) that allows to plug-in lifecycle observers or enumerate beans (I assume that those functionality is required by other more “advanced” features provided by the framework).
Creating a BeanFactory instance for testing purposes
Before explaining how to do that… I want to give you an advice:
If you need to pass a Spring “ApplicationContext” or a “BeanFactory” in a lot of tests, you need to review your design and/or the way that you are testing. Maybe you are putting direct dependencies with the Spring context when all you need is to pass the required instances directly or maybe you are instantiating too much just to create an unit test.
After that short advice, here are some small code examples:
You can define beans by code, sadly the API is really ugly: BeanDefinitionBuilder provides you with methods to define beans (Ey, Spring guys! A good internal DSL to do this will be nice in the next release of Spring):
final StaticApplicationContext context = new StaticApplicationContext();
final BeanDefinitionBuilder builder =
BeanDefinitionBuilder.genericBeanDefinition(MockDataSource.class);
context.registerBeanDefinition("dataSource", builder.getBeanDefinition());
new MyApplication(context).run(); // MyApplication(BeanFactory context)
Using StaticListableBeanFactory you can pass instances, instead of using bean definitions (really useful to do tests):
final StaticListableBeanFactory context = new StaticListableBeanFactory();
context.addBean("dataSource", new MockDataSource());
new MyApplication(context).run();
By using a GenericApplicationContext you can combine XML from different sources with beans defined in code, for example:
GenericApplicationContext context = new GenericApplicationContext();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(context);
// this xml define the contentManager bean
reader.loadBeanDefinitions(new ClassPathResource("spring-config.xml"));
context.registerBeanDefinition("dataSource",
BeanDefinitionBuilder.genericBeanDefinition(MockDataSource.class)
.getBeanDefinition());
context.refresh();
// contentManager uses dataSource
System.out.println(context.getBean("contentManager"));
Ok that’s all for this time :)
18 abril 2011
Java coding best practices
Instead of giving them a big document that nobody is going to read, I preferred to make a small list of good practices - and try to fit them in a A4 page.
I'm not a big fan of "best practices" documents. I know that they are good to standardize the work and share knowledge. But there is a "dark side": a "best practice" depends on a particular context. So is good to understand why a practice was good and distinguish when there are better options; in the end you can't get blood out of a stone: the best way to produce good quality systems is to work with good professionals.
So this is the list of best practices that I made (in no particular order):
Choose Meaningful Names
- Classes:
If a class represents a concept of the problem domain, use a well known name for that domain (the technical analyst/product owner is our friend here).
Forget the name, look at the class responsibilities and them choose a name based on that (CRC helps).
Using suffixes like "Data", "Info", "Object"; doesn't add information and can be confusing ie. What is the difference between Customer and CustomerInfo?
- If you still don't known how to name something and you invent a new name: communicate that to your team, and be sure that everybody agrees to name "the thing" in the same way.
- When you start to feel uncomfortable with a method or class name and you have a better name, don't heistate: rename it.Write short methods
- Remember the "Extract method" refactoring shortcut keys, you are going to use them a lot.
- Methods from comments: usualy in a long method programmers start to use comments to explain parts of the code, create a new method instead of using comments in that way.
- If you can't do an extract method because you end with lots of parameters or you have some obscure dependency; is a sign that something is wrong with the design. Sometimes using a "Method Object" to fix that helps to find what is missing.Avoid undesired side-effects (use immutable objects as much as possible)
- Avoid mutable global state, ie: non-final static variables or "singleton" like globals (see "Avoid Singletons").
- Try to use constructors with arguments instead of a combination of "default constructor/setters" (this is related with "Avoid object instances in invalid state" and "Avoid nulls")-
- Be careful when returning collections: most of the Java collections are mutable, returning those could expose your object internal state.Avoid object instances in an "invalid state"
- Declare required instance variables as final, and initialize them in the constructor.
- Use good defaults.
- If you end with a constructor with lots of parameters: refactor the code, maybe you can group of those parameters in an object.
- If you need to build a complex instance, you can create a builder (but never put the validations in the builder, put them in the constructor).Avoid nulls
- You can use the Null Object pattern
- Use good defaults instead of nullUse singletons ONLY to represent unique objects
- A lot of programmers use the singleton pattern to point to globally well known objects, this combined with mutable objects is a time bomb: lots of undesired side effects and code that could have concurrency problems.
- If you need a "setInstance" method for testing purposes: is not a singleton!
- Usually "Null Objects" can be singletons, but most of the time singletons are not needed.
- Singletons should be immutable.Classes should have one responsibility - one reason to change
- Using the CRC method sometimes helps to think in single responsibility classes.
- Classes with many responsibilities tend to be difficult to test with unit tests; so a hard to write unit test could be a sign that you are dealing with too many responsibilities.Write unit tests like specifications
- In TDD the most confusing part of unit test is the word "test". If you think in tests like in small specifications of that your code supposes to do, then is very easy to write tests first.
- Don't put to many assertions in one test method, is better to have only one or two assertions per unit test method. Why? You'll start working faster with the TDD cycle and if something fails is easy to see what's happening.Use exceptions instead of return codes
- If you "Avoid Nulls" you will find this straight forward.
- Think in exceptions in a layered way, and handle them in the place that you could do something about it.
- Don't hide exceptions with empty catch blocks: if you have a bug it will be very difficult to find.
- Never catch Throwable: because you can catch sub-classes of Error too.When creating polymorphic class hierarchies: remember the substitution principle
- If you have a method that is overridden by different subclasses: avoid coupling between the type of the method argument and specific subclasses.
- If you need to cast an argument, maybe you are not following the substitution principle, take account that even if the "subtypes" are ok to the compiler you will not be able to change implementations easily.Optimize only if it's necessary and with the help of a profiler
- Applying optimization tips blindly... tend to create wrong assumptions, and sometimes code that is difficult to understand.
- A linear search in a collection with a bounded number of elements is O(1) (even when the max number of elements is 100, 1000 or 10000)
- I/O is orders of magnitude more expensive than CPU/Memory tasks.
- Generational Garbage Collectors like the ones in Java are optimized for short lived objects, it means that if you need to create objects to write more understandable code: create them. Early optimizations in object instance creation could cause other problems in concurrency and impact in the GC time; optimize object creation ONLY if you conclude that is necessary after using a profiler.
23 febrero 2011
Reingeniería de software (2da parte)
Moose esta compuesto de varios frameworks, de los cuales voy a mencionar los que creo principales para la tarea de design recovery:
- FAMIX*: es un modelo que representa entidades de código y sus relaciones (para su posterior análisis estático). * En la documentación y papers que lei sobre Moose se me hizo un tanto confusa la diferencia entre FAME y FAMIX. Por lo que pude entender FAMIX es el nombre metamodelo y FAME la implementación en Moose para el intercambio de estos meta modelos.
- Moose Core: trabaja sobre el meta modelo de FAMIX permitiendo hacer consultas usando Smalltalk.
- Mondrian: es un framework de visualización, que facilita la creación de vistas polimetricas (vistas polimetricas? más a continuación).
Hasta acá todo muy lindo con la lista de herramientas, pero... ¿Cómo me ayudan a entender un sistema?
Cuando vi Moose por primera vez me hice la misma pregunta. En realidad, como no estaba familiarizado con las herramientas de design recovery mi pregunta fue mucho más básica: ¿Para que sirve Moose?
Si bien hay un libro online (estilo wiki), no me quedaba claro como se usaba la herramienta hasta que leí la tesis de doctorado de Michele Lanza (es especial el capitulo 4).
Para quienes quieran buscar ejemplos de como usar las herramientas de Moose, les recomiendo leer esta tesis. Y para los que no, les cuento brevemente de que trata.
Como mencione en el post anterior el problema principal de la reingeniería no es hacer el rediseño (o refactoring), si no entender lo que hay. Y como los sistemas suelen ser enormes, es necesario generar y visualizar de alguna manera información de resumen que ayude a entender las cosas a alto nivel, o al menos guiarnos hacia los lugares de importancia.
Con este objetivo Lanza trabajo en un tipo de visualización llamado vistas polimetricas que mezcla entidades de software y sus relaciones, junto con métricas de software. (en este articulo o en el libro Object-Oriented Metrics in Practice pueden encontrar una explicación detallada de las vistas polimetricas).
La idea de mezclar métricas en la visualización, es la de poder identificar visualmente posibles candidatos a mirar. Acá van algunos ejemplos:
System Hotspots View
Da una idea general del tamaño del sistema. En este gráfico cada rectángulo representa una clase, y su tamaño esta dado por el número de métodos. (Los cuadrados grises son meta clases, esto se hizo sobre un sistema en Smalltalk, en general tener muchos métodos en la meta clase es un indicador que están faltando abstracciones adecuadas -es casi como tener muchos métodos static en Java):
System Complexity View
Muestra las relaciones de herencia entre clases y los tamaños de cada una de ellas (ancho: número de atributos, alto: número de métodos).
Inheritance Classification View
Las dos vistas anteriores fueron un ejemplo de lo que Lanza llama "first contact" views, es decir sirven para tener un pantallazo muy general, en cambio esta vista se aplica a una jerarquía de clases.
La forma de árbol muestra relaciones de herencia, el ancho de las cajas la cantidad de métodos agregados, el alto la cantidad de métodos sobre-escritos y el color (en escala de grises) la cantidad de métodos sobre-escritos que llaman a super.
Estos fueron algunos ejemplos, hay más vistas utilizadas para obtener información a distintos niveles de detalle. Cabe aclarar que la idea es que estas herramientas de visualización sean interactivas (ej. pasar el mouse sobre cada caja nos dice la clase), de nada sirve tener cajas de colores si no sabemos de que parte del sistema nos esta hablando.
¿Donde entra Moose en todo esto? Moose es un framework que permite crear estas vistas y explotar la información. "Out of the box" no es una herramienta automática para hacer una evaluación del sistema, si no más bien un framework, que se puede automatizar (usando Smalltalk) e integrar a sistemas de integración continúa (como Hudson o mejor dicho Jenkins).
Nota: Moose esta hecho en Smalltalk, pero puede analizar sistemas en Java usando un conversor de Java al formato FAMIX.¿Existen otras herramientas de este estilo?
Mas o menos. Moose es un framework general, lo más parecido a un framework general de este tipo es una vieja herramienta llamada Rigi (que ya parece estar obsoleta - al menos no se actualiza desde hace mucho).
Si existen sistemas que proveen visualizaciones particulares, por ejemplo CodeCity es un software muy interesante que lee modelos de FAMIX y realiza vistas basadas en métricas pero utilizando una metáfora de ciudad en 3d, por ejemplo la siguiente es una representación del JDK (versión 1.5) realizada con CodeCity
Si mal no recuerdo: alto=número de métodos, ancho=número de atributos, los colores varían según lo que se quiera ver por ejemplo se pueden utilizar para resaltar "design disharmonies" -otro término para "code smells". Las "manzanas" de la ciudad representan paquetes.
13 abril 2010
Composición
La intención del ejercicio era que los alumnos piensen en como utilizar objetos para lidiar con la complejidad de aplicar las diferentes ofertas.
Esperaba también que se dieran cuenta que el precio de un producto depende de factores como: la fecha, el "mercado", ofertas u estrategias de negocio.
Imagine que comenzarían planteando la clase Producto con un "estilo base de datos":
Y que a lo largo del ejercicio iban a llegar a una especie de "lista de precios":
Sin embargo no fue como esperaba. Para uno de los alumnos la expresión "el producto tiene un precio" significaba que el un producto tenia que responder directamente su precio. Trate de justificar mi posición desde distintos puntos de vista, pero luego abandone la discusión.
Nota al margen: Mi experiencia dando clases me enseño que es mejor no entrar en discusiones sin salida. Es preferible escuchar pacientemente y expresar cuales son los problemas del planteo desde distintos puntos de vista, con suerte la discusión evoluciona y se aprende mucho en el proceso. Pero si la discusión se estanca, es preferible abandonarla.
Escribo sobre este ejemplo después de tanto tiempo, por que veo que el problema de como expresar la relación de composición se repite en otros contextos.
Por ejemplo ¿Cómo modelarían que "un cliente que tiene contactos"? Una forma de hacerlo es:
Sin embargo, al hacer la traducción literal de este diagrama de clases, el modelo esta diciendo: "el cliente esta compuesto de contactos". La diferencia parece sutil, pero a medida que el sistema crece tiene consecuencias importantes:
- Agregar nuevos contactos implica modificar la instancia de cliente y trae consecuencias en la forma de asegurar que se cumplan las reglas de negocio, por ejemplo: que un cliente cambie su numero CUIT es raro y puede afectar a los sistemas de facturación; pero es esperable que sus contactos cambien seguido sin mayores consecuencias para el resto del sistema, entonces hay que distinguir entre "modificaciones pesadas" y "modificaciones livianas".
- Transmitir el cliente por "el cable" implica transmitir sus contactos, y como esta información puede ser pesada uno tiene que usar alguna estrategia como un objeto que represente una vista del cliente sin sus contactos (DTO), o bien hacer que la lista de contactos se cargue por demanda (lazy load).
Nota al margen: Creo que uno de los causantes de confusión es el uso del verbo tener. En nuestra manera de expresarnos es natural decir "el producto tiene un precio", sin embargo el "tiene" de nuestro lenguaje no expresa necesariamente una relación de composición. Erich Fromm, escribió un libro muy bueno llamado "Tener o Ser" que habla sobre la influencia del "tener" en la sociedad. Es curioso como el consumismo cambio nuestras formas de expresión, por ejemplo en lugar de decir "siento un dolor de cabeza" decimos "tengo un dolor de cabeza".
En cierto sentido el problema es similar al de Producto-Precio, una alternativa para expresar la relación del cliente con sus contactos es tener una "agenda de contactos" mediante la cual se pueden encontrar los contactos para un cliente:
Creo que pensar que un modelo es correcto por que expresa "la realidad" mejor que otro, sufre del problema de dar por sentado que nuestra percepción y traducción al sistema es absolutamente correcta: para uno de mis alumnos la realidad que "el producto tiene un precio", se traducía correctamente en "unProducto.getPrecio()".
Por eso es necesario tener en cuenta cuestiones que afectan a la percepción y traducción del problema:
- El verbo "tener" tiene un uso extremadamente ambiguo en nuestra cultura, por ejemplo es más adecuado decir que a un producto se le asigna un precio y quizás de esa forma ya vemos el dominio con otra perspectiva.
- Los "ritmos" de cambio pueden hacer que sea más conveniente pensar en objetos distintos. Por ejemplo el precio varia con el tiempo y el entorno, pero el producto no; algo similar ocurre con el cliente y sus contactos.
Separar las cosas que cambian a distintas velocidades ayuda a evitar side effects innecesarios y simplifica la aplicación de reglas de negocio. - Ver si existe en el dominio un intermediario en la relación, como por ejemplo una "lista de precios". Es claro que el software es un medio distinto: la lista de precio existe físicamente por que es una herramienta para definir y recordar el precio de un producto, y ese rol puede cumplirse en software sin necesidad de tener una "lista de precios" propiamente dicha. Sin embargo explorar esas analogías puede servir para ver conceptos del dominio.
10 diciembre 2009
Test driven development... algunas lecciones aprendidas
TDD != Testing en busca de bugs
Es muy común confundir TDD con testing: al menos comparten la palabra "test" en el nombre.
La diferencia esta en que uno generalmente usa la palabra "testing" para referirse a la búsqueda de bugs. Mientras que en TDD la intención es otra: uno busca agilizar el diseño, facilitando la incorporación de cambios.
A grandes rasgos TDD consiste en expresar con un programa las "expectativas" sobre lo que se va a desarrollar.
int main() {
printf("Hello World\n")
return 0;
}
Compilamos...
hello.c: In function ‘main’:
hello.c:5: error: expected ‘;’ before ‘}’ token
Arreglamos código, compilamos y ejecutamos, hasta que vemos en la pantalla el resultado esperado:
La diferencia usando TDD es que primero escribimos un programa para verificar el resultado esperado (algo que comúnmente hacemos de forma manual):
verificar(salidaDePrograma, "Hello World");
Nuestro "test", no intenta buscar bugs probando diferentes casos, o casos "borde" que puedan ser problemáticos.
Si no que simplemente es una forma de establecer que es lo que queremos hacer.
Esta diferencia es sutil pero tiene muchas implicancias en la forma de trabajar:
- No es necesario pensar casos de test complejos, solo basta con pensar que es lo que se quiere hacer.
- Con TDD el ciclo de escribir código - compilar, se transforma en: escribir tests/código - compilar/ejecutar tests.
Es decir, crear/ejecutar tests debe ser parte común del desarrollo. Si los tests tardan en ejecutarse se transforman en una carga.
En mi caso los "tests" usados de esta forma se convierten en una especie de "TODO list". En efecto muchas veces siento que si no tengo planteado un "test" (aunque sea en mi cabeza) no sé por donde empezar.
¿Vale la pena dedicar tiempo a escribir tests?
El ejemplo de "Hello World" muestra una cuestión evidente: programar el test puede ser mucho más difícil que hacer el programa a testear.
Cabe preguntarse si es siempre así, y si el trabajo adicional vale la pena.
En general no siempre es tan difícil escribir tests, y hay factores que influyen en la dificultad:
- Componentes y frameworks "acoplados" a sistemas externos.
Por ejemplo, si quisieran escribir el test del "Hello World" en C deberían capturar el STDOUT del programa. Suele ser fácil reemplazar la salida standard, aunque si el stream fuese parametrizable sería mucho más fácil. - Frameworks que no proveen interfaces separadas de a implementación.
Siguiendo con el "Hello World", supongamos que podemos parametrizar el stream de salida, sería ideal poder capturar la salida en un String.
Si el "Stream" que nos brinda el lenguaje no permite hacerlo, podemos hacer otra implementación que cumpla la misma interfaz y capture el String. Pero si "Stream" es una clase cerrada, estamos en problemas. - Diseños que no cumplen con el principio de Single Responsibility.
Si la "responsabilidad" de lo que queremos testear es acotada, entonces el test también va a ser más corto y fácil de escribir. - Dependencias globales mutables (como Singletons que no son "singletons").
En un post anterior ya hable sobre los problemas del estado global... así que voy a evitar la tentación de volver a comentarlos :) - Tests con grandes conjuntos de datos.
Complican la mantención del test y por lo general también hacen que la ejecución sea más lenta. - El lenguaje/tecnología a usar influyen en la forma de trabajo.
Por ejemplo en Smalltalk es muy común desarrollar mientras se ejecuta el test (completando lo que falta implementar en el debugger), ya que entorno permite modificar clases sin "parar el mundo". En Java este tipo de prácticas no es posible.
En otro extremo el lenguaje C++ no cuenta con reflection out-of-the-box, y por lo tanto escribir tests es mucho más trabajoso (hay que indicarle al framework explicitamente que tests ejecutar).
Es conveniente tener en cuenta la tecnología que se usa, por ejemplo en C++ intentería tener tests más grandes para evitar el "overhead" que implica crear tests nuevos. En Smalltalk, Java u otros lenguajes el entorno ayuda a que este problema no exista.
Desde un punto de vista de management, quizás la cuestión esta en cual es el "retorno de inversión" de lidiar con todo esto. Para mi el valor esta en que:
- Al tener tests automatizados uno tiene más confianza a la hora de hacer refactorings o cambios en el diseño.
Esto puedo afirmarlo con mi experiencia: trabaje en un sistema financiero donde teníamos una gran cantidad de tests (si no recuerdo mal, más de 10.000).
Tuve que hacer un rediseño importante de una parte "core" del sistema, hice los cambios y emepecé a ver los tests que fallaban.
Los tests que entendí, los arregle. Los que no, los consulte con sus autores y los arreglamos en conjunto. Todo este trabajo hubiese llevado muchísimo más tiempo sin tests automatizados.
- Ayuda a tener un diseño más abierto a los cambios
La razón es simple: cuanto más acoplado esta un objeto al uso de sistemas o frameworks externos, más difícil es testearlo. Por lo tanto con TDD uno tiende a diseñar de forma tal de disminuir este acoplamiento.
Lo mismo ocurre cuando los objetos tienen dependencias globales mutables: se vuelven dificiles de testear por que un test cambia el estado global, haciendo que otro test falle. Por lo tanto uno tiende a evitar este tipo de globales.
A la larga las consecuencias de usar TDD son beneficiosas para el diseño.
No todo es color verde...
Como mencione antes: la intención de TDD es facilitar cambios y ayudar en el diseño. No encontrar bugs.Por lo tanto aunque la aplicación pase los tests de unidad, puede contener errores funcionales (casos de test mal planteados), bugs por casos "especiales" y problemas de UI.
Volviendo al ejemplo de este sistema en el que trabaje con más de 10.000 tests de unidad:
Llego el día "D": empaquetamos todo para producción y llevamos el software al cliente.
A las horas recibimos un llamado: un botón de la UI permanecía deshabilitado y el cliente no podía acceder a una funcionalidad del programa (funcionalidad que estaba desarrollada, pero inaccesible). Asi que tuvimos que corregir el problema y hacer un nuevo deploy.
Los tests de unidad no reemplazan a un equipo de QA. No es la intención de TDD reemplazar las prácticas comunes de testing y verificación de calidad.
La diferencia esta en los detalles
Supongamos los convencí de las "bondades" de TDD. Asi que empiezan a practicarlo en un proyecto.Al tiempo hay una alta probabilidad de que empiece a pasar lo siguiente. Algunos tests fallan pero arreglarlos es un problema: los tests se volvieron enormes e inmantenibles. Solución rápida: los tests que fallan se ignoran. Un par de meses despúes los tests que fallan se siguen ignorando, y se pierde la ventaja de TDD: si necesitamos hacer un cambio de diseño no tenemos forma de saber que rompimos.
¿Por que se llega a este punto?
Principalmente es una cuestión de la costumbre del equipo en hacer y mantener los tests.
Pero además hay un montón de detalles que con el tiempo se acumulan cual bola de nieve e influyen en que los tests se vuelvan inmantenibles. Algunos "detalles" a tener en cuenta:
No depender de bases de datos externas
Hacer que un test de unidad requiera de una base de datos es problemático por que:- El entorno de desarrollo es dificil de configurar: cada desarrollador tiene que configurar los drivers y conexiones para poder empezar a trabajar con los tests.
- Si se usa una base compartida: un cambio hace que los tests fallen para el resto de los desarrolladores. Para hacer TDD los tests se deben correr todo el tiempo, asi que tener tests que fallen por cambios de otros es inadminsible.
- Es más trabajoso para usar un servidor de integración continua: hay que asegurarse de que el servidor de integración use correctamente la base de datos de prueba.
- La ejecución de los tests suele ser más lenta, asi que correr todos los tests no es algo que se haga muy seguido.
En el caso de que un test necesite una base de datos es necesario distinguir:
- Que es lo que se esta testeando: ¿Quiero realmente testear el acceso a la base de datos?
- ¿Puedo reemplazar el acceso a la base de datos, por una implementación que "simule" la respuesta y no acceda a una base de datos real?
Muchas veces uno quiere testear la implementación a la base de datos, por ejemplo se usa Hibernate para responder algunas consultas y cambiar la implementación por una que simule la base no aporta nada.
En esos casos lo mejor es usar una base en memoria (como HSQLDB) y levantarla durante el test.
HSQLDB es liviano, soporta Hibernate y toda la sintaxis de SQL. El unico punto en contra es que si uno quiere "ver" como se guardan las cosas en la base hay que frenar el test para no bajar el servidor.
El siguiente es un pequeño ejemplo de como usar HSQLDB en un test:
public static final String DEFAULT_DATABASE_NAME = "test";
public static final int DEFAULT_SERVER_PORT = 9001;
private Server hsqldbServer;
private HsqlProperties hsqldbProperties;
private String jdbcConnectionUrl;
public TestDatabase(String databaseName, int serverPort) {
hsqldbProperties = new HsqlProperties();
jdbcConnectionUrl = "jdbc:hsqldb:hsql://localhost:" + serverPort + "/" + databaseName;
hsqldbProperties.setProperty("server.port", serverPort);
hsqldbProperties.setProperty("server.database.0", databaseName);
hsqldbProperties.setProperty("server.dbname.0", databaseName);
}
public static TestDatabase startWithDefaultConfiguration() {
return new TestDatabase(DEFAULT_DATABASE_NAME, DEFAULT_SERVER_PORT).start();
}
private TestDatabase start() {
hsqldbServer = new Server();
hsqldbServer.setProperties(hsqldbProperties);
hsqldbServer.start();
return this;
}
public void shutdown() {
if (hsqldbServer == null) return;
hsqldbServer.shutdown();
}
}
private static TestDatabase database;
@BeforeClass
public static void startTestDatabase() {
database = TestDatabase.startWithDefaultConfiguration();
}
@AfterClass
public static void shutdownHsqldbServer() {
database.shutdown();
}
}
No usar herencia para compartir instancias de prueba
Usar herencia para compartir instancias entre tests es un error muy común.Supongamos que estamos testeando la implementación de CajaDeAhorro. Para crear una instancia de la caja de ahorros, necesitamos una instancia de Cliente, que a su vez necesita una instancia de Contrato, que a su vez necesita una instancia de... bueno se hacen a la idea: crear todas estas instancias es trabajoso, nos tomamos el trabajo :(
Despúes queremos testear CuentaCorriente, y queremos reusar las instancias que creamos para el test de CajaDeAhorro.
Muchas veces se suele crear una superclase para el test, supongamos AbstractTest, donde se colocan estas instancias que necesitamos compartir.
Los desarrolladores empiezan a heredar todos los tests de AbstractTest, y van "subiendo" instancias que quieren compartir entre tests.
¿Se ve el problema?
AbstractTest termina siendo una gran bolsa de gatos. Cuando se quiere refactorizar AbstractTest hay otro problema: es engorroso buscar las referencias a cada variable usada en las subclases. Por lo general este refactoring implica mucho trabajo y nadie lo hace. AbstractTest sobrevive a varias versiones transformandose en una inmensa bola de lodo.
La solución:
No usar herencia para compartir instancias entre los tests (usar herencia para re usar código es mala idea, en este caso es malisima).
Es preferible crear una clase, por ejemplo ClienteTestResource (suelo usar el sufijo "TestResource" para esas clases) que brinda instancias de Cliente para los tests.
Nada evita que ClienteTestResource no se convierta tambien en una bolsa de gatos, pero la cuestión es más controlada: uno puede crear clases distintas para agrupar recursos necesarios en los tests. Y a diferencia de AbstractTest, los desarrolladores solo usan ClienteTestResource cuando lo necesitan.
Evitar código redundante
Otro problema común es la actitud de: "hago copy & paste, total es un test!".Esto es perjudicial: los tests tambien hay que mantenerlos. Si hay que hacer un "copy & paste" para crear instancias que se necesitan en el test... usar un "TestResource".
Si hay que implementar un método en común para facilitar el testing: ¿Entonces por que no implementarlo en el modelo? Por ejemplo, si estoy testeando el balance de una cuenta y el codigo del test es algo asi:
...
private function Date crearFecha(String s) {
SimpleDateFormat sdf .....
return sdf.parse(s);
}
Y empiezo a copiar "crearFecha" en varios tests, la alternativa es dar una interfaz amigable en Cuenta (la otra alternativa es tener una interfaz más amigable para construir instancias de Date en general, un buen ejemplo de esto en Smalltalk es el framework Chaltén, que pemite expresar fechas como: "Jaunary first, 2009" donde Jaunary es un objeto first y , son mensajes... asi que la expresión es basicamente da una fecha) que permita escribir:
Eso no quita que el metodo balanceAl(Date) no tenga que existir. El formateo de fechas depende del Locale y no debería usarse con un Locale implicito en la aplicación. Sin embargo es conveniente pensar en la facilidad de uso de las interfaces que uno provee: las interfaces de los objetos son como las interfaces graficas.
Tener interfaces que permitan expresar las cosas de forma natural, usando defaults apropiados y simplificando ciertas cuestiones, ayuda muchisimo a la hora de escribir tests.
La clave es pensar que cuando uno programa esta construyendo un lenguaje, y esto abarca tambien a los tests. No es lo mismo escribir:
Que escribir:
for (String s : coleccion) {
if (s.equals("hola")) found = true;
}
assertTrue(found);
(Nota: sé que existe coleccion.contains("hola") pero quería explicitar la fealdad de esta alternativa ;) )
Un test por "observación"
Supongamos que para una CajaDeAhorro queremos testear que:- Los depositos incrementan el balance.
- Las extracciones decrementan el balance.
- No se puede hacer una extracción si no hay fondos.
Uno puede estar tentado a testear todo esto junto:
cuenta.depositar(monto);
assertThat(cuenta.balance(), is(monto * 2));
cuenta.extraer(cuenta.balance());
assertThat(cuenta.balance(), is(0));
try {
cuenta.extraer(monto);
fail();
} catch (NoHayFondosException e) {}
Pero tiene algunos problemas:
- Si el test falla es necesario debuggear para saber si el problema esta en depositar, extraer o balance.
- El ejemplo es chico, pero en tests más grandes los errores tienden ser más dificiles de ver.
En comparación esta solución es mejor:
assertThat(cuenta.balance(), is(0));
cuenta.depositar(monto);
assertThat(cuenta.balance(), is(monto));
}
@Test public void lasExtraccionesDecrementanElBalance() {
assertThat(cuenta.balance(), is(not(0)));
cuenta.extraer(cuenta.balance());
assertThat(cuenta.balance(), is(0));
}
@Test(expected=NoHayFondosException.class)
public void noSePuedeHacerUnaExtraccionSiNoHayFondos() {
assertThat(cuenta.balance(), is(not(0)));
cuenta.extraer(cuenta.balance() * 2);
}
Parece más largo pero tiene varias ventajas:
- Es más facil ver que esta fallando.
- Si un test falla es más facil de corregir.
- Tiene (para mi) una implicación psicologica: ir haciendo pequeños tests que van pasando, en general me ayuda a tener un mejor ritmo de trabajo. Incluso entre pequeños test a veces se me ocurren rediseños que no habia pensado originalmente.
Evitar tests no deterministicos
Cuando el resultado esperado depende de un valor del "entorno", por ejemplo: la hora actual o un número random. Es común terminar con tests no deterministicos: a veces pasan y otras veces no.Por ejemplo supongamos que estamos testeando una aplicación que genera registros de auditoria con la hora actual:
sistema.ejecutarMetodoQueGeneraAuditoria();
assertThat(sistema.logDeAuditoria(), hasItem(registroEsperado));
El problema es que si la comparación de RegistroAuditoria involucra comparar el "time stamp", entonces el test pasa o falla según la resolución de la hora y si hubo pausas en el medio de la ejecución (por ejemplo se ejecutó el GC).
Una forma de evitar este tipo de cosas es "fijar" estos valores. Por ejemplo podemos tener una interfaz Clock que provee la hora:
sistema = new Sistema(fixedClock);
registroEsperado = new RegistroAuditoria(nuevoTimestamp, etc, etc, etc);
sistema.ejecutarMetodoQueGeneraAuditoria();
assertThat(sistema.logDeAuditoria(), hasItem(registroEsperado));
Ya no hay más problemas de indeterminismo :)
Evitar delays
Muchas veces por razones de concurrencia, se termina agregando al test un "delay" (por ejemplo se ejecuta un thread y quiero esperar a que el thread actualize un valor que despues voy a verificar).Este tipo de test tiene dos problemas:
- El delay hace que los tests corran más lento.
- Por lo general resultan en tests no-deterministicos por que dependen de como la plataforma maneja la ejecución de los threads.
La alternativa es investigar un poco más lo que se esta testeando. Si es necesario tener el thread separado en el test conviene usar un semanforo (o monitor en Java) para hacer un wait hasta que se modifique el valor.
Lamentablemente no tengo ningun ejemplo a mano para mostrar en el blog.
Lo he hecho tanto en Smalltalk y en Java, y quizas lo unico que puedo agregar es que las cosas concurrentes son extremandamente dificiles de testear.
Algunas veces una solución intermedia es usar "yield" para que la VM ejecute otros procesos, esto salva el delay pero puede generar tambien tests no-deterministicos... la mejor opción sigue siendo usar algun tipo de semaforo.
De todas formas vale la pena hacer la "investigación" para evitar el delay: uno termina aprendiendo muchas cosas de concurrencia :), y tener un delay de un 1seg en varios tests es muy molesto.
Usar mock objects con cuidado
Un mock object es simplemente una implementación "de mentira" que se usa para reemplazar en un test a una implementación real (esta es al menos mi definición de mock object).En el ejemplo anterior FixedClock es una implementación de mentira de Clock, que nos facilita el testing.
Existen frameworks de mock objects que por lo general:
- Facilitan la implementación de mock objects usando meta-programación.
- Permiten hacer tests de caja blanca, pudiendo validar si se ejecuto o no un método del mock object.
En mi experiecia estos frameworks terminan derivando en tests dificiles de mantener:
- Los tests de "caja blanca" suelen ser una mala idea, al primer refactoring fallan y se convierten en un "dolor" de mantener. En esos casos es mejor re-plantearse el caso de test (y recordar cual es la intención de los tests en TDD).
- En general es mucho más facil tener una interfaz y hacer la implementación de mentira para el test que usar un framework de mock objects.
- Cuando no se generan dependencias adicionales (explicación a continuación) y no hay problemas como en el ejemplo de FixedClock, es más facil usar objetos reales que mock objects (usar un "TestResource" puede ayudar a escribir menos codigo en estos casos).
- En lenguajes dinamicos como Smalltalk es muchisimo mas facil crear mock objects, pero tambien tienen problemas: si se usan muchos mock objects y se hace un refactoring, es probable que los test compilen y pasen... aunque deberían haber fallado.
Un pequeño hint: usar mock objects no es necesariamente malo, pero necesitar de mucha logica en un mock object es signo de que algo esta mal. A veces no se necesita un mock object si no una implementación más sencilla de la "interfaz" que queremos simular.
Ser cuidadoso con las dependencias de los tests
Ya estoy llegando al final de este enorme post.El tema de las dependencias de tests requiere un poco más de explicación, asi que voy a ser breve:
Muchas veces uno expresa los tests a más alto nivel por ejemplo:
Este test abarca toda una historia de usuario y a veces es bueno automatizarlo.
Sin embargo hay que tener en cuenta que este tipo de test no solo es más largo, si no que posiblemente dependa de otras implementaciones: por ejemplo el carrito de compras se implementa en un proyecto, la generación de factura en otro y quien usa ambas implementaciones (llamemosle Cajero) usa interfaces asi que el proyecto donde esta el Cajero no depende de una implementacion particular de carrito de compra o el generador de factura.
¿Donde colocamos el test?
Si lo hacemos en el proyecto donde esta Cajero, y no usamos mock objects, generamos dependencias adicionales que no son necesarias (o quizas dependencias circulares que son problematicas). Y si usamos mock objects para todo, el test se vuelve más dificil de mantener.
Lo mejor en estos casos es ver el test a otro nivel. Si quieren pueden llamarle "test de integración" (siguiendo una convención que usabamos en Mercap, prefiero llamarles "user story test" por que abarcan una historia de usuario, y reservar el nombre "test de integración" para tests que verifican la integración con sistemas externos).
Es conveniente que este tipo de test este en un proyecto separado. De esta forma se evitan dependencias circulares, y es más facil compartir información entre dintintos "user story tests" (usando de "TestResources").
Además este tipo de test suele ser más lento, y por lo general no se ejecuta con la misma frecuencia que los tests de unidad.
El problema de este tipo de test es que son más grandes, tienen más dependencias y por lo tanto más dificiles de mantener.
Pero cuando las fechas de entrega apremian, a veces no hay tiempo para estar haciendo muchos tests a nivel unitario. En esos casos con tests de este estilo se puede testear la implementación manteniendo un estilo TDD (uno escribe el "user story test" antes de empezar), sin necesidad de crear un test por cada una de las clases que se usan "internamente". En este caso las herramientas de covertura pueden ayudar a ver que se cubre con el "user story test" y que no.
Si bien esta es una alternativa que complementa a los tests de unidad, no los reemplaza: los tests de unidad son una buena forma de trabajar a "nivel micro", pueden ejecutarse todo el tiempo durante el desarrollo y son más faciles de mantener.
Espero no haberlos dormido con este post enorme, trate de resumir un poco las "lecciones aprendidas" con TDD.
Hasta la proxima :)







