2011-02-12 7 views
19

Me gustaría comenzar a hacer más pruebas unitarias en mis aplicaciones, pero me parece que la mayoría de las cosas que hago no son adecuadas para la prueba unitaria. Sé cómo se supone que las pruebas unitarias funcionan en ejemplos de libros de texto, pero en aplicaciones del mundo real no parecen ser de mucha utilidad.Programas de pruebas unitarias que en su mayoría interactúan con recursos externos

Algunas aplicaciones que escribo tienen una lógica muy simple e interacciones complejas con cosas que están fuera de mi control. Por ejemplo, me gustaría escribir un daemon que reaccione a las señales enviadas por algunas aplicaciones y cambie algunas configuraciones de usuario en el sistema operativo. Puedo ver tres dificultades:

  • primero Tengo que poder hablar con las aplicaciones y ser notificado de sus eventos;
  • luego necesito interactuar con el sistema operativo cada vez que recibo una señal, a fin de cambiar la configuración de usuario adecuada;
  • finalmente todo esto debería funcionar como daemon.

Todas estas cosas son potencialmente delicadas: tendré que buscar posiblemente API complejas y puedo introducir errores, por ejemplo, al interpretar mal algunos parámetros. ¿Qué pueden hacer las pruebas unitarias por mí? Puedo simular tanto la aplicación externa como el SO, y verificar que dada una señal de la aplicación, llamaré al método API apropiado en el SO. Esta es ... bueno, la parte trivial de la aplicación.

En realidad, la mayoría de las cosas que hago implican la interacción con bases de datos, el sistema de archivos u otras aplicaciones, y estas son las partes más delicadas.

Para otro ejemplo, mira my build tool PHPmake. Me gustaría refactorizarlo, ya que no está muy bien escrito, pero me temo que haré esto ya que no tengo pruebas. Entonces me gustaría agregar algunos. El punto es que las cosas que puede ser roto por la refactorización no pueden ser capturados por las pruebas unitarias:

  • Una de las cosas que hacer es decidir qué cosas se van a construir y que uno ya están al día, y esto depende de la hora de la última modificación de los archivos. Esta vez en realidad es modificada por procesos externos, cuando se activa algún comando de compilación.
  • Quiero estar seguro de que la salida de los procesos externos se muestra correctamente. Algunas veces los comandos buikd requieren alguna entrada, y eso también debe ser manejado correctamente. Pero no sé a priori qué procesos se ejecutarán, puede ser cualquier cosa.
  • Alguna lógica está involucrada en la coincidencia de patrones, y esta puede parecer parte comprobable. Pero las funciones que hacen la coincidencia de patrones usan (además de su propia lógica) la función PHP glob, que funciona con el sistema de archivos. Si me burlo de un árbol en lugar del sistema de archivos real, glob no funcionará.

Podría seguir con más ejemplos, pero el punto es el siguiente. A menos que tenga algunos algoritmos delicados, la mayor parte de lo que hago implica la interacción con recursos externos, y esto no es adecuado para las pruebas unitarias. Más que esto, a menudo esta interacción es en realidad la parte no trivial. Todavía muchas personas ven las pruebas unitarias como una herramienta básica. ¿Qué me estoy perdiendo? ¿Cómo puedo aprender a ser un mejor probador?

+0

Vea también http://stackoverflow.com/questions/4980825/real-world-unit-tests – Raedwald

Respuesta

8

Creo que abre una serie de cuestiones en su pregunta.

En primer lugar, cuando su aplicación se integra con entornos externos como el sistema operativo, otros hilos, etc., debe separar (1) la lógica que está vinculada con el entorno externo y (2) su código comercial. es decir, lo que hace tu aplicación. Esto no es diferente de cómo separaría la GUI y el SERVIDOR en una aplicación (o aplicación web).

En segundo lugar, debe preguntar si debe probar la lógica simple. Yo diría que depende. A menudo es bueno tener pruebas de la funcionalidad de búsqueda/almacenamiento simple. Es como la base de su aplicación ... por lo tanto, es importante. Otras cosas de negocios construidas sobre su base que son muy simples, fácilmente puede encontrarse sintiendo que está perdiendo el tiempo, y más que nada :-)

En tercer lugar, refactorizar un programa existente y probarlo en su estado actual puede ser un problema Si su programa PHP produce un conjunto de archivos sobre la base de alguna entrada, bueno, tal vez ese sea su punto de entrada a las pruebas. Seguro que las pruebas pueden ser de alto nivel, pero es una manera fácil de garantizar que después de la refactorización, su programa produzca el mismo resultado. Por lo tanto, apunte a pruebas de mayor nivel en esa situación en la fase de inicio de sus esfuerzos de refactorización.

Me gustaría recomendar algo de literatura, pero solo puedo proponer un título. "Trabajar eficazmente con código heredado" Por Micheal Feathers. Es un buen comienzo. Otro sería "Patrones de prueba de xUnit: código de prueba de refactorización" de Gerard Meszaros (aunque ese libro es mucho más descuidado y LLENO de texto de copiar y pegar).

+1

+1: Si el programa depende de material externo, simplemente simule esas cosas externas en las pruebas de su unidad separando el I/O código de la lógica y prueba la unidad lógica. Las pruebas unitarias no deben brindarle una cobertura de errores del 100%, ningún método de prueba lo hace. Simplemente no te molestes, haz lo mejor que puedas. –

+0

Sí, así es cómo lo programaría en primer lugar: Primero, la lógica funciona, luego maneje la integración con el entorno externo. –

+0

Esta es una buena respuesta. Particularmente: si tiene algún código altamente acoplado que le gustaría someter a prueba, primero escriba algunas pruebas funcionales/de extremo a extremo que verifiquen las entradas iniciales con las salidas esperadas. Luego se forma una prueba de regresión que le da confianza para abordar el desacoplamiento dentro sin romper el comportamiento general. Iterar ese enfoque, agregando más pruebas de comparación de entrada-salida según sea necesario. – Ben

2

"Pruebas unitarias" prueba una unidad de tu código. No se deben involucrar herramientas externas. Esto parece ser complicado para su primera aplicación (sin saber mucho al respecto;)) pero el phpMake se puede probar por unidades, estoy seguro ... porque hormiga, gradle y maven también se pueden probar por unidad;)!

Pero, por supuesto, también puedes probar tu primera aplicación automatizada. Hay several different layers uno podría probar una aplicación.

Así que la tarea para usted es encontrar una forma automática de probar su aplicación, ya sea prueba de integración o lo que sea.

E.g. ¡podrías escribir scripts de shell, que afirman alguna salida! Con eso, asegúrese de que su aplicación se comporta correctamente ...

4

En cuanto a su problema sobre bases de código que no están cubiertos actualmente por pruebas en las que le gustaría empezar refactorización existente, sugeriría la lectura:

Working Effectively with Legacy Code por las plumas Micheal.

Ese libro le brinda técnicas sobre cómo tratar los problemas que podría enfrentar con PHPMake. Proporciona formas de introducir costuras para la prueba, donde antes no había ninguna.


Además, con el código que toca decir que los sistemas de archivos, puede abstraer el sistema de archivos llama detrás de una envoltura delgada, con el adapter. Las pruebas unitarias estarían en contra de una implementación falsa de la interfaz abstracta que implementa la clase de ajuste.

En algún momento, llega a un nivel suficientemente bajo donde una unidad de código no se puede aislar para la prueba unitaria, ya que depende de la biblioteca o llamadas API (como en la implementación de producción de la envoltura). Una vez que esto sucede, las pruebas de integración son realmente las únicas pruebas de desarrollo automático que puede escribir.

1

Las pruebas de interacciones con recursos externos son pruebas de integración, no pruebas unitarias.

Las pruebas de su código para ver cómo se comportaría si se hubieran producido interacciones externas particulares pueden ser pruebas unitarias. Esto se debe hacer escribiendo su código para usar inyección de dependencia, y luego, en la prueba de la unidad, inyectando objetos de prueba como dependencias.

Por ejemplo, considere una pieza de código que añade los resultados de una llamada a un servicio a los resultados de una llamada a otro servicio:

public int AddResults(IService1 svc1, IService2 svc2, int parameter) 
{ 
    return svc1.Call(parameter) + svc2.Call(parameter); 
} 

Esto se comprueba mediante introducción de objetos simulados para el dos servicios:

private class Service1Returns1 : IService1 
{ 
    public int Call(int parameter){return 1;} 
} 

private class Service2Returns1 : IService2 
{ 
    public int Call(int parameter){return 1;} 
} 

public void Test1And1() 
{ 
    Assert.AreEqual(2, AddResults(new Service1Returns1(), new Service2Returns1(), 0)); 
} 
+0

El punto que estoy planteando es que la adición es una operación bastante trivial en comparación con la conexión a estos servicios, por lo que en realidad está probando la única parte de la que puede estar seguro de que funciona. – Andrea

+0

@Andrea: eso fue solo un ejemplo. Lo hice para colaboraciones de servicios mucho más complicadas. –

+0

@Andrea: el otro error que pareces estar cometiendo es el código de prueba unitaria que ya está escrito. En su lugar, utilice desarrollo basado en pruebas, y no tendrá ningún código que no tenga pruebas unitarias. –

2

Lo recomiendo google tech-talk on unit testing.

El vídeo se reduce a

  • escribir el código para que sepa tan poco acerca de cómo se va a utilizar como sea posible. Cuantas menos suposiciones haga tu código, más fácil será probarlo. Evite la lógica compleja en constructores, el uso de singletons, miembros de clase estáticos, etc.
  • aísle su código del mundo externo (comunicaciones, bases de datos, tiempo real) y asegúrese de que su código solo se comunica con su capa de aislamiento. De lo contrario, escribir pruebas será una pesadilla en términos de configuración de "entorno falso".
  • pruebas de la unidad deben probar historias; eso es lo que realmente entendemos y nos importa; dada una clase con un método foo(), testFoo() es poco informativo. En realidad recomiendan nombres de prueba como itShouldCloseConnectionEvenWhenExceptionThrown(). Idealmente, sus historias deberían cubrir suficiente funcionalidad para que pueda reconstruir las especificaciones de las historias.

NOTA: el video y esta publicación usan Java como ejemplo; sin embargo, los puntos principales representan cualquier idioma.

1

En primer lugar, si las pruebas unitarias no parecen ser de mucha utilidad en sus aplicaciones, ¿por qué desea comenzar a hacer más? ¿Qué te motiva a preocuparte por eso? Definitivamente es una pérdida de tiempo si a) haces todo perfecto la primera vez y nada cambia alguna vez ob) decides que es una pérdida de tiempo y lo haces mal.

Si realmente cree que realmente desea realizar pruebas unitarias, la respuesta a sus preguntas es la misma: encapsulación. En su ejemplo de daemon, podría crear un ApplcationEventObeservationProxy con una interfaz muy estrecha que simplemente implemente métodos de paso. El propósito de esta clase es hacer nada pero encapsula completamente el resto de su código de la biblioteca de observación de eventos de terceros (nada significa nada, no hay lógica aquí). Haga lo mismo para la configuración del sistema operativo. Entonces puede probar la unidad por completo la clase que hace acciones basadas en eventos. Yo recomendaría tener una clase separada para el daemon-ness que solo envuelve tu clase principal, hará las pruebas más fáciles.

Hay un par de ventajas de este enfoque fuera de las pruebas unitarias. Una es que si encapsula el código que interactúa directamente con el sistema operativo, es más fácil desactivarlo. Este tipo de código es particularmente propenso a la rotura fuera de su control (es decir, parches MS). También es probable que desee admitir más de un sistema operativo, y si la lógica específica del sistema operativo no está enredada con el resto de su lógica, será más fácil. El otro beneficio es que te verás obligado a darte cuenta de que hay más lógica comercial en tu aplicación de lo que piensas.:)

Por último, no olvide que las pruebas unitarias son la base de un buen producto, pero no el único. Tener una serie de pruebas que explore y verifique las llamadas a la API del sistema operativo que utilizará es una buena estrategia para las partes "difíciles" de este problema. También debe tener pruebas de extremo a extremo que garanticen que los eventos en sus aplicaciones causen cambios en la configuración del sistema operativo.

0

Como otras respuestas sugeridas Trabajar eficazmente con Legacy Code Por Micheal Feathers es una buena lectura. Si tiene que lidiar con el código heredado y desea asegurarse de que la interacción del sistema funcione como se espera, intente primero escribir pruebas de integración. Y luego es más apropiado escribir pruebas unitarias para probar el comportamiento de los métodos que se valoran desde el punto de vista de los requisitos. Sus pruebas tienen un propósito completamente diferente al de las pruebas de integración. Es más probable que las pruebas unitarias mejoren el diseño de su sistema en lugar de probar cómo se junta todo.