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

103

Estoy tratando de acostumbrarme a escribir pruebas unitarias regularmente con mi código, pero he leído que primero es importante escribir código comprobable . Esta pregunta toca los principios SÓLIDOS de escribir código comprobable, pero quiero saber si esos principios de diseño son beneficiosos (o al menos no perjudiciales) sin planear escribir pruebas en absoluto. Para aclarar, entiendo la importancia de escribir exámenes; No se trata de 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 esto como un código incorrecto porque produce los datos (el tiempo) que usa internamente, lo que dificulta la prueba. Sin embargo, para mí, parece una exageración pasar el tiempo como argumento. En algún momento, el valor debe ser inicializado, y ¿por qué no más cercano al 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 implica que este propósito puede / debe cambiarse. Esta y otras preguntas me llevan a preguntarme si el código comprobable es sinónimo de código "mejor".

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


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

WannabeCoder
fuente
24
Hay una propiedad especial de la función que requiere que pases en el tiempo llamada idempotencia. Dicha función producirá el mismo resultado cada vez que se llame con un valor de argumento dado, lo que no solo lo hace más verificable, sino más composible y más fácil de razonar.
Robert Harvey
44
¿Puedes definir "mejor código"? ¿quieres decir "mantenible" ?, "más fácil de usar sin IOC-Container-Magic"?
k3b
77
Supongo que nunca ha fallado una prueba porque utilizó la hora real del sistema y luego cambió el desplazamiento de la zona horaria.
Andy
55
Es mejor que el código no comprobable.
Tulains Córdova
14
@RobertHarvey No llamaría a eso idempotencia, diría que es transparencia referencial : si se func(X)devuelve "Morning", reemplazar todas las ocurrencias de func(X)con "Morning"no cambiará el programa (es decir, llamar funcno hace nada más que devolver el valor). La idempotencia implica eso func(func(X)) == X(que no es de tipo correcto), o que func(X); func(X);realiza los mismos efectos secundarios que func(X)(pero no hay efectos secundarios aquí)
Warbo

Respuestas:

116

Con respecto a la definición común de pruebas unitarias, diría que no. He visto un código simple complicado debido a la necesidad de torcerlo para adaptarlo al marco de prueba (por ejemplo, interfaces e 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 por arte de 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 uso el código que se puede mantener.

Esto no significa no probar, sino ajustar las herramientas a su medida, y no al revés. Hay otras formas de probar (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 que una unidad es generalmente una clase, no un método), o puede utilizar su programa con pruebas de integración automatizadas. Tal vez no sea tan bonito como su marco de prueba se ilumina con marcas verdes, pero buscamos código probado, no la gamificación del proceso, ¿verdad?

Puede hacer que su código sea fácil de mantener y seguir siendo 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 reemplace las funciones en tiempo de ejecución para burlarse de ellas, en lugar de requerir que el código se compile con simulacros en su lugar). Un marco de prueba de unidad mejor le permite reemplazar la funcionalidad del sistema GetCurrentTime () con la suya propia, en tiempo de ejecución, por lo que no necesita introducir envoltorios artificiales para que se adapte a la herramienta de prueba.

gbjbaanb
fuente
3
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Ingeniero mundial
2
Creo que vale la pena señalar que conozco al menos un idioma que le permite hacer lo que describe su último párrafo: Python con Mock. Debido a la forma en que funcionan las importaciones de módulos, casi cualquier cosa, aparte de las palabras clave, se puede reemplazar con un simulacro, incluso métodos / clases / etc. API estándar. Entonces eso es posible, pero puede requerir que el lenguaje esté diseñado de una manera que admita ese tipo de flexibilidad.
jpmc26
66
Creo que hay una diferencia entre "código comprobable" y "código [retorcido] para adaptarse al marco de prueba". No estoy seguro de a dónde voy con este comentario, aparte de decir que estoy de acuerdo en que el código "retorcido" es malo, y el código "comprobable" con buenas interfaces es bueno.
Bryan Oakley
2
Expresé algunos de mis pensamientos en los comentarios del artículo (ya que los comentarios extendidos no están permitidos aquí), ¡échale un vistazo! Para ser claros: soy el autor del artículo mencionado :)
Sergey Kolodiy
Tengo que estar de acuerdo con @BryanOakley. El "código comprobable" sugiere que sus preocupaciones están separadas: es posible probar un aspecto (módulo) sin interferencia de otros aspectos. Yo diría que esto es diferente de "ajustar sus convenciones de prueba específicas de soporte de proyecto". Esto es similar a los patrones de diseño: no deben ser forzados. El código que utiliza correctamente los patrones de diseño se consideraría código fuerte. Lo mismo se aplica a los principios de prueba. Si hacer que su código sea "comprobable" resulta en torcer el código de su proyecto en exceso, está haciendo algo mal.
Vince Emigh
68

¿Escribir 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 sea comprobable o no. No tener pruebas unitarias significa que no ha terminado con su código / función.

Fuera del camino, no diría que es importante escribir código comprobable, 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 en lo que la gente llama.

Entonces, para mí, siempre hay un conjunto de prioridades al escribir código:

  1. Haga que funcione : si el código no hace lo que debe hacer, no tiene valor.
  2. Que sea mantenible : si el código no se puede mantener, dejará de funcionar rápidamente.
  3. Hágalo flexible : si el código no es flexible, dejará de funcionar cuando inevitablemente surjan negocios y le pregunte si el código puede hacer XYZ.
  4. Hazlo rápido : más allá de un nivel base aceptable, el rendimiento es solo salsa.

Las pruebas unitarias ayudan a mantener el código, pero solo hasta cierto punto. Si hace que el código sea menos legible o más frágil para que las pruebas unitarias funcionen, eso 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 facilidad de mantenimiento. Para algo como el tiempo actual, hacer que sea flexible es bueno, pero perjudica la mantenibilidad al hacer que el código sea más difícil de usar y más complejo. Dado que la mantenibilidad es más importante, generalmente me equivocaré hacia el enfoque más simple, incluso si es menos comprobable.

Telastyn
fuente
44
Me gusta la relación que usted señala entre comprobable y flexible, eso hace que todo el problema sea más comprensible para mí. La flexibilidad permite que su código se adapte, pero necesariamente lo hace un poco más abstracto y menos intuitivo de entender, pero eso es un sacrificio valioso por los beneficios.
WannabeCoder
3
Dicho esto, a menudo veo métodos que deberían haber sido privados, obligados a nivel público o de paquete, para que el marco de prueba de la unidad pueda acceder a ellos directamente. Lejos de un enfoque ideal.
Jwenting
44
@WannabeCoder Por supuesto, solo vale la pena agregar flexibilidad cuando le ahorra tiempo al final. Es por eso que no escribimos todos los métodos contra una interfaz: la mayoría de las veces es más fácil reescribir el código en lugar de incorporar demasiada flexibilidad desde el principio. YAGNI sigue siendo un principio extremadamente poderoso: solo asegúrate de que sea lo que sea que "no vas a necesitar" sea, agregarlo retroactivamente no te dará más trabajo en promedio que implementarlo antes de tiempo. Es el código que no sigue a YAGNI el que tiene más problemas con flexibilidad en mi experiencia.
Luaan
3
"No tener pruebas unitarias significa que no ha terminado con su código / función" - No es cierto. La "definición de hecho" es algo que el equipo decide. Puede o no incluir algún grado de cobertura de prueba. Pero en ninguna parte hay un requisito estricto que diga que una función no se puede "hacer" si no hay pruebas para ello. El equipo puede optar por exigir pruebas o no.
Aroth
3
@Telastyn En más de 10 años de desarrollo, nunca tuve un equipo que exigiera un marco de prueba de unidad, y solo dos que incluso tenían uno (ambos tenían una cobertura deficiente). Un lugar requería un documento de Word sobre cómo probar la función que estaba escribiendo. Eso es. Quizás tengo mala suerte? No estoy en contra de la prueba unitaria (en serio, modifico el sitio SQA.SE, ¡soy una prueba unitaria muy profesional!), Pero no he encontrado que estén tan extendidos como dice su declaración.
corsiKa
50

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

A nadie le importan las pruebas en sí. Es un hecho triste de la vida que necesitamos grandes conjuntos de pruebas de regresión porque no somos lo suficientemente brillantes como para escribir código perfecto sin verificar constantemente nuestro equilibrio. Si pudiéramos, el concepto de pruebas sería desconocido, y todo esto no sería un problema. Ciertamente desearía poder. Pero la experiencia ha demostrado que casi todos nosotros no podemos, por lo tanto, las pruebas que cubren nuestro código son algo bueno, incluso si nos quitan tiempo de escribir código comercial.

¿Cómo las pruebas mejoran nuestro código comercial independientemente de las pruebas mismas? Al obligarnos a segmentar nuestra funcionalidad en unidades que se demuestra fácilmente que son correctas. Estas unidades también son más fáciles de corregir que las que de otro modo estaríamos tentados a escribir.

Su ejemplo de tiempo es un buen punto. Mientras solo tenga una función que devuelva la hora actual, podría pensar que no tiene sentido programarla. ¿Qué tan difícil puede ser hacer 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 regresa su función, no porque desconfíe de su currentMillis()llamada de una línea , sino porque necesita verificar a las personas que llaman de esa llamada en circunstancias controladas. Como puede ver, hacer que el código sea comprobable es útil incluso si solo, no parece merecer tanta atención.

Kilian Foth
fuente
Otro ejemplo es si desea extraer una parte del código de un proyecto a otro lugar (por cualquier razón). Cuanto más independientes sean las diferentes partes de la funcionalidad, más fácil será extraer exactamente la funcionalidad que necesita y nada más.
valenterry 01 de
10
Nobody cares about the tests themselves-- Hago. Encuentro que las pruebas son una mejor documentación de lo que hace el código que cualquier comentario o archivo léame.
jcollum 01 de
He estado leyendo lentamente sobre las prácticas de prueba durante un tiempo (como de alguna manera quién todavía no realiza pruebas unitarias) y tengo que decir, la última parte sobre la verificación de la llamada en circunstancias controladas y el código más flexible que viene con hizo que todo tipo de cosas encajaran en su lugar. Gracias.
plast1k
12

En algún momento, el valor debe ser inicializado, y ¿por qué no más cercano al consumo?

Porque es posible que deba 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 desee, no solo "ahora" (con "ahora" significa cuando llama al código).

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

Básicamente, si bien puede argumentar que no hay ningún incentivo para hacer que el código sea verificable 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 basado en la hora actual, al convertirlo en un parámetro implica que este propósito puede / debe cambiarse.

También podría argumentar que el propósito de este método es devolver algún valor basado en un valor de tiempo, y lo necesita para generarlo 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á.

utnapistim
fuente
10

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

En algún momento, el valor debe ser inicializado, y ¿por qué no más cercano al consumo?

Precisamente porque, en el ejemplo al que te estás refiriendo, hace que el código sea inestable. A menos que solo ejecute un subconjunto de sus pruebas en diferentes momentos del día. O reinicia 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 él.

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 aún más las responsabilidades, de modo que la parte fuera de su control ( DateTime.Now) tenga el menor impacto en el resto de su código. Hacerlo hará que el código anterior sea más simple, con el efecto secundario de ser sistemáticamente comprobable.

Eric King
fuente
1
Por lo tanto, tendría que hacer la prueba temprano en la mañana para verificar que obtiene un resultado de "Noche" cuando lo desea. Eso es difícil. Ahora suponga que desea verificar que el manejo de la fecha sea correcto el 29 de febrero de 2016 ... Y algunos programadores de iOS (y probablemente otros) están plagados de un error de principiante que arruina las cosas poco antes o después del comienzo del año, ¿cómo prueba para eso. Y por experiencia, comprobaría el manejo de la fecha el 2 de febrero de 2020.
gnasher729
1
@ gnasher729 Exactamente mi punto. "Hacer que este código sea comprobable" es un cambio simple que puede resolver muchos problemas (de prueba). Si no desea automatizar las pruebas, entonces supongo que el código se puede pasar como está. Pero sería mejor una vez que sea "comprobable".
Eric King
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 el código de llamada para inicializar y administrar una dependencia adicional, y aunque GetTimeOfDayes 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, escribir código comprobable 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 directamente en un límite de tiempo, haciendo que algunos elementos se representen con el tiempo anterior y otros con el tiempo posterior. También puede acelerar su programa al evitar repetidas llamadas al sistema para obtener la hora actual.

Por supuesto, esas mejoras arquitectónicas dependen en gran medida de que alguien note las oportunidades y las implemente. Uno de los mayores peligros de enfocarse tan de cerca en las unidades es perder de vista el panorama general.

Muchos marcos de pruebas unitarias le permiten a un mono parchear 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 ++. Examina esa habilidad en situaciones en las que parece que el costo de la prueba no vale la pena.

Karl Bielefeldt
fuente
+1: necesita mejorar el diseño y la arquitectura para facilitar la escritura de pruebas unitarias.
Bћовић
3
+ - lo que importa es la arquitectura de su código. Una prueba más fácil es solo un efecto secundario feliz.
gbjbaanb
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 hablando en términos generales de las características que contribuyen a la capacidad de prueba También contribuyen a un buen código, independientemente de la capacidad de prueba.

En términos generales, el código comprobable es código maleable. Está en fragmentos pequeños, discretos, cohesivos, por lo que se puede solicitar la reutilización de bits individuales. Está bien organizado y bien nombrado (para poder probar algunas funciones, presta más atención a los nombres; si no estuviera escribiendo pruebas, el nombre para 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 para su uso desde otros contextos que el propósito original previsto. Está SECO, por lo que es menos desordenado y más fácil de comprender.

Si. Es una buena práctica escribir código comprobable, incluso independientemente de las pruebas.

Carl Manaster
fuente
no está de acuerdo con que esté SECO: envolviendo GetCurrentTime en un método MyGetCurrentTime está repitiendo la llamada del sistema operativo sin ningún beneficio, excepto para ayudar a las herramientas de prueba. Ese es solo el ejemplo más simple, empeoran mucho en realidad.
gbjbaanb
1
"reparando la llamada del sistema operativo sin ningún beneficio": hasta que termine ejecutándose en un servidor con un reloj, hablando con un servidor aws en una zona horaria diferente, y eso rompe su código, y luego tiene que pasar por todo su código y actualícelo para usar MyGetCurrentTime, que en su lugar devuelve UTC. ; sesgo del reloj, horario de verano, y hay otras razones por las cuales podría no ser una buena idea confiar ciegamente en la llamada del sistema operativo, o al menos tener un solo punto donde puede colocar otro reemplazo.
Andrew Hill
8

Escribir código comprobable es importante si desea poder demostrar que su código realmente funciona.

Tiendo a estar de acuerdo con los sentimientos negativos sobre deformar su código en contorsiones atroces solo para ajustarlo a un marco de prueba particular.

Por otro lado, todos aquí, en algún momento u otro, han tenido que lidiar con esa función mágica de 1,000 líneas de largo que es atroz tener que lidiar, prácticamente no se puede tocar sin romper uno o más oscuros, no dependencias 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 indetectable por definición. La noción (que no carece de mérito) de que los marcos de prueba se han vuelto exagerados no debe tomarse como una licencia gratuita para escribir código no comprobable y de baja calidad, en mi opinión.

Los ideales de desarrollo basados ​​en pruebas tienden a empujarlo a escribir procedimientos de responsabilidad única, por ejemplo, y eso definitivamente es algo bueno. Personalmente, digo comprar con responsabilidad única, fuente única de verdad, alcance controlado (sin variables globales) y mantener las dependencias frágiles al mínimo, y su código será verificable. ¿Probable por algún marco de prueba específico? Quién sabe. Pero tal vez es el marco de prueba el que necesita ajustarse a un buen código, y no al revés.

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

Entonces, cerca de mi resumen, ¿el código comprobable es mejor ?

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

Pero sí creo que un código mejor tiende a ser también un código comprobable .

Y que si está hablando de software serio para su uso en esfuerzos serios, el envío de código no probado no es lo más responsable que podría hacer 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 códigos y es un poco tonto fingir lo contrario. ¿Cómo le gustaría haber sido astronauta en el transbordador espacial si el sistema de menús que lo conecta con los sistemas vitales en el transbordador no fue probado? ¿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 mil pruebas unitarias? Espero que no. Solo digo...

Craig
fuente
1
"un código mejor tiende a ser también código comprobable" Esta es la clave. Hacerlo comprobable no lo mejora. Mejorarlo a menudo lo hace comprobable, y las pruebas a menudo le brindan información que puede usar para mejorarlo, pero la mera presencia de pruebas no implica calidad, y hay excepciones (raras).
anaximander
1
Exactamente. Considera el contrapositivo. Si es un código no comprobable, no se prueba. Si no se prueba, ¿cómo sabe si funciona o no en una situación en vivo?
pjc50
1
Todo lo que prueba es que el código pasa las pruebas. De lo contrario, el código probado por la unidad estaría libre de errores y sabemos que ese no es el caso.
wobbily_col
@anaximander Exactamente. Existe al menos la posibilidad de que la mera presencia de pruebas sea una contraindicación que resulte en un código de peor calidad si todo el enfoque está en marcar las casillas de verificación. "¿Al menos siete pruebas unitarias para cada función?" "Cheque." Pero realmente creo que si el código es de calidad, será más fácil probarlo.
Craig
1
... pero hacer una burocracia a partir de las pruebas puede ser un desperdicio total y no producir información útil o resultados confiables. Sin importar; Desearía que alguien hubiera probado el error SSL Heartbleed , ¿sí? o el error de error de Apple Goto ?
Craig
5

Sin embargo, para mí, parece una exageración pasar el tiempo como argumento.

Tienes razón, y con la burla puedes hacer que el código sea comprobable y evitar 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 supongamos que desea probar lo que sucede durante un segundo intercalar. Como usted dice, para probar esto de la manera exagerada, tendría que 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 admite segundos bisiestos, 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 aún 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 pasar un valor para now). Evita esto utilizando un simulacro . A partir del código de producción original:

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 le da varios beneficios:

  • Estás probando time_of_day independientemente de sus dependencias.
  • Está probando la misma ruta de código que el 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 facilitarán cosas como esta. Por ejemplo, dado que debe referirse a la función simulada como una cadena, no puede hacer que los IDE la cambien automáticamente cuando time_of_daycomience a usar otra fuente por tiempo.

l0b0
fuente
FYI: su argumento predeterminado es incorrecto. Solo se definirá una vez, por lo que su función siempre devolverá la hora en que se evaluó por primera vez.
ahruss
4

Una calidad de código bien escrito es que es robusto de cambiar . Es decir, cuando se produce un cambio en los 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 todo nuestro otro código. En las pruebas unitarias, eliminamos gran parte de este entorno. Nuestro código ahora es 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 la producción.

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

cbojar
fuente
4

Según mi experiencia, una de las decisiones más importantes y de mayor alcance que tomas al crear un programa es cómo dividir el código en unidades (donde "unidades" se usa en su sentido más amplio). Si está utilizando un lenguaje OO basado en clases, debe dividir todos los mecanismos internos utilizados para implementar el programa en un cierto número de clases. Luego debe dividir el código de cada clase en varios 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á 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 dan como resultado menos errores (incluso antes de comenzar a probar y depurar). Hacen que sea más fácil cambiar y mantener. Curiosamente, resulta que una vez que encuentra un buen desglose, generalmente también es más fácil de probar que uno deficiente.

¿Por qué esto es tan? No creo que pueda entender y explicar todas las razones. Pero una razón es que un buen desglose significa invariablemente elegir un "tamaño de grano" moderado para las unidades de implementación. No desea agrupar 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 más fácil de escribir, pero también facilita la prueba.

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 las pruebas. Un buen diseño generalmente significa que cada unidad de implementación tendrá un número moderado de dependencias de las demás. 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 en su pregunta, no creo que "un buen diseño de código" sea equivalente a decir que todas las dependencias externas (como una dependencia en el reloj del sistema) siempre deben "inyectarse". Esa podría ser una buena idea, pero es un tema separado de lo que estoy describiendo aquí y no profundizaré en sus pros y sus contras.

Por cierto, incluso si realiza llamadas directas a las funciones del sistema que devuelven la hora actual, actúan en el sistema de archivos, etc., esto no significa que no pueda probar su código de forma aislada. El truco consiste en utilizar 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. (Esperemos que su tiempo de ejecución de idioma sea de código abierto y fácil de construir. Si la ejecución de su código implica un paso de enlace, es de esperar que también sea fácil controlar con qué bibliotecas está vinculado).

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

Alex D
fuente
1

Si va con principios SÓLIDOS , estará en el lado bueno, especialmente si extiende esto con KISS , DRY y YAGNI .

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

Si es un método más complejo en el que manipula datos y desea asegurarse de que funcionará incluso si tiene que cambiar la lógica interna, entonces sería una gran opción para un método de prueba. Muchas veces tuve que cambiar una pieza de código después de varios días / semanas / meses, y estaba 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 aún funcionaba. Así que estaba seguro de que mi cambio no rompió algún código antiguo en producción.

Otro aspecto de escribir pruebas 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 .

BtD
fuente