¿Es el código verificable mejor el código?

100

Estoy intentando adquirir el hábito de escribir pruebas unitarias regularmente con mi código, pero he leído que primero es importante escribir código verificable . Esta pregunta toca los principios SÓLIDOS de la escritura de código verificable, pero quiero saber si esos principios de diseño son beneficiosos (o al menos no perjudiciales) sin planear en absoluto escribir pruebas. Para aclarar - entiendo la importancia de escribir pruebas; Esto no es una pregunta sobre su utilidad.

Para ilustrar mi confusión, en la pieza que inspiró esta pregunta, el escritor da un ejemplo de una función que verifica la hora actual y devuelve algún valor dependiendo de la hora. El autor señala este código como incorrecto porque produce los datos (el tiempo) que usa internamente, lo que dificulta la prueba. Para mí, sin embargo, parece una exageración pasar en el tiempo como un argumento. En algún punto, el valor debe inicializarse, y ¿por qué no estar más cerca del consumo? Además, el propósito del método en mi mente es devolver algún valor basado en la hora actual , al convertirlo en un parámetro que implica que este propósito puede / debe cambiarse. Esto, y otras preguntas, me llevan a preguntarme si el código comprobable era sinónimo de código "mejor".

¿Sigue siendo una buena práctica escribir código comprobable aún en ausencia de pruebas?

¿Es el código comprobable realmente más estable? se ha sugerido como un duplicado. Sin embargo, esa pregunta es sobre la "estabilidad" del código, pero me pregunto de manera más general si el código también es superior por otras razones, como la legibilidad, el rendimiento, el acoplamiento, etc.     

pregunta WannabeCoder 01.07.2015 - 16:59
fuente

12 respuestas

112

Con respecto a la definición común de pruebas unitarias, diría que no. He visto un código simple enrevesado debido a la necesidad de torcerlo para adecuarlo al marco de prueba (por ejemplo, interfaces y IoC en todas partes, lo que hace que las cosas sean difíciles de seguir a través de capas de llamadas de interfaz y datos que deberían ser obvios pasados por magia). Dada la elección entre el código que es fácil de entender o el código que es fácil de probar por unidad, siempre utilizo el código que se puede mantener.

Esto no significa no probar, sino ajustar las herramientas a su medida, no al revés. Hay otras formas de realizar pruebas (pero el código difícil de entender siempre es un código incorrecto). Por ejemplo, puede crear pruebas unitarias que sean menos granulares (por ejemplo, la actitud de Martin Fowler de Martin Fowler es que clase, no un método), o puede golpear su programa con pruebas de integración automatizadas en su lugar. Tal vez no sea tan bonito como su marco de prueba se ilumina con marcas verdes, pero estamos después del código probado, no de la gamificación del proceso, ¿no?

Puede hacer que su código sea fácil de mantener y aún así ser bueno para las pruebas unitarias definiendo buenas interfaces entre ellas y luego escribiendo pruebas que ejerzan la interfaz pública del componente; o podría obtener un mejor marco de prueba (uno que reemplaza las funciones en tiempo de ejecución para burlarse de ellas, en lugar de requerir que el código se compile con las simulaciones en su lugar). Un mejor marco de prueba de unidad le permite reemplazar la funcionalidad GetCurrentTime () del sistema con la suya, en tiempo de ejecución, por lo que no necesita introducir envoltorios artificiales para esto solo para adaptarse a la herramienta de prueba.

    
respondido por el gbjbaanb 01.07.2015 - 17:13
fuente
68
  

¿La escritura de código comprobable sigue siendo una buena práctica incluso en ausencia de pruebas?

Lo primero es lo primero, la ausencia de pruebas es un problema mucho más grande que su código que se puede probar o no. No tener pruebas de unidad significa que no ha terminado con su código / función.

Aparte de eso, no diría que es importante escribir código verificable, es importante escribir código flexible . El código inflexible es difícil de probar, por lo que hay mucha superposición en el enfoque y lo que la gente llama.

Para mí, siempre hay un conjunto de prioridades en el código de escritura:

  1. Haz que funcione : si el código no hace lo que tiene que hacer, es inútil.
  2. Haz que sea mantenible : si el código no es mantenible, dejará de funcionar rápidamente.
  3. Hágalo flexible : si el código no es flexible, dejará de funcionar cuando, inevitablemente, el negocio llegue y le preguntará si el código puede hacer XYZ.
  4. Hágalo rápido : más allá de un nivel básico aceptable, el rendimiento es simplemente una salsa.

Las pruebas unitarias ayudan al mantenimiento del código, pero solo hasta cierto punto. Si hace que el código sea menos legible, o más frágil para hacer que las pruebas unitarias funcionen, se vuelve contraproducente. El "código comprobable" es generalmente un código flexible, por lo que es bueno, pero no tan importante como la funcionalidad o la capacidad de mantenimiento. Para algo como la hora actual, hacer eso flexible es bueno, pero perjudica la capacidad de mantenimiento al hacer que el código sea más difícil de usar y más complejo. Dado que la capacidad de mantenimiento es más importante, por lo general erraré hacia un enfoque más simple, incluso si es menos comprobable.

    
respondido por el Telastyn 01.07.2015 - 17:17
fuente
48

Sí, es una buena práctica. La razón es que la capacidad de prueba no es para el propósito de las pruebas. Es por el bien de la claridad y la comprensión que trae consigo.

A nadie le importan las pruebas. Es un hecho triste de la vida que necesitamos grandes suites de pruebas de regresión porque no somos lo suficientemente inteligentes como para escribir código perfecto sin verificar constantemente nuestra base. Si pudiéramos, el concepto de pruebas sería desconocido, y todo esto no sería un problema. Ciertamente me gustaría poder hacerlo. Pero la experiencia ha demostrado que casi todos nosotros no podemos, por lo tanto, las pruebas que cubren nuestro código son una buena cosa, incluso si nos quitan tiempo para escribir el código de negocios.

¿De qué manera las pruebas mejoran nuestro código de negocio independientemente de la prueba? Al obligarnos a segmentar nuestra funcionalidad en unidades que fácilmente se demuestra que son correctas. Estas unidades también son más fáciles de acertar que las que, de lo contrario, tendríamos la tentación de escribir.

Tu ejemplo de tiempo es un buen punto. Siempre que solo tenga una función que regrese a la hora actual, podría pensar que no tiene sentido programarla. ¿Qué tan difícil puede ser conseguir esto bien? Pero inevitablemente, su programa usará esta función en otro código, y definitivamente desea probar ese código en diferentes condiciones, incluso en diferentes momentos. Por lo tanto, es una buena idea poder manipular el tiempo que retorna su función, no porque desconfíe de su llamada currentMillis() de una línea, sino porque necesita verificar la persona que llama de esa llamada bajo control circunstancias. Como puede ver, tener un código comprobable es útil, aunque por sí solo, no parece merecer tanta atención.

    
respondido por el Kilian Foth 01.07.2015 - 17:09
fuente
12
  

En algún punto, el valor debe inicializarse, y ¿por qué no es lo más cercano al consumo?

Porque es posible que necesite reutilizar ese código, con un valor diferente al generado internamente. La capacidad de insertar el valor que va a utilizar como parámetro garantiza que pueda generar esos valores en cualquier momento que lo desee, no solo "ahora" (que significa "ahora" cuando se llama el código).

Hacer que el código sea verificable en efecto significa hacer que el código pueda (desde el principio) usarse en dos escenarios diferentes (producción y prueba).

Básicamente, si bien puede argumentar que no hay incentivo para hacer que el código se pueda probar en ausencia de pruebas, existe una gran ventaja al escribir código reutilizable, y los dos son sinónimos.

  

Además, el propósito del método en mi mente es devolver algún valor en función de la hora actual, al convertirlo en un parámetro que implica que este propósito puede / debe cambiarse.

También podría argumentar que el propósito de este método es devolver algo de valor basado en un valor de tiempo, y lo necesita para generar ese valor basado en "ahora". Uno de ellos es más flexible, y si te acostumbras a elegir esa variante, con el tiempo, tu tasa de reutilización de código aumentará.

    
respondido por el utnapistim 01.07.2015 - 17:37
fuente
10

Puede parecer una tontería decirlo de esta manera, pero si quieres poder probar tu código, entonces sí, escribir código verificable es mejor. Usted pregunta:

  

En algún punto, el valor debe inicializarse, y por qué no es lo más cercano   al consumo?

Precisamente porque, en el ejemplo al que hace referencia, hace que ese código no se pueda probar. A menos que solo ejecute un subconjunto de sus pruebas en diferentes momentos del día. O restableces el reloj del sistema. O alguna otra solución. Todo lo cual es peor que simplemente hacer que su código sea flexible.

Además de ser inflexible, ese pequeño método en cuestión tiene dos responsabilidades: (1) obtener la hora del sistema y luego (2) devolver algún valor basado en ello.

public static string GetTimeOfDay()
{
    DateTime time = DateTime.Now;
    if (time.Hour >= 0 && time.Hour < 6)
    {
        return "Night";
    }
    if (time.Hour >= 6 && time.Hour < 12)
    {
        return "Morning";
    }
    if (time.Hour >= 12 && time.Hour < 18)
    {
        return "Afternoon";
    }
    return "Evening";
}

Tiene sentido desglosar las responsabilidades aún más, de modo que la parte fuera de su control ( DateTime.Now ) tenga el menor impacto en el resto de su código. Hacer esto hará que el código anterior sea más simple, con el efecto secundario de ser comprobable sistemáticamente.

    
respondido por el Eric King 01.07.2015 - 17:20
fuente
9

Ciertamente tiene un costo, pero algunos desarrolladores están tan acostumbrados a pagar que han olvidado que el costo está ahí. Para su ejemplo, ahora tiene dos unidades en lugar de una, ha requerido que el código de llamada inicialice y administre una dependencia adicional, y mientras que GetTimeOfDay es más comprobable, está de vuelta en el mismo barco probando su nuevo IDateTimeProvider . Es solo que si tiene buenas pruebas, los beneficios generalmente superan los costos.

Además, hasta cierto punto, la escritura de códigos comprobables lo alienta a diseñar su código de una manera más fácil de mantener. El nuevo código de administración de dependencias es molesto, por lo que querrá agrupar todas sus funciones dependientes del tiempo, si es posible. Eso puede ayudar a mitigar y corregir errores como, por ejemplo, cuando carga una página justo en un límite de tiempo, con algunos elementos representados usando el tiempo anterior y algunos usando el tiempo posterior. También puede acelerar su programa al evitar llamadas repetidas al sistema para obtener la hora actual.

Por supuesto, esas mejoras arquitectónicas dependen en gran medida de que alguien se dé cuenta de las oportunidades y las implemente. Uno de los mayores peligros de centrarse tan estrechamente en las unidades es perder de vista el panorama general.

Muchos marcos de prueba de unidad le permiten parchar un objeto simulado en tiempo de ejecución, lo que le permite obtener los beneficios de la capacidad de prueba sin todo el desorden. Incluso lo he visto hecho en C ++. Examine esa capacidad en situaciones en las que parece que el costo de la capacidad de prueba no vale la pena.

    
respondido por el Karl Bielefeldt 01.07.2015 - 17:53
fuente
8

Es posible que no todas las características que contribuyen a la capacidad de prueba sean deseables fuera del contexto de la capacidad de prueba: tengo problemas para encontrar una justificación no relacionada con la prueba para el parámetro de tiempo que cita, por ejemplo, pero en términos generales las características que contribuir a la capacidad de prueba también contribuir al buen código independientemente de la capacidad de prueba.

En términos generales, el código comprobable es un código maleable. Está en trozos pequeños, discretos, cohesivos, por lo que se pueden reutilizar los bits individuales. Está bien organizado y tiene un nombre adecuado (para poder probar algunas funciones, debe prestar más atención a la asignación de nombres; si no estuviera escribiendo las pruebas, el nombre de una función de un solo uso sería menos importante). Tiende a ser más paramétrico (como su ejemplo de tiempo), por lo que está abierto a otros contextos que el propósito original. Es SECO, por lo tanto menos abarrotado y más fácil de comprender.

Sí. Es una buena práctica escribir código verificable, incluso sin importar las pruebas.

    
respondido por el Carl Manaster 01.07.2015 - 17:26
fuente
8

Escribir código verificable es importante si quieres poder probar que tu código realmente funciona.

Tiendo a estar de acuerdo con los sentimientos negativos acerca de convertir tu código en contorsiones atroces para que se ajuste a un marco de prueba en particular.

Por otra parte, todos los que estamos aquí, en algún momento u otro, han tenido que lidiar con esa función mágica de 1.000 líneas que es simplemente atroz tener que lidiar, prácticamente no se puede tocar sin romper uno o más oscuros Dependencias no obvias en otro lugar (o en algún lugar dentro de sí mismo, donde la dependencia es casi imposible de visualizar) y es prácticamente imposible de probar por definición. La noción (que no es sin mérito) de que los marcos de prueba se han exagerado no debe tomarse como una licencia gratuita para escribir código de baja calidad e inestable, en mi opinión.

Los ideales de desarrollo impulsados por pruebas tienden a empujarlo a escribir procedimientos de responsabilidad única, por ejemplo, y que es definitivamente algo bueno. Personalmente, digo que compre una única responsabilidad, una única fuente de verdad, un alcance controlado (sin variables globales extrañas) y mantenga las dependencias frágiles al mínimo, y su código será comprobable. ¿Se puede probar con algún marco de prueba específico? Quién sabe. Pero tal vez sea el marco de prueba el que necesita ajustarse al buen código, y no al revés.

Pero para ser claro, el código que es tan inteligente, o tan largo y / o interdependiente, que no sea comprendido fácilmente por otro ser humano no es un buen código. Y también, casualmente, no es un código que se pueda probar fácilmente.

Por lo tanto, al acercarse a mi resumen, ¿es el código comprobable mejor ?

No lo sé, tal vez no. La gente aquí tiene algunos puntos válidos.

Pero creo que el código mejor tiende a ser también un código verificable .

Y que si está hablando de software serio para uso en actividades serias, el envío de códigos no probados no es lo más responsable que podría estar haciendo con el dinero de su empleador o de sus clientes.

También es cierto que algunos códigos requieren pruebas más rigurosas que otros y es un poco tonto pretender lo contrario. ¿Cómo le gustaría haber sido un astronauta en el transbordador espacial si el sistema de menú que lo conectó con los sistemas vitales del transbordador no se probó? ¿O un empleado en una planta nuclear donde los sistemas de software que monitorean la temperatura en el reactor no fueron probados? Por otro lado, ¿un poco de código que genera un informe simple de solo lectura requiere un camión contenedor lleno de documentación y pruebas de mil unidades? Espero que no. Sólo digo ...

    
respondido por el Craig 02.07.2015 - 04:11
fuente
5
  

Para mí, sin embargo, parece una exageración pasar en el tiempo como un argumento.

Tienes razón, y con burla puedes hacer que el código sea verificable y evite pasar el tiempo (intención de juego de palabras indefinida). Código de ejemplo:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Ahora digamos que quieres probar lo que sucede durante un segundo. Como dices, para probar esto de manera excesiva deberías cambiar el código (de producción):

def time_of_day(now=None):
    now = now if now is not None else datetime.datetime.utcnow()
    return now.strftime('%H:%M:%S')

Si Python admitió los primeros segundos el código de prueba se vería así:

def test_handle_leap_second(self):
    actual = time_of_day(
        now=datetime.datetime(year=2015, month=6, day=30, hour=23, minute=59, second=60)
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Puede probar esto, pero el código es más complejo de lo necesario y las pruebas still no pueden ejercer de manera confiable la rama de código que usará la mayoría del código de producción (es decir, no pasa un valor) para now ). Para solucionar esto, utiliza un simulacro . A partir del original código de producción:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Código de prueba:

@unittest.patch('datetime.datetime.utcnow')
def test_handle_leap_second(self, utcnow_mock):
    utcnow_mock.return_value = datetime.datetime(
        year=2015, month=6, day=30, hour=23, minute=59, second=60)
    actual = time_of_day()
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Esto da varios beneficios:

  • Estás probando time_of_day independientemente de sus dependencias.
  • Está probando la misma ruta del código como código de producción.
  • El código de producción es lo más simple posible.

En una nota al margen, es de esperar que los futuros marcos de burla hagan que esto sea más fácil. Por ejemplo, ya que debes referirte a la función simulada como una cadena, no puedes hacer que los IDEs cambien automáticamente cuando time_of_day comienza a usar otra fuente por tiempo.

    
respondido por el l0b0 04.07.2015 - 15:57
fuente
4

Una calidad de código bien escrito es que es robusto para cambiar . Es decir, cuando aparece un cambio de requisitos, el cambio en el código debe ser proporcional. Este es un ideal (y no siempre se logra), pero escribir código comprobable nos ayuda a acercarnos a este objetivo.

¿Por qué nos ayuda a acercarnos? En producción, nuestro código opera dentro del entorno de producción, incluida la integración e interacción con todos los demás códigos. En pruebas unitarias, eliminamos gran parte de este entorno. Nuestro código ahora está siendo robusto para cambiar porque las pruebas son un cambio . Estamos usando las unidades de diferentes maneras, con diferentes entradas (simulacros, entradas incorrectas que en realidad nunca podrían pasar, etc.) de las que usaríamos en producción.

Esto prepara nuestro código para el día en que ocurra un cambio en nuestro sistema. Digamos que nuestro cálculo de tiempo necesita tomar un tiempo diferente basado en una zona horaria. Ahora tenemos la capacidad de pasar ese tiempo y no tenemos que hacer ningún cambio en el código. Cuando no queremos pasar una hora y queremos usar la hora actual, solo podemos usar un argumento predeterminado. Nuestro código es robusto para cambiar porque es comprobable.

    
respondido por el cbojar 01.07.2015 - 17:34
fuente
4

Según mi experiencia, una de las decisiones más importantes y de mayor alcance que toma al construir un programa es cómo se divide el código en unidades (donde se usa "unidades" en su versión más amplia sentido). Si está utilizando un lenguaje OO basado en clases, debe dividir todos los mecanismos internos utilizados para implementar el programa en un número determinado de clases. Entonces necesitas dividir el código de cada clase en un número de métodos. En algunos idiomas, la opción es cómo dividir su código en funciones. O si hace lo de SOA, debe decidir cuántos servicios construirá y qué incluirá en cada servicio.

El desglose que elija tiene un efecto enorme en todo el proceso. Las buenas elecciones hacen que el código sea más fácil de escribir y producen menos errores (incluso antes de comenzar a probar y depurar). Facilitan el cambio y el mantenimiento. Curiosamente, resulta que una vez que encuentra un buen desglose, por lo general también es más fácil probar que uno malo.

¿Por qué esto es así? No creo que pueda entender y explicar todas las razones. Pero una razón es que un buen desglose invariablemente significa elegir un "tamaño de grano" moderado para las unidades de implementación. No desea incluir demasiada funcionalidad y demasiada lógica en una sola clase / método / función / módulo / etc. Esto hace que su código sea más fácil de leer y escribir, pero también hace que sea más fácil de probar.

Sin embargo, no es solo eso. Un buen diseño interno significa que el comportamiento esperado (entradas / salidas / etc) de cada unidad de implementación se puede definir de manera clara y precisa. Esto es importante para la prueba. Un buen diseño generalmente significa que cada unidad de implementación tendrá un número moderado de dependencias sobre las otras. Eso hace que su código sea más fácil de leer y entender para otros, pero también facilita la prueba. Las razones continúan; quizás otros puedan articular más razones que yo no puedo.

Con respecto al ejemplo de su pregunta, no creo que "buen diseño de código" sea equivalente a decir que todas las dependencias externas (como una dependencia del reloj del sistema) siempre deben ser "inyectadas". Podría ser una buena idea, pero es un tema aparte de lo que describo aquí y no profundizaré en sus pros y sus contras.

Incidentalmente, incluso si realiza llamadas directas a las funciones del sistema que devuelven la hora actual, actúa sobre el sistema de archivos, etc., esto no significa que no pueda hacer una prueba unitaria de su código de forma aislada. El truco es usar una versión especial de las bibliotecas estándar que le permite falsificar los valores de retorno de las funciones del sistema. Nunca he visto a otros mencionar esta técnica, pero es bastante simple de hacer con muchos lenguajes y plataformas de desarrollo. (Esperamos que el tiempo de ejecución de tu idioma sea de código abierto y fácil de compilar. Si la ejecución de tu código implica un paso de enlace, también es fácil controlar con qué bibliotecas está vinculada).

En resumen, el código comprobable no es necesariamente un código "bueno", pero el código "bueno" generalmente es comprobable.

    
respondido por el Alex D 02.07.2015 - 08:28
fuente
1

Si está siguiendo los principios de SOLID estará en el lado bueno, especialmente si amplíe esto con KISS , DRY , y YAGNI .

Un punto faltante para mí es el punto de la complejidad de un método. ¿Es un método simple de getter / setter? Entonces solo escribir pruebas para satisfacer su marco de prueba sería una pérdida de tiempo.

Si es un método más complejo en el que manipula los datos y desea asegurarse de que funcionará incluso si tiene que cambiar la lógica interna, sería una excelente opción para un método de prueba. Muchas veces tuve que cambiar un código después de varios días / semanas / meses, y me sentí muy feliz de tener el caso de prueba. Cuando desarrollé el método por primera vez, lo probé con el método de prueba y estaba seguro de que funcionará. Después del cambio mi código de prueba principal todavía funcionaba. Así que estaba seguro de que mi cambio no rompió un código antiguo en producción.

Otro aspecto de las pruebas de escritura es mostrar a otros desarrolladores cómo usar su método. Muchas veces, un desarrollador buscará un ejemplo sobre cómo usar un método y cuál será el valor de retorno.

Solo mis dos centavos .

    
respondido por el BtD 01.07.2015 - 19:02
fuente

Lea otras preguntas en las etiquetas