unitarios unitarias test pruebas ejemplos atdd unit-testing refactoring automated-tests integration-testing code-coverage

unit testing - unitarias - ¿Debería uno probar la implementación interna, o solo probar el comportamiento público?



tdd (15)

Axioma: cada programador debe probar su propio código

No creo que esto sea universalmente cierto.

En la criptografía, hay un dicho muy conocido: "es fácil crear un cifrado tan seguro que no sabes cómo romperlo tú mismo".

En su proceso de desarrollo típico, usted escribe su código, luego lo compila y lo ejecuta para verificar que haga lo que usted cree que hace. Repita esto un montón de tiempo y se sentirá bastante seguro acerca de su código.

Su confianza lo hará un tester menos vigilante. Quien no comparta su experiencia con el código no tendrá el problema.

Además, un nuevo par de ojos puede tener menos ideas preconcebidas no solo sobre la fiabilidad del código sino también sobre lo que hace el código. Como consecuencia, pueden llegar a casos de prueba en los que el autor del código no haya pensado. Uno esperaría que esos descubran más errores o difundan un poco más el conocimiento sobre lo que hace el código alrededor de la organización.

Además, hay un argumento para hacer que para ser un buen programador tienes que preocuparte por los casos extremos, pero para ser un buen probador tienes que preocuparte obsesivamente ;-) también, los probadores pueden ser más baratos, por lo que puede valer la pena tener un separado equipo de prueba por esa razón.

Creo que la pregunta principal es esta: ¿qué metodología es la mejor para encontrar errores en el software? Hace poco vi un video (sin enlace, lo siento) que indica que las pruebas aleatorias son más baratas y efectivas que las pruebas generadas por humanos.

Dado software donde ...

  • El sistema consiste en algunos subsistemas
  • Cada subsistema consta de algunos componentes
  • Cada componente se implementa usando muchas clases

... Me gusta escribir pruebas automatizadas de cada subsistema o componente.

No escribo una prueba para cada clase interna de un componente (excepto en la medida en que cada clase contribuye a la funcionalidad pública del componente y, por lo tanto, se puede probar / probar desde afuera a través de la API pública del componente).

Cuando refactorizo ​​la implementación de un componente (que a menudo hago, como parte de agregar nuevas funcionalidades), no es necesario alterar ninguna prueba automatizada existente: porque las pruebas solo dependen de la API pública del componente y de las API públicas. típicamente se están expandiendo en lugar de modificarse.

Creo que esta política contrasta con un documento como Refactoring Test Code , que dice cosas como ...

  • "... examen de la unidad ..."
  • "... una clase de prueba para cada clase en el sistema ..."
  • "... idealmente se considera que el ratio de código de prueba / código de producción ... se aproxima a una proporción de 1: 1 ..."

... todo lo cual supongo que estoy en desacuerdo (o al menos no practico).

Mi pregunta es, si no está de acuerdo con mi política, ¿podría explicar por qué? ¿En qué escenarios es este grado de prueba insuficiente?

En resumen:

  • Las interfaces públicas se prueban (y vuelven a probar), y rara vez cambian (se agregan pero rara vez se modifican)
  • Las API internas están ocultas detrás de las API públicas, y se pueden cambiar sin volver a escribir los casos de prueba que prueban las API públicas.

Nota al pie: algunos de mis ''casos de prueba'' se implementan realmente como datos. Por ejemplo, los casos de prueba para la IU consisten en archivos de datos que contienen varias entradas de usuario y las salidas del sistema esperadas correspondientes. Probar el sistema significa tener un código de prueba que lee cada archivo de datos, reproduce la entrada en el sistema y afirma que obtiene el resultado esperado correspondiente.

Aunque rara vez necesito cambiar el código de prueba (porque las API públicas generalmente se agregan en vez de modificarse), sí encuentro que algunas veces (por ejemplo, dos veces por semana) necesito cambiar algunos archivos de datos existentes. Esto puede ocurrir cuando modifico la salida del sistema para mejor (es decir, la nueva funcionalidad mejora la salida existente), lo que podría causar que una prueba existente ''falle'' (porque el código de prueba solo intenta afirmar que la salida no ha cambiado). Para manejar estos casos, hago lo siguiente:

  • Vuelva a ejecutar el conjunto de pruebas automatizadas que tiene un indicador especial en tiempo de ejecución, que le indica que no afirme el resultado, sino que capture el nuevo resultado en un nuevo directorio
  • Use una herramienta de diferencia visual para ver qué archivos de datos de salida (es decir, qué casos de prueba) han cambiado, y para verificar que estos cambios sean buenos y como se esperaba, dada la nueva funcionalidad
  • Actualice las pruebas existentes copiando los nuevos archivos de salida del nuevo directorio en el directorio desde el cual se ejecutan los casos de prueba (sobrescribiendo las pruebas anteriores)

Nota al pie: por "componente", me refiero a algo así como "una DLL" o "un ensamblaje" ... algo que es lo suficientemente grande para ser visible en una arquitectura o diagrama de implementación del sistema, a menudo implementado con docenas o 100 clases, y con una API pública que consta de solo 1 o un puñado de interfaces ... algo que puede asignarse a un equipo de desarrolladores (donde se asigna un componente diferente a un equipo diferente) y que, por lo tanto, de acuerdo con la Ley de Conway, una API pública relativamente estable.

Nota al pie: El artículo Prueba orientada a objetos: mito y realidad dice:

Mito: las pruebas de caja negra son suficientes. Si hace un trabajo cuidadoso en el diseño de casos de prueba usando la interfaz de clase o la especificación, puede estar seguro de que la clase se ha ejercitado completamente. Las pruebas de caja blanca (al observar la implementación de un método para diseñar pruebas) violan el concepto mismo de encapsulación.

Realidad: la estructura OO es importante, parte II. Muchos estudios han demostrado que los paquetes de prueba de caja negra pensados ​​para ser extremadamente minuciosos por los desarrolladores solo ejercitan de un tercio a la mitad de los enunciados (sin mencionar las rutas o estados) en la implementación bajo prueba. Hay tres razones para esto. En primer lugar, las entradas o estados seleccionados normalmente ejercen rutas normales, pero no fuerzan todas las rutas / estados posibles. En segundo lugar, las pruebas de caja negra por sí solas no pueden revelar sorpresas. Supongamos que hemos probado todos los comportamientos especificados del sistema bajo prueba. Para tener la certeza de que no hay comportamientos no especificados, necesitamos saber si el paquete de pruebas Black-box no ha ejercido ninguna parte del sistema. La única forma en que se puede obtener esta información es mediante la instrumentación del código. En tercer lugar, a menudo es difícil ejercer la excepción y el manejo de errores sin examinar el código fuente.

Debo añadir que estoy haciendo pruebas funcionales de whitebox: veo el código (en la implementación) y escribo pruebas funcionales (que dirigen la API pública) para ejercitar las diversas ramas de código (detalles de la implementación de la característica).


¿Todavía estás siguiendo este enfoque? También creo que este es el enfoque correcto. Solo debe probar las interfaces públicas. Ahora la interfaz pública puede ser un servicio o algún componente que recibe información de algún tipo de UI o cualquier otra fuente.

Pero debe poder evolucionar el servicio o componente principal utilizando el enfoque de Prueba Primero. es decir, defina una interfaz pública y pruébela para una funcionalidad básica. fallará Implementa esa funcionalidad básica usando clases de fondo API. Escriba API para satisfacer solo este último caso de prueba. Luego sigue preguntando qué puede hacer el servicio más y evolucionar.

Solo la decisión de equilibrio que se debe tomar es romper el único servicio o componente grande en pocos servicios y componentes más pequeños que puedan reutilizarse. Si cree firmemente que un componente se puede reutilizar en los proyectos. Luego, se deben escribir pruebas automáticas para ese componente. Pero nuevamente, las pruebas escritas para el gran servicio o componente deben duplicar funcionalmente ya probado como un componente.

Ciertas personas pueden entrar en la discusión teórica de que no se trata de pruebas unitarias. Entonces eso está bien. La idea básica es tener pruebas automatizadas que prueben su software. Entonces, ¿qué pasa si no está a nivel de unidad. Si cubre la integración con la base de datos (que usted controla), entonces solo es mejor.

Avíseme si ha desarrollado algún buen proceso que funcione para usted ... desde su primera publicación ...

respetos ameet


Depende de tu diseño y de dónde sea el mayor valor. Un tipo de aplicación puede exigir un enfoque diferente a otro. A veces apenas se detecta algo interesante con pruebas unitarias, mientras que las pruebas funcionales / de integración producen sorpresas. A veces, las pruebas unitarias fallan cientos de veces durante el desarrollo, atrapando a muchos, muchos errores en la fabricación.

A veces es trivial. La forma en que algunas clases se juntan hace que el retorno de la inversión de probar cada ruta sea menos tentador, por lo que puede trazar una línea y pasar a martillear algo más importante / complicado / muy utilizado.

A veces no es suficiente con solo probar la API pública porque hay una lógica particularmente interesante al acecho, y es demasiado doloroso poner el sistema en marcha y ejercitar esas rutas particulares. Eso es cuando probar las agallas de eso vale la pena.

En estos días, tiendo a escribir numerosas, (a menudo extremadamente) clases simples que hacen una o dos cosas en la parte superior. Luego implemento el comportamiento deseado al delegar toda la funcionalidad complicada a esas clases internas. Es decir, tengo interacciones un poco más complejas, pero clases realmente simples.

Si cambio mi implementación y tengo que refactorizar algunas de esas clases, generalmente no me importa. Mantengo mis pruebas aisladas lo mejor que puedo, por lo que a menudo es un simple cambio hacer que vuelvan a funcionar. Sin embargo, si tengo que descartar algunas de las clases internas, a menudo reemplazo un puñado de clases y escribo algunas pruebas completamente nuevas. A menudo escucho gente quejándose de tener que mantener las pruebas actualizadas después de la refactorización y, aunque a veces es inevitable y tedioso, si el nivel de granularidad es lo suficientemente bueno, generalmente no es un gran problema tirar algunas pruebas de código +.

Siento que esta es una de las principales diferencias entre diseñar para probar y no molestar.


Estoy de acuerdo con la mayoría de las publicaciones aquí, sin embargo, agregaría esto:

Hay una prioridad principal para probar interfaces públicas, luego protegidas, luego privadas.

Por lo general, las interfaces públicas y protegidas son un resumen de una combinación de interfaces privadas y protegidas.

Personalmente: debes probar todo. Dado un sólido conjunto de pruebas para funciones más pequeñas, se le dará una mayor confianza de que los métodos ocultos funcionan. También estoy de acuerdo con el comentario de otra persona sobre refactorización. La cobertura del código lo ayudará a determinar dónde están los bits adicionales de código y a refactorizarlos si es necesario.


Estoy de acuerdo en que la cobertura del código idealmente debería ser del 100%. Esto no necesariamente significa que 60 líneas de código tendrían 60 líneas de código de prueba, pero que cada ruta de ejecución se prueba. Lo único más molesto que un error es un error que aún no se ha ejecutado.

Al solo probar la API pública, corre el riesgo de no probar todas las instancias de las clases internas. Realmente estoy diciendo lo obvio al decir eso, pero creo que debería mencionarse. Cuanto más se prueba cada comportamiento, más fácil es reconocer no solo que está roto, sino también lo que está roto.


Hasta ahora, ha habido muchas respuestas excelentes a esta pregunta, pero quiero agregar algunas notas propias. Como prefacio: soy un consultor para una gran empresa que ofrece soluciones tecnológicas a una amplia gama de grandes clientes. Digo esto porque, en mi experiencia, estamos obligados a probar mucho más a fondo que la mayoría de las tiendas de software (salvo quizás los desarrolladores de API). Estos son algunos de los pasos que seguimos para garantizar la calidad:

  • Prueba interna de la unidad:
    Se espera que los desarrolladores creen pruebas unitarias para todo el código que escriben (léase: todos los métodos). Las pruebas unitarias deben cubrir condiciones de prueba positivas (¿funciona mi método?) Y condiciones de prueba negativas (¿el método arroja una ArgumentNullException cuando uno de mis argumentos requeridos es nulo?). Normalmente incorporamos estas pruebas en el proceso de compilación utilizando una herramienta como CruiseControl.net
  • Prueba del sistema / prueba de montaje:
    A veces, este paso se llama algo diferente, pero aquí es cuando comenzamos a probar la funcionalidad pública. Una vez que sepa que todas sus unidades individuales funcionan como se espera, desea saber que sus funciones externas también funcionan de la manera que usted cree que deberían. Esta es una forma de verificación funcional ya que el objetivo es determinar si todo el sistema funciona como debería. Tenga en cuenta que esto no incluye ningún punto de integración. Para la prueba del sistema, debe utilizar interfaces simuladas en lugar de las reales para que pueda controlar la salida y crear casos de prueba a su alrededor.
  • Prueba de integración del sistema:
    En esta etapa del proceso, desea conectar sus puntos de integración al sistema. Por ejemplo, si está usando un sistema de procesamiento de tarjetas de crédito, querrá incorporar el sistema en vivo en esta etapa para verificar que aún funcione. Desea realizar pruebas similares a la prueba de sistema / ensamblaje.
  • Prueba de verificación funcional:
    La verificación funcional es usuarios que se ejecutan a través del sistema o que usan API para verificar que funciona como se espera. Si ha creado un sistema de facturación, esta es la etapa en la que ejecutará sus scripts de prueba de principio a fin para asegurarse de que todo funcione como lo diseñó. Esta es obviamente una etapa crítica en el proceso, ya que te dice si ya has hecho tu trabajo.
  • Prueba de certificación:
    Aquí, coloca a los usuarios reales frente al sistema y les permite probarlo. Lo ideal es que ya haya probado su interfaz de usuario en algún momento con sus partes interesadas, pero esta etapa le indicará si a su público objetivo le gusta su producto. Es posible que haya escuchado esto llamado algo así como un "candidato de lanzamiento" por otros proveedores. Si todo va bien en esta etapa, sabes que eres bueno para pasar a la producción. Las pruebas de certificación siempre deben realizarse en el mismo entorno que utilizará para la producción (o un entorno idéntico al menos).

Por supuesto, sé que no todos siguen este proceso, pero si lo miras de principio a fin, puedes comenzar a ver los beneficios de los componentes individuales. No he incluido cosas como pruebas de verificación de compilación, ya que ocurren en una línea de tiempo diferente (por ejemplo, todos los días). Personalmente, creo que las pruebas unitarias son críticas, ya que le dan una idea profunda de qué componente específico de su aplicación está fallando en qué caso de uso específico. Las pruebas unitarias también lo ayudarán a aislar qué métodos funcionan correctamente para que no pierda tiempo mirándolos para obtener más información acerca de un error cuando no hay nada de malo en ellos.

Por supuesto, las pruebas unitarias también podrían ser incorrectas, pero si desarrolla sus casos de prueba a partir de su especificación funcional / técnica (tiene una, ¿verdad?;)), No debería tener demasiados problemas.


La respuesta es muy simple: está describiendo las pruebas funcionales, que es una parte importante del control de calidad del software. La implementación interna de pruebas es una prueba unitaria, que es otra parte del control de calidad del software con un objetivo diferente. Es por eso que siente que la gente no está de acuerdo con su enfoque.

Las pruebas funcionales son importantes para validar que el sistema o subsistema hace lo que se supone que debe hacer. Todo lo que el cliente vea debe probarse de esta manera.

La prueba de unidad está aquí para verificar que las 10 líneas de código que acaba de escribir hagan lo que se supone que deben hacer. Le da una mayor confianza en su código.

Ambos son complementarios. Si trabaja en un sistema existente, es probable que las pruebas funcionales funcionen primero. Pero tan pronto como agregue código, probarlo también es una buena idea.


Mi práctica es probar las partes internas a través de la API / UI pública. Si no se puede acceder a algún código interno desde el exterior, entonces refactorizaré para eliminarlo.


No deberías pensar ciegamente que una unidad == una clase. Creo que puede ser contraproducente. Cuando digo que escribo una prueba unitaria, estoy probando una unidad lógica, "algo" que proporciona cierto comportamiento. Una unidad puede ser una sola clase, o pueden ser varias clases trabajando juntas para proporcionar ese comportamiento. A veces comienza como una sola clase, pero evoluciona para convertirse en tres o cuatro clases más tarde.

Si empiezo con una clase y escribo pruebas para eso, pero más tarde se convierte en varias clases, generalmente no escribiré pruebas separadas para las otras clases; son detalles de implementación en la unidad que se está probando. De esta manera, dejo crecer mi diseño y mis pruebas no son tan frágiles.

Yo solía pensar exactamente como CrisW demonstartes en esta pregunta, que probar en niveles más altos sería mejor, pero después de obtener más experiencia, mis pensamientos se moderan a algo entre eso y "cada clase debe tener una clase de prueba". Cada unidad debe tener pruebas, pero elijo definir mis unidades ligeramente diferentes de lo que hice una vez. Podrían ser los "componentes" de los que CrisW habla, pero a menudo también es una sola clase.

Además, las pruebas funcionales pueden ser lo suficientemente buenas como para demostrar que su sistema hace lo que se supone que debe hacer, pero si desea conducir su diseño con ejemplos / pruebas (TDD / BDD), las pruebas de palanca inferior son una consecuencia natural. Podrías tirar esas pruebas de bajo nivel cuando hayas terminado la implementación, pero eso sería un desperdicio: las pruebas son un efecto secundario positivo. Si decides realizar refactorizaciones drásticas que invaliden tus pruebas de bajo nivel, las descartas y escribes una nueva.

Separar el objetivo de probar / probar su software y usar pruebas / ejemplos para impulsar su diseño / implementación puede aclarar mucho esta discusión.

Actualización: Además, básicamente hay dos formas de hacer TDD: afuera adentro y adentro afuera. BDD promueve la entrada externa, lo que conduce a pruebas / especificaciones de mayor nivel. Sin embargo, si comienzas desde los detalles, escribirás pruebas detalladas para todas las clases.


No tengo mi copia de Lakos delante de mí, así que en lugar de citarlo simplemente señalaré que hace un mejor trabajo que el de explicar por qué las pruebas son importantes en todos los niveles.

El problema de probar solo el "comportamiento público" es que una prueba de este tipo brinda muy poca información. Capturará muchos errores (al igual que el compilador detectará muchos errores), pero no puede decirle dónde están los errores. Es común que una unidad mal implementada devuelva buenos valores durante mucho tiempo y luego deje de hacerlo cuando cambian las condiciones; si esa unidad se hubiera probado directamente, el hecho de que se haya implementado mal se habría evidenciado antes.

El mejor nivel de granularidad de prueba es el nivel de unidad. Proporcione pruebas para cada unidad a través de su (s) interfaz (es). Esto le permite validar y documentar sus creencias sobre cómo se comporta cada componente, lo que a su vez le permite probar el código dependiente al probar solo la nueva funcionalidad que presenta, lo que a su vez mantiene las pruebas cortas y segmentadas. Como beneficio adicional, realiza pruebas con el código que están probando.

Para expresarlo de manera diferente, es correcto probar solo el comportamiento público, siempre que observe que cada clase visible públicamente tiene un comportamiento público.


Personalmente también compruebo partes protegidas, porque son "públicas" para tipos heredados ...


Pruebo detalles de implementación privados así como también interfaces públicas. Si cambio un detalle de implementación y la nueva versión tiene un error, esto me permite tener una mejor idea de dónde está realmente el error y no solo de qué está afectando.


Puede codificar pruebas funcionales; esta bien. Pero debe validar el uso de la cobertura de prueba en la implementación, para demostrar que el código que se está probando tiene un propósito relativo a las pruebas funcionales, y que realmente hace algo relevante.


Si está practicando el desarrollo puro basado en pruebas, entonces solo implementa cualquier código después de que tenga cualquier prueba que falla, y solo implementa el código de prueba cuando no tiene pruebas fallidas. Además, solo implemente lo más simple para realizar una prueba de falla o aprobación.

En la práctica limitada de TDD, he tenido que ver cómo esto me ayuda a realizar pruebas unitarias para cada condición lógica producida por el código. No estoy del todo seguro de que el 100% de las funciones lógicas de mi código privado esté expuesto en mis interfaces públicas. La práctica de TDD parece complementaria a esa métrica, pero todavía hay funciones ocultas no permitidas por las API públicas.

Supongo que podrías decir que esta práctica me protege contra futuros defectos en mis interfaces públicas. O lo encuentra útil (y le permite agregar nuevas funciones más rápidamente) o encuentra que es una pérdida de tiempo.


[Una respuesta a mi propia pregunta]

Tal vez una de las variables que importa mucho es la cantidad de programadores diferentes que hay codificación:

  • Axioma: cada programador debe probar su propio código

  • Por lo tanto: si un programador escribe y entrega una "unidad", entonces también deberían haber probado esa unidad, posiblemente escribiendo una "prueba de unidad"

  • Corolario: si un programador solo escribe un paquete completo, es suficiente que el programador escriba pruebas funcionales de todo el paquete (no es necesario escribir pruebas de unidad de unidades dentro del paquete, ya que esas unidades son detalles de implementación a los que otros programadores no tienen acceso directo / exposición).

Del mismo modo, la práctica de construir componentes "falsos" con los que puedes contrastar:

  • Si tiene dos equipos que crean dos componentes, cada uno puede necesitar "simular" el componente del otro para que tengan algo (el simulacro) contra el cual probar su propio componente, antes de que su componente se considere listo para las "pruebas de integración" subsiguientes, y antes de que el otro equipo haya entregado su componente contra el cual su componente puede ser probado.

  • Si está desarrollando todo el sistema, puede hacer crecer todo el sistema ... por ejemplo, desarrollar un nuevo campo de GUI, un nuevo campo de base de datos, una nueva transacción comercial y una nueva prueba de sistema / funcional, todo como parte de uno iteración, sin necesidad de desarrollar "burlas" de ninguna capa (ya que en su lugar puedes probar contra la realidad).