Mocks para trabajar con ficheros

71 views
Skip to first unread message

Xurxo Fresco

unread,
May 2, 2013, 5:55:05 PM5/2/13
to tdde...@googlegroups.com
 Hola a todos:

Desde que empezé con esto del TDD me tropiezo casi siempre con la necesidad de hacer dobles para trabajar con recursos de sistema: ficheros, la fecha del sistema, accesos a internet, etc. En la mayoría de los casos me basta con hacer un Fake que implementa una propidad o método pero en el caso de los ficheros la cosa se complica. Por ejemplo, si quiero probar que un método graba un archivo hago un doble de File y compruebo si se llama al método con los valores adecuados

// arrange
var File = new Mock<IFileWrap>();

// action
....
// assert
File
.Verify(f => f.WriteAllText(@"C:\titulo__.txt","blablabla"), Times.Once());

Haciendo esto siento que estoy probando la implementación, y no el comportamiento o la funcionalidad.

He pensado que quizá sea logico tener una especie de librerías de mock que simulen el sistema de ficheros completo. De echo, debería ya existir dicha libreria proque este problema lo deberian haber tenido cientos de desarrolladores antes. Pero no encuentro ninguna, exceptuando fs4net (https://github.com/toroso/fs4net), que no cumple todas mis necesidades.

¿Soy yo o le pasa a más gente? ¿Estoy sobredimensionando el problema? ¿Es una locura una libreria así o merece la pena hacerla?

Gracias de antemano por los comentarios

-- 
Xurxo Fresco
@xurxof

Carlos Ble

unread,
May 2, 2013, 5:59:54 PM5/2/13
to tdde...@googlegroups.com
Hola,
en esos casos limite yo simplemente hago tests de integracion. Algun unitario con un doble para comprobar la llamada al wrapper que escribe en disco y el resto con tests de contrato (integracion tipo fine-grained).

Saludos :-)


2013/5/2 Xurxo Fresco <xur...@gmail.com>

--
Has recibido este mensaje porque estás suscrito al grupo "TDDev" de Grupos de Google.
Para anular la suscripción a este grupo y dejar de recibir sus correos electrónicos, envía un correo electrónico a tddev-sp+u...@googlegroups.com.
Para publicar una entrada en este grupo, envía un correo electrónico a tdde...@googlegroups.com.
Visita este grupo en http://groups.google.com/group/tddev-sp?hl=es.
Para obtener más opciones, visita https://groups.google.com/groups/opt_out.
 
 



--
Carlos Ble
www.CarlosBle.com - @carlosble 
Check out my book on TDD: www.dirigidoPorTests.com/el-libro
Try my latest app for team productivity: www.liveTeamApp.com
Need test doubles for python?: www.pyDoubles.org

Diego Jancic

unread,
May 2, 2013, 6:09:50 PM5/2/13
to tdde...@googlegroups.com
No se si es lo adecuado o no, pero te paso uno que hice hace mucho para un proyecto.

Disclaimer: Lo hice hace mucho y para un sistema que no esta en produccion, asi que no doy fe de que ande. Creo que te puede servir para empezar y quizas cambiarlo.

Usa xUnit para los tests, si te gusta y lo vas a usar decime y lo pongo en Google Code. Quizas a alguien mas le sirva.

Abrazo!
Diego



2013/5/2 Xurxo Fresco <xur...@gmail.com>

--
AbstractFileSystem.zip

Angel Java Lopez

unread,
May 2, 2013, 6:12:37 PM5/2/13
to tdde...@googlegroups.com
Con respecto a los ficheros, depende del caso, pero en general voy y trabajo con ficheros.

Tal vez la alternativa que uso, es que la clase que tengo que implementar con TDD no graba un fichero, sino que graba un StreamWriter o algo parecido, y le paso un MemoryStream en test en varios de los tests.

La T de TDD, es de Tests. Con tal que la ejecucion de los tests sea rapida, y no interfieran en el flujo de trabajo, voy por el fichero.

Lo mismo hago con bases de datos (con la excepcion de que gran parte del desarrollo agil lo trato de hacer sin base de datos, solo la agrego en algun punto, no al principio)

Pero, pregunta, cual es el caso de uso para tus ficheros? Tal vez teniendo mas contexto, tendria otras opciones para sugerir.

Nos leemos!

Angel "Java" Lopez
@ajlopez



2013/5/2 Xurxo Fresco <xur...@gmail.com>

--

Alfredo Casado

unread,
May 2, 2013, 6:26:17 PM5/2/13
to tdde...@googlegroups.com

Como recomendacion general, no hagas mock de clases que no sean tuyas. Crea alguna abstraccion que tenga sentido en tu dominio y mockea esta abstraccion.

Luego puedes hacer test de integracion de la implementación de esta abstraccion o bien puedes hacer tests de integracion de grano mas grueso que funcionen contra ficheros reales, o puedes hacer las dos cosas al mismo tiempo, lo que te de mejores resultados.

Todavía me acuerdo cuando empezando con tdd mockeaba JDBC jeje, en aquel momento me parecía una idea estupenda... deberíamos fundar el club de "yo hize mocks del acceso a datos (o a ficheros)"

Xurxo Fresco

unread,
May 2, 2013, 7:02:15 PM5/2/13
to tdde...@googlegroups.com
Angel Java Lopez dixo o 03/05/2013 0:12:
Con respecto a los ficheros, depende del caso, pero en general voy y trabajo con ficheros.
Pero, pregunta, cual es el caso de uso para tus ficheros? Tal vez teniendo mas contexto, tendria otras opciones para sugerir.

Habitualmente los suelo usar para configuraciones sencillas y rápidas. En un proyecto más serio, para guardar info de paquetes de actualizaciones (permisos por cliente, hash de ficheros, versiones disponibles, changelogs, etc)

Gracias por los comentarios!
-- 
Xurxo Fresco
@xurxof

Angel Java Lopez

unread,
May 2, 2013, 7:12:35 PM5/2/13
to tdde...@googlegroups.com
Ah, bien, tengo mas contexto. No tengo claro tu segundo caso "guardar info de paquetes .. ", pero puedo imaginar mucho mas claro lo de "usar configuraciones sencillas y rapidas".

Lo que hago en esos casos, es

- La configuracion termina estando en un objeto
- En las primeras pruebas, paso ese objeto, armado en el Arrange de cada test, o suite de tests
- En algun punto, armo con TDD un lector de configuracion desde un archivo, probandolo con varios archivos, es decir, en el Act hago que el lector de configuracion consuma tal archivo, y en el Assert verifico lo que espero
- En algunos tests nuevos (o refactor de los viejos) paso a usar uno, dos o tres archivos de configuracion, que reflejen el estado de los casos de uso que estoy probando

Pero gran parte de los test simplemente se basan en configuraciones que quedan en objetos en memoria.

No se si tu caso de uso de "configuraciones" tambien pide cambiar y guardar

El tener en algun punto un lector de configuraciones desde archivo, me permite despues (cuando se necesite, no romper YAGNI), tener un lector de configuracion desde una base de datos, o desde un servicio web, o de un archivo encriptado

Angel "Java" Lopez
@ajlopez



JJG

unread,
May 3, 2013, 2:58:10 AM5/3/13
to tdde...@googlegroups.com

Hola a todos.

 

Se me ocurren dos alternativas:

1)      Si pruebas una funcionalidad que se apoya en grabar un fichero, es interesante buscar la manera de que no grabe el fichero real, por ejemplo pasándole algún fake (no mock) que guarde el contenido en un String o un stream y luego puedas verificar.

 

2)      Si pruebas una funcionalidad cuyo objetivo es grabar un fichero, lo más lógico es verificar si el fichero se ha creado. Esto es sencillo de hacer, por ejemplo, creando un assert que comprueba si el fichero existe o no.

Si el segundo punto enlentece mucho las pruebas, puedes poner estas pruebas en un paquete a parta (o utilizar funcionalidad experimental de JUnit para etiquetarlas en otro conjunto) y ejecutarlas menos a menudo que las otras.

 

En cualquier caso, más importante que la rapidez es probar lo que la funcionalidad verdaderamente debe hacer. Ten en cuenta que aunque verifiques la llamada correctamente, eso no significa que,  a la hora de la verdad, el fichero se cree adecuadamente.

 

Un saludo.

Xurxo Fresco

unread,
May 3, 2013, 7:34:31 AM5/3/13
to tdde...@googlegroups.com
Veo que hay múltiples opciones y que me toca elegir...

- Hacer test de integración + test con wrapper
- Hacer test que trabajen con ficheros reales, al menos cuando la funcionalidad es directamente grabar un fichero
- Crear mock (gracias Diego, deberías subir el código, nunca se sabe a quien le puede servir de inspiración )
- Crear abstracción y testar contra su interface  (Don't mock types you don't own)

Tengo que darle más vueltas, pero la ultima opción parece la mas acertada:
- permite TDD
- es sencilla
- parece un buen patron para aislar referencias externas,


Gracias a todos por los comentarios!!!

Xurxo
@xurxof

Alfredo Casado dixo o 03/05/2013 0:26:
-- 
Xurxo Fresco
@xurxof

Carlos Ble

unread,
May 4, 2013, 5:53:38 PM5/4/13
to tdde...@googlegroups.com

Hola,
Wrapper y abstraccion en este caso son lo mismo para mi. Wrapper es para encapsular y poder usar dobles :-)

Vicenç Garcia

unread,
May 5, 2013, 3:26:44 AM5/5/13
to tdde...@googlegroups.com
Buenas,

yo es lo que hago también. Si haces BDD (si trabajas con .Net mírate SpecFlow) los tests con los ficheros "de verdad" los puedes hacer en estos tests y por debajo vas trabajando con TDD y con abstracciones.

Como dice el dicho: "All problems in computer science can be solved by another level of indirection... except for the problem of too many layers of indirection." (http://en.wikipedia.org/wiki/Indirection) :-)


2013/5/4 Carlos Ble <ble.j...@gmail.com>

Xurxo Fresco

unread,
May 5, 2013, 4:17:27 AM5/5/13
to tdde...@googlegroups.com
Lo del BDD me queda un poco lejano (aun me cuesta formular historias de usuario!) pero guardo el consejo.

Si no me equivoco, este es el tipo de solucion del que hablamos, siendo IFileWrap un interface con los métodos que voy usando y que se sustiurá en producción por una instancia FileWrap que envuelve el Ssytem.File real :

[Test]
        public void Construct_IfFileNotExists_CreateNewDefaultValues() {
            // arrange
            var DefaultConfigStrings = new[] {"imapUser:pepe...@gmail.com", "imapPass:password", "imapHost:imap.gmail.com", "imapPort:993", "imapUseSSL:true", @"savePath:c:\mails\", "fromFilter:u...@blabla.com", "fromFilter:d...@blabla.com", "fromFilter:tr...@blabla.com"};
            var FileMock = new Mock<IFileWrap>();
            FileMock.Setup(f => f.Exists(@"c:\newslettersaver.cfg")).Returns(false);
            FileMock.Setup(f => f.WriteAllLines(@"c:\newslettersaver.cfg", DefaultConfigStrings));
            // action
            Config Cfg = new Config(@"c:\newslettersaver.cfg", FileMock.Object);
            // assert WriteAllLines call
            File.Verify(f => f.WriteAllLines(@"c:\newslettersaver.cfg", DefaultConfigStrings), Times.Once());
            Assert.AreEqual("pepe...@gmail.com", Cfg.ImapUser);
            Assert.AreEqual("password", Cfg.ImapPass);
            Assert.AreEqual("imap.gmail.com", Cfg.ImapHost);
Gracias de nuevo por vuestros comentarios.

Xurxo
@xurxof



2013/5/5 Vicenç Garcia <vincen...@gmail.com>



--
Xurxo Fresco

Vicenç Garcia

unread,
May 5, 2013, 4:21:19 AM5/5/13
to tdde...@googlegroups.com
El último File.Verify sería FileMock.Verify pero si, esa es la idea.


2013/5/5 Xurxo Fresco <xur...@gmail.com>

Vicenç Garcia

unread,
May 5, 2013, 4:24:41 AM5/5/13
to tdde...@googlegroups.com
De echo no haría falta declarar la interfaz, ya q solo la vas a utilizar para esa clase (YAGNI). Con declarar los métodos virtuales deberías tener suficiente.


2013/5/5 Vicenç Garcia <vincen...@gmail.com>

Alfredo Casado

unread,
May 5, 2013, 6:23:13 AM5/5/13
to tdde...@googlegroups.com
No veo cual es el sentido de crear un Wrapper que simplemente tenga los metodos que la propia clase File, no estas ganando gran cosa con este enfoque y tendras que tener una clase cuya única función es delegar en los métodos de File sin aportar nada más.

En este caso tu abstracción es Config, le podemos buscar un mejor nombre, en vista del test algo como ImapConfiguration estaría bien. Esta clase ImapConfiguration yo la probaría con ficheros reales y luego las clases que necesiten esta configuración simplemente reciben una instancia de ImapConfiguration en el constructor (Inversión de dependencias). Con esto puedes hacer test unitarios de cualquier otra clase que dependa de esta configuración sin problema, y la configuración en si que lo único que hace es leer de un fichero unos datos tiene en mi opinión muy poco sentido testearla sin ficheros reales.

Por otro lado, esta configuración es un value object (inmutable), en los test de las clases que usen esta configuración puedes simplemente instanciar una configuración con los valores que necesites, ni siquiera necesitas mockearla. 

Otra cosa, tampoco le pasaría en el constructor un path de fichero, este conocimiento que lo tenga internamente la clase ImapConfiguration (puede ser una constante, puede ser una variable de entorno) de esta manera tienes una abstracción que te da la configuración de Imap y listo, y será esa clase la que conozca los detalles de donde esta esa configuración, de si es un fichero, de si los datos del fichero se pueden sobreescribir por variables de entorno etc,etc.


Diego Güemes

unread,
May 5, 2013, 7:41:41 AM5/5/13
to tdde...@googlegroups.com
Buenas, un tema muuuuy interesante ;)

Personalmente creo que partiría en dos el test:
Uno que verifique la expectativa del mock. Aunque en este ejemplo tengo mis dudas, porque primero haces un stub del fichero y luego verificas contra el valor que le pusiste en el stub... no le veo mucho sentido ahí.

Otro que compruebe la transformación de los valores del array a tu objeto de configuración.

Quizás he dicho alguna barbaridad. Si es así, comentadlo por favor :)

Muchas gracias de antemano,
Diego.

Xurxo Fresco

unread,
May 5, 2013, 1:59:00 PM5/5/13
to tdde...@googlegroups.com
Aunque tu propuesta me parece interesante, el wrapper permitiría centralizar en un único punto los posibles fallos. Mediante test unitario se prueba que FileWrapper que se usa correctamente; en integración o con ficheros reales, que realmente hace lo que tiene que hacer.

X
urxo



2013/5/5 Alfredo Casado <casado....@gmail.com>

No veo cual es el sentido de crear un Wrapper que simplemente tenga los metodos que la propia clase File, no estas ganando gran cosa con este enfoque y tendras que tener una clase cuya única función es delegar en los métodos de File sin aportar nada más.

En este caso tu abstracción es Config, le podemos buscar un mejor nombre, en vista del test algo como ImapConfiguration estaría bien. Esta clase ImapConfiguration yo la probaría con ficheros reales y luego las clases que necesiten esta configuración simplemente reciben una instancia de ImapConfiguration en el constructor (Inversión de dependencias). Con esto puedes hacer test unitarios de cualquier otra clase que dependa de esta configuración sin problema, y la configuración en si que lo único que hace es leer de un fichero unos datos tiene en mi opinión muy poco sentido testearla sin ficheros reales.

Por otro lado, esta configuración es un value object (inmutable), en los test de las clases que usen esta configuración puedes simplemente instanciar una configuración con los valores que necesites, ni siquiera necesitas mockearla. 

Otra cosa, tampoco le pasaría en el constructor un path de fichero, este conocimiento que lo tenga internamente la clase ImapConfiguration (puede ser una constante, puede ser una variable de entorno) de esta manera tienes una abstracción que te da la configuración de Imap y listo, y será esa clase la que conozca los detalles de donde esta esa configuración, de si es un fichero, de si los datos del fichero se pueden sobreescribir por variables de entorno etc,etc.





--
Xurxo Fresco

Carlos Ble

unread,
May 5, 2013, 5:49:22 PM5/5/13
to tdde...@googlegroups.com
Hola,
Un test no es unitario si accede al filesystem: http://pragprog.com/magazines/2012-01/unit-tests-are-first

Saludos


2013/5/5 Xurxo Fresco <xur...@gmail.com>

--
Has recibido este mensaje porque estás suscrito al grupo "TDDev" de Grupos de Google.
Para anular la suscripción a este grupo y dejar de recibir sus correos electrónicos, envía un correo electrónico a tddev-sp+u...@googlegroups.com.
Para publicar una entrada en este grupo, envía un correo electrónico a tdde...@googlegroups.com.
Visita este grupo en http://groups.google.com/group/tddev-sp?hl=es.
Para obtener más opciones, visita https://groups.google.com/groups/opt_out.
 
 



--

Alfredo Casado

unread,
May 5, 2013, 6:18:56 PM5/5/13
to tdde...@googlegroups.com
Xurco:

"Mediante test unitario se prueba que FileWrapper que se usa correctamente".

¿El problema es que que es "usar correctamente FileWrapper"?, en tu test estas decidiendo que la forma correcta de escribir un fichero es utilizar el metodo "writeAllLines", imagina que descubres que usando el método "writeAllText" consigues un mejor tiempo de escritura, con tu enfoque tienes varias opciones:

- No cambiar los test y cambiar la implementación de FileWrapper para que llame a writeAllText cuando recibe la llamada a writeAllLines, extraño como poco.

- Ampliar la interfaz de la clase FileWrapper para añadir este método y cambiar tu test. Además ampliar el test de integración que tengas sobre la clase FileWrapper

Tal como yo te lo planteo, cuando tengas que cambiar un detalle de implementación en el acceso a ficheros cambias la clase ImapConfiguration y tus test no tienes que cambiarlos y te están sirviendo para comprobar que esa nueva implementación funciona. Por supuesto el test de ImapConfiguration es un test de integración no unitario, se trata de una clase de infraestructura pura y dura.

Los mocks son muy útiles para ayudarte a guiar tu diseño de forma incremental, como la clase File no es tuya y ya esta diseñada un mock sobre esta clase (o sobre un wrapper identico) no te ayuda a diseñar una mejor solución y estas creando test muy acoplados a decisiones puramente de infraestructura (como usar la interfaz de File es una decisión de infraestructura). 

Y fijate la duplicación que tienes que añadir en tu código:

class FileWrapper {
      
     def writeAllLines(....,....) {
             delegate.writeAllLines(...,...)
     }
     
     def anotherFileOperation(...) {
            delegate.anotherFileOperation(...)
    }
 
}

Una clase así es duplicación, no del tipo más común, pero duplicación al fin y al cabo. Una clase así para mi es complejidad gratuita, no añade nada a tu negocio y dificulta la comprensión del sistema y su capacidad de cambio. Para mi va claramente en contra de las reglas de diseño simple (de las 3 ultimas) "minimize duplicación, maximize clarity, has fewer elements".

Xurxo Fresco

unread,
May 6, 2013, 3:41:52 AM5/6/13
to tdde...@googlegroups.com

Por usar correctamente me refería a lo que Carlos Ble mencionaba como " Algun unitario con un doble para comprobar la llamada al wrapper que escribe en disco "

Hasta ahora veo que hay dos tendencias:
- delegar a las pruebas de integración que el archivo se escribe (o hacerlo en un test, sabiendo que no es un test unitario)
- abstraer el acceso a disco, hacer test unitario para verificar la llamada y en integración ver que efectivamente se graba

Sobre los test de integracion, contrato, etc. no tengo mucho conocimiento, pero entiendo que si se hacen test unitarios sobre las llamadas al files system se puede detectar posibles fallo durante las iteraciones de TDD (a costa de duplicar codigo o usar librerías preexistentes, eso hay que reconocerlo)

(Gracias a todos por vuestros comentarios. Se aprende un montón :D )

Xurxo

Alfredo Casado dixo o 06/05/2013 0:18:
-- 
Xurxo Fresco
@xurxof

Carlos Ble

unread,
May 6, 2013, 7:47:11 AM5/6/13
to tdde...@googlegroups.com
Hola!
Esos tests de integracion de ambito reducido que demuestran que las interfaces hacen lo que dicen, se llaman tests de contrato.

Saludos :-)


2013/5/6 Xurxo Fresco <xur...@gmail.com>

--
Has recibido este mensaje porque estás suscrito al grupo "TDDev" de Grupos de Google.
Para anular la suscripción a este grupo y dejar de recibir sus correos electrónicos, envía un correo electrónico a tddev-sp+u...@googlegroups.com.
Para publicar una entrada en este grupo, envía un correo electrónico a tdde...@googlegroups.com.
Visita este grupo en http://groups.google.com/group/tddev-sp?hl=es.
Para obtener más opciones, visita https://groups.google.com/groups/opt_out.
 
 

Xurxo Fresco

unread,
May 6, 2013, 9:52:49 AM5/6/13
to tdde...@googlegroups.com
Gracias por el enlace!!!

Tengo pendiente la lectura de la serie de artículos desde hace tiempo...  De hoy no pasa. (los otros 500 textos pendientes tendrán que esperar un día más)

Xurxo

Carlos Ble dixo o 06/05/2013 13:47:
-- 
Xurxo Fresco
@xurxof
Reply all
Reply to author
Forward
0 new messages