He leído que usar "nuevo" en un constructor (para cualquier otro objeto que no sea de valor simple) es una mala práctica, ya que hace que las pruebas unitarias sean imposibles (ya que esos colaboradores también deben crearse y no se pueden burlar). Como no tengo mucha experiencia en pruebas unitarias, estoy tratando de reunir algunas reglas que aprenderé primero. Además, ¿es esta una regla que generalmente es válida, independientemente del idioma utilizado?
unit-testing
constructors
Ezoela Vacca
fuente
fuente
new
significa cosas diferentes en diferentes idiomas.new
palabra clave. ¿Sobre qué idiomas preguntas?Respuestas:
Siempre hay excepciones, y discrepo con el 'siempre' en el título, pero sí, esta guía es generalmente válida y también se aplica fuera del constructor.
El uso de new en un constructor viola la D en SOLID (principio de inversión de dependencia). Hace que su código sea difícil de probar porque las pruebas unitarias tienen que ver con el aislamiento; Es difícil aislar la clase si tiene referencias concretas.
Sin embargo, no se trata solo de pruebas unitarias. ¿Qué sucede si quiero apuntar un repositorio a dos bases de datos diferentes a la vez? La capacidad de pasar en mi propio contexto me permite instanciar dos repositorios diferentes que apuntan a diferentes ubicaciones.
No usar new en el constructor hace que su código sea más flexible. Esto también se aplica a los lenguajes que pueden usar construcciones que no sean
new
para la inicialización de objetos.Sin embargo, claramente, debe usar su buen juicio. Hay muchas ocasiones en que está bien usarlo
new
, o donde sería mejor no hacerlo, pero no tendrá consecuencias negativas. En algún momento en algún lugar,new
tiene que ser llamado. Solo tenga mucho cuidado al llamarnew
dentro de una clase de la que dependen muchas otras clases.Hacer algo como inicializar una colección privada vacía en su constructor está bien, e inyectarlo sería absurdo.
Cuantas más referencias tenga una clase, más cuidadoso deberías ser para no llamar
new
desde dentro.fuente
head = null
alguna manera mejora el código? ¿Por qué sería mejor dejar las colecciones incluidas como nulas y crearlas a pedido que hacerlo en el constructor?Si bien estoy a favor de usar el constructor para simplemente inicializar la nueva instancia en lugar de crear varios otros objetos, los objetos auxiliares están bien, y debe usar su juicio sobre si algo es un auxiliar interno o no.
Si la clase representa una colección, puede tener una matriz auxiliar interna o una lista o hashset. Se usaría
new
para crear estos ayudantes y se consideraría bastante normal. La clase no ofrece inyección para usar diferentes ayudantes internos y no tiene ninguna razón para hacerlo. En este caso, desea probar los métodos públicos del objeto, que podrían ir a acumular, eliminar y reemplazar elementos en la colección.En cierto sentido, la construcción de clase de un lenguaje de programación es un mecanismo para crear abstracciones de nivel superior, y creamos tales abstracciones para cerrar la brecha entre el dominio del problema y las primitivas del lenguaje de programación. Sin embargo, el mecanismo de clase es solo una herramienta; varía según el lenguaje de programación y, algunas abstracciones de dominio, en algunos lenguajes, simplemente requieren múltiples objetos al nivel del lenguaje de programación.
En resumen, debe juzgar si la abstracción simplemente requiere uno o más objetos internos / auxiliares, mientras la persona que llama todavía la ve como una sola abstracción o si los otros objetos estarían mejor expuestos a la persona que llama para crear control de dependencias, lo que se sugiere, por ejemplo, cuando la persona que llama ve estos otros objetos al usar la clase
fuente
No todos los colaboradores son lo suficientemente interesantes como para realizar pruebas unitarias por separado, podría (indirectamente) probarlos a través de la clase de alojamiento / creación de instancias. Es posible que esto no se alinee con la idea de algunas personas de necesitar evaluar cada clase, cada método público, etc. especialmente cuando se realiza la prueba después. Cuando use TDD, puede refactorizar este 'colaborador' extrayendo una clase donde ya está completamente bajo prueba desde su primer proceso de prueba.
fuente
Not all collaborators are interesting enough to unit-test separately
Fin de la historia :-), este caso es posible y nadie se atrevió a mencionarlo. @Joppe, te animo a elaborar un poco la respuesta. Por ejemplo, podría agregar algunos ejemplos de clases que son simples detalles de implementación (no aptos para reemplazo) y cómo se pueden extraer más adelante si lo consideramos necesario.new File()
para hacer cualquier cosa relacionada con el archivo, no tiene sentido prohibir esa llamada. ¿Qué vas a hacer, escribir pruebas de regresión contra elFile
módulo stdlib ? No es probable. Por otro lado, recurrirnew
a una clase inventada por uno mismo es más dudoso.new File()
.Tenga cuidado al aprender "reglas" para problemas que nunca ha encontrado. Si se encuentra con alguna "regla" o "práctica recomendada", le sugiero que busque un ejemplo simple de juguete de dónde se supone que se debe usar esta regla, e intente resolver ese problema usted mismo , ignorando lo que dice la "regla".
En este caso, podría intentar crear 2 o 3 clases simples y algunos comportamientos que deberían implementar. Implemente las clases de la manera que se sienta natural y escriba una prueba unitaria para cada comportamiento. Haga una lista de cualquier problema que haya encontrado, por ejemplo, si comenzó con cosas que funcionan de una manera, luego tuvo que regresar y cambiarlo más tarde; si te confundiste sobre cómo se supone que las cosas encajan entre sí; si te molesta escribir boilerplate; etc.
Luego intente resolver el mismo problema siguiendo la "regla". Nuevamente, haga una lista de los problemas que encontró. Compare las listas y piense qué situaciones podrían ser mejores al seguir la regla y cuáles no.
En cuanto a su pregunta real, tiendo a favorecer un enfoque de puertos y adaptadores , donde hacemos una distinción entre "lógica central" y "servicios" (esto es similar a distinguir entre funciones puras y procedimientos efectivos).
La lógica central se trata de calcular cosas "dentro" de la aplicación, en función del dominio del problema. Podría contener clases como
User
,Document
,Order
,Invoice
, etc bien de que tenga clases básicas llamannew
para otras clases principales, ya que son detalles de implementación "internos". Por ejemplo, crear unOrder
poder también crea unInvoice
y unDocument
detalle de lo que se ordenó. No hay necesidad de burlarse de estos durante las pruebas, ¡porque estas son las cosas reales que queremos probar!Los puertos y adaptadores son la forma en que la lógica central interactúa con el mundo exterior. Aquí es donde cosas como
Database
,ConfigFile
,EmailSender
, etc. viven. Estas son las cosas que dificultan las pruebas, por lo que es recomendable crearlas fuera de la lógica central y pasarlas según sea necesario (ya sea con inyección de dependencia o como argumentos de métodos, etc.).De esta manera, la lógica central (que es la parte específica de la aplicación, donde vive la lógica comercial importante y está sujeta a la mayor rotación) se puede probar por sí sola, sin tener que preocuparse por las bases de datos, archivos, correos electrónicos, etc. Simplemente podemos pasar algunos valores de ejemplo y verificar que obtenemos los valores de salida correctos.
Los puertos y adaptadores se pueden probar por separado, utilizando simulacros para la base de datos, el sistema de archivos, etc. sin tener que preocuparse por la lógica empresarial. Simplemente podemos pasar algunos valores de ejemplo y asegurarnos de que estén almacenados / leídos / enviados / etc. apropiadamente.
fuente
Permítame responder la pregunta, reuniendo lo que considero que son los puntos clave aquí. Citaré a algún usuario por brevedad.
Sí, el uso de
new
constructores internos a menudo conduce a fallas de diseño (por ejemplo, acoplamiento apretado) que hace que nuestro diseño sea rígido. Difícil de probar sí, pero no imposible. La propiedad en juego aquí es la resistencia (tolerancia a los cambios) 1 .Sin embargo, la cita anterior no siempre es cierta. En algunos casos, podría haber clases destinadas a estar estrechamente acopladas . David Arno ha comentado una pareja.
Exactamente. Algunas clases (por ejemplo, clases internas) podrían ser simples detalles de implementación de la clase principal. Estos deben ser probados junto con la clase principal, y no son necesariamente reemplazables o extensibles.
Además, si nuestro culto SÓLIDO nos hace extraer estas clases , podríamos estar violando otro buen principio. La llamada Ley de Deméter . Lo cual, por otro lado, considero que es realmente importante desde el punto de vista del diseño.
Entonces, la respuesta probable, como de costumbre, es depende . Usar
new
constructores internos puede ser una mala práctica. Pero no siempre de manera sistemática.Por lo tanto, nos lleva a evaluar si las clases son detalles de implementación (la mayoría de los casos no lo serán) de la clase principal. Si lo son, déjelos en paz. Si no lo son, considere técnicas como la raíz de composición o la inyección de dependencia por contenedores IoC .
1: el objetivo principal de SOLID no es hacer que nuestro código sea más verificable. Es para hacer que nuestro código sea más tolerante a los cambios. Más flexible y, en consecuencia, más fácil de probar.
Nota: David Arno, TheWhisperCat, espero que no te importe que te haya citado.
fuente
Como un ejemplo simple, considere el siguiente pseudocódigo
Dado que
new
es un simple detalle de implementación deFoo
, y ambosFoo::Bar
yFoo::Baz
son parte deFoo
, cuando las pruebas unitarias deFoo
no tienen sentido burlarse de las partesFoo
. Solo se burla de las partes exterioresFoo
cuando realiza pruebas unitariasFoo
.fuente
Sí, usar 'nuevo' en las clases raíz de su aplicación es un olor a código. Significa que está bloqueando a la clase para que use una implementación específica, y no podrá sustituirla por otra. Opte siempre por inyectar la dependencia en el constructor. De esa manera, no solo podrá inyectar fácilmente dependencias simuladas durante las pruebas, sino que hará que su aplicación sea mucho más flexible al permitirle sustituir rápidamente diferentes implementaciones si es necesario.
EDITAR: Para los votantes que votan abajo: aquí hay un enlace a un libro de desarrollo de software que marca 'nuevo' como posible olor a código: https://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 # v = onepage & q = new% 20keyword% 20code% 20smell & f = false
fuente
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.
¿Por qué es esto un problema? No todas las dependencias de un árbol de dependencias deberían poder reemplazarsenew
palabra clave no es una mala práctica, y nunca lo fue. Lo importante es cómo usas la herramienta. No utiliza un mazo, por ejemplo, donde un martillo de bola será suficiente.