unit-testing clojure

unit testing - ¿Cómo administro efectivamente un código base de Clojure?



unit-testing (3)

Un compañero de trabajo y yo somos novatos de Clojure. Comenzamos un proyecto hace un par de meses, pero rápidamente descubrimos que nos era difícil lidiar con nuestro código base. En 500 LOC, básicamente no teníamos idea de dónde comenzar con la depuración, cuando las cosas iban mal (lo cual era frecuente). En lugar de pares, las funciones obtenían listas, o números, o lo que sea que tengas.

Ahora estamos comenzando un proyecto nuevo pero relacionado y estamos migrando gran parte del código anterior. Pero de nuevo estamos golpeando una pared.

Nos preguntamos, ¿cómo administramos efectivamente un proyecto de Clojure, especialmente cuando hacemos cambios en el código existente?

Lo que se nos ha ocurrido:

  • Uso liberal de pruebas unitarias.
  • Uso liberal de pre, post-condiciones.
  • Declaraciones de tipo informal en comentarios de funciones.
  • use defrecord / defstruct / defprotocol para implementar un modelo de datos, lo que realmente simplificaría las pruebas

Pero las condiciones previas no parecen ser usadas muy a menudo. Pruebas unitarias + comentarios solo ayudarán mucho. Y parece que los programadores de Clojure no suelen implementar modelos de datos formales.

¿Simplemente no conseguimos Clojure? ¿Cómo saben los programadores de Clojure que su código es robusto y correcto?


abstracciones compostables simples

"Es mejor tener 100 funciones operando en una estructura de datos que tener 10 funciones operando en 10 estructuras de datos". - Alan J. Perlis

Para mí todo se trata de componer funciones simples . Intente dividir cada función en las unidades más pequeñas que pueda y luego tenga otra función que las componga para hacer el trabajo que necesita. Usted sabe que está en buena forma, cada función se puede probar de forma independiente. Si le subes demasiado a las macros, esto puede dificultar este paso porque las macros se componen de manera diferente.

SECO, en serio, simplemente no te repitas

comenzando con funciones bien descompuestas en un grupo de espacios de nombres; Cada vez que necesito una de las partes compuestas en otra parte, "elevo" esa función hasta una biblioteca incluida por ambos espacios de nombres. De esta manera, sus abstracciones de uso común evolucionan a lo largo del proyecto en "marco suficiente". Es muy difícil hacer esto a menos que realmente tengas abstracciones compactables discretas.


Creo que esto es realmente un área en evolución. Clojure no ha existido desde hace mucho tiempo para todas las mejores prácticas y herramientas asociadas para la gestión de una base de código grande que se desarrollará todavía.

Algunas sugerencias de mi experiencia:

  • Estructure su código de manera "de abajo hacia arriba" : en general, la forma en que quiera estructurar su código tendrá el código de "utilidad" en la parte superior del archivo (o importado de otro espacio de nombres) y el código de "lógica de negocios" que utiliza estas funciones de utilidad hacia el final del archivo. Si esto parece difícil de hacer, entonces es probable que sea un indicio de que su código necesita una refactorización.

  • Pruebas como ejemplos : el código de prueba en clojure funciona muy bien tanto para verificar la cordura de su código como para la documentación (por ejemplo, "¿qué tipo de parámetro espera esta función?"). Si contrae un error, consulte sus pruebas para verificar sus suposiciones y escriba un par de pruebas nuevas para eliminar lo que está mal.

  • Mantenga las funciones simples y compóngalas . Es una extensión del " principio de responsabilidad única " a la programación funcional. Considero más de 5-10 líneas en una función de Clojure como un olor principal del código (si esto parece extremo, solo recuerda que probablemente puedas lograr tanto en 5-10 líneas de Clojure como puedas con 50-100 líneas de Java / DO#)

  • Cuidado con los "hábitos imperativos" : cuando empecé a usar Clojure, escribí muchos códigos de pseudo-imperativos en Clojure. Un ejemplo sería emular un bucle for con "dotimes" y acumular algún resultado dentro de un átomo. Esto puede ser doloroso: no es idiomático, es confuso y, por lo general, existe una forma funcional mucho más inteligente, más sencilla y menos propensa a los errores de hacerlo. Esto requiere práctica, pero vale la pena a largo plazo ...

  • Depurar en el REPL : por lo general, cuando encuentro un problema, la codificación en el REPL es la forma más fácil de eliminarla. En general, esto significa ejecutar algunas partes específicas del algoritmo más grande para verificar las suposiciones, etc.

  • Refactorice las funciones de utilidad comunes : probablemente encontrará un montón de elementos comunes o estructura repetida en muchas funciones. Vale la pena extraer esto en una función o macro que puede reutilizar en otros lugares o proyectos; de esa manera, puede probarla de manera mucho más rigurosa y tener los beneficios en múltiples lugares. ¡Puntos de bonificación si puedes conseguirlo todo el tiempo en sentido ascendente en Clojure! Si lo hace lo suficientemente bien, entonces su base de código principal será extremadamente sucinta y, por lo tanto, fácil de administrar, no contiene nada más que el código genuinamente específico del dominio.


Perdón por desenterrar esta vieja pregunta, las respuestas de Mikera y Arthur son excelentes, pero también es algo en lo que me he preguntado mientras estaba aprendiendo Clojure, y pensé en mencionar cómo organizamos los archivos.

De manera similar a garantizar que cada función tenga un solo trabajo, agrupamos las funciones relacionadas en espacios de nombres para que sea más fácil navegar por el código. Por lo tanto, es posible que tengamos un espacio de nombres para las funciones que brindan acceso a una base de datos en particular o que brindan una colección de utilidades relacionadas con HTTP. Esto mantiene cada archivo relativamente pequeño, y hace que las pruebas sean más fáciles de encontrar. También hace que la refactorización sea mucho más sencilla. Esto no es nada nuevo, pero vale la pena tenerlo en cuenta.