¿Hay algún valor real en la unidad que prueba un controlador en ASP.NET MVC?

33

Espero que esta pregunta dé algunas respuestas interesantes porque es una que me ha molestado por un tiempo.

¿Hay algún valor real en la unidad que prueba un controlador en ASP.NET MVC?

Lo que quiero decir con eso es que, la mayoría de las veces, (y no soy un genio), mis métodos de controlador son, incluso en su forma más compleja, algo como esto:

public ActionResult Create(MyModel model)
{
    // start error list
    var errors = new List<string>();

    // check model state based on data annotations
    if(ModelState.IsValid)
    {
        // call a service method
        if(this._myService.CreateNew(model, Request.UserHostAddress, ref errors))
        {
            // all is well, data is saved, 
            // so tell the user they are brilliant
            return View("_Success");
        }
    }

    // add errors to model state
    errors.ForEach(e => ModelState.AddModelError("", e));

    // return view
    return View(model);
}

La mayor parte del trabajo pesado se realiza mediante la tubería MVC o mi biblioteca de servicio.

Entonces, tal vez las preguntas para hacer podrían ser:

  • ¿Cuál sería el valor de la unidad que prueba este método?
  • ¿no se rompería Request.UserHostAddressy ModelStatecon una NullReferenceException? ¿Debo tratar de burlarme de estos?
  • si refractaria este método en un "ayudante" reutilizable (¡lo cual probablemente debería hacer, teniendo en cuenta cuántas veces lo hago!), ¿probaría que incluso valdría la pena cuando todo lo que realmente estoy probando es principalmente la "tubería" que, presumiblemente, ¿ha sido probado a una pulgada de su vida por Microsoft?

Creo que mi punto es realmente , hacer lo siguiente parece completamente inútil e incorrecto

[TestMethod]
public void Test_Home_Index()
{
    var controller = new HomeController();
    var expected = "Index";
    var actual = ((ViewResult)controller.Index()).ViewName;
    Assert.AreEqual(expected, actual);
}

Obviamente estoy siendo obtuso con este ejemplo exageradamente inútil, pero ¿alguien tiene alguna sabiduría para agregar aquí?

Estoy deseando que llegue ... Gracias.

Piscinas de hígado Número 9
fuente
Creo que el retorno de la inversión (ROI) en esa prueba en particular no vale la pena, a menos que tenga tiempo y dinero infinitos. Escribiría pruebas que Kevin señala para verificar cosas que tienen más probabilidades de romperse o que lo ayudarán a refactorizar algo con confianza o a garantizar que se produzca la propagación de errores como se esperaba. Las pruebas de tuberías, si es necesario, se pueden hacer a un nivel más global / de infraestructura y a nivel de métodos individuales serán de poco valor. Sin decir que no tienen valor, sino "poco". Entonces, si proporciona un buen retorno de la inversión en su caso, ¡anímate, de lo contrario, captura el pez más grande primero!
Mrchief

Respuestas:

18

Incluso para algo tan simple, una prueba unitaria tendrá múltiples propósitos

  1. Confianza, lo que se escribió se ajusta al resultado esperado. Puede parecer trivial verificar que devuelve la vista correcta, pero el resultado es evidencia objetiva de que se cumplió el requisito
  2. Pruebas de regresión. Si el método Create necesita cambiar, aún tiene una prueba unitaria para la salida esperada. Sí, la salida podría cambiar y eso da como resultado una prueba frágil, pero aún así es una verificación contra el control de cambio no administrado

Para esa acción en particular, probaría lo siguiente

  1. ¿Qué sucede si _myService es nulo?
  2. ¿Qué sucede si _myService.Create arroja una excepción, arroja otras específicas para manejar?
  3. ¿Un _myService.Create exitoso devuelve la vista _Success?
  4. ¿Se propagan los errores hasta ModelState?

Usted señaló la comprobación de Solicitud y Modelo para NullReferenceException y creo que ModelState.IsValid se encargará de manejar NullReference para Modelo.

Mockear la solicitud le permite protegerse contra una solicitud nula, que generalmente es imposible en la producción, creo, pero puede suceder en una prueba de unidad. En una prueba de integración, le permitiría proporcionar diferentes valores de UserHostAddress (una solicitud aún es ingresada por el usuario en lo que respecta al control y debe probarse en consecuencia)

Kevin
fuente
Hola Kevin, gracias por tomarte el tiempo de responder. Voy a dejarlo un rato para ver si alguien más entra con algo, pero hasta ahora el tuyo es el más lógico / claro.
LiverpoolsNumber9
Spifty Me alegra que te haya ayudado.
Kevin
3

Mis controladores también son muy pequeños. La mayor parte de la "lógica" en los controladores se maneja utilizando atributos de filtro (incorporados y escritos a mano). Por lo tanto, mi controlador generalmente solo tiene un puñado de trabajos:

  • Cree modelos a partir de cadenas de consulta HTTP, valores de formulario, etc.
  • Realizar alguna validación básica
  • Llamar a mi capa de datos o negocios
  • Generar un ActionResult

ASP.NET MVC realiza la mayor parte del enlace del modelo automáticamente. DataAnnotations también maneja la mayor parte de la validación para mí.

Incluso con tan poco para probar, todavía los escribo normalmente. Básicamente, pruebo que se llama a mis repositorios y que ActionResultse devuelve el tipo correcto . Tengo un método conveniente para ViewResultasegurarme de que se devuelva la ruta de vista correcta y que el modelo de vista se vea como yo esperaba. Tengo otro para verificar que el controlador / acción correcto está configurado RedirectToActionResult. Tengo otras pruebas para JsonResult, etc. etc.

Un resultado desafortunado de subclasificar la Controllerclase es que proporciona muchos métodos convenientes que utilizan HttpContextinternamente. Esto dificulta la prueba unitaria del controlador. Por esta razón, normalmente pongo HttpContextllamadas dependientes detrás de una interfaz y paso esa interfaz al constructor del controlador (uso la extensión web Ninject para crear mis controladores por mí). Esta interfaz es generalmente donde pego las propiedades de ayuda para acceder a la sesión, los ajustes de configuración, el IPrinciple y los ayudantes de URL.

Esto requiere mucha diligencia debida, pero creo que vale la pena.

Travis Parks
fuente
Gracias por tomarse el tiempo para responder, pero 2 cuestiones de inmediato. En primer lugar, los "métodos de ayuda" en las pruebas unitarias son muy peligrosos. En segundo lugar, "prueba de que se llaman mis repositorios", ¿quieres decir mediante inyección de dependencia?
LiverpoolsNumber9
¿Por qué los métodos de conveniencia serían peligrosos? Tengo una BaseControllerTestsclase donde viven todos. Me burlo de mis repositorios. Los conecto usando Ninject.
Travis Parks
¿Qué sucede si ha cometido un error o una suposición incorrecta en sus ayudantes? Mi otro punto fue que, solo una prueba de integración (es decir, de extremo a extremo) podría "probar" si se llama a sus repositorios. En una prueba unitaria, de todos modos, "renovaría" o se burlaría de sus repositorios manualmente.
LiverpoolsNumber9
Pasas el repositorio al constructor. Te burlas durante la prueba. Usted se asegura de que se actúe el simulacro como se esperaba. Los ayudantes simplemente deconstruyen ActionResults para inspeccionar las URL, modelos, etc. pasados
Travis Parks
Ok, bastante justo: no entendí un poco lo que querías decir con "prueba de que se llaman mis repositorios".
LiverpoolsNumber9
2

Obviamente, algunos controladores son mucho más complejos que eso, pero se basan únicamente en su ejemplo:

¿Qué sucede si myService lanza una excepción?

Como nota al margen.

Además, cuestionaría la sabiduría de pasar una lista por referencia (es innecesario ya que C # pasa por referencia de todos modos, pero incluso si no fuera así), pasando una acción de Acción de error (Acción) que el servicio puede usar para enviar mensajes de error a que podría manejarse como lo desee (tal vez desee agregarlo a la lista, tal vez desee agregar un error de modelo, tal vez desee registrarlo).

En tu ejemplo:

en lugar de errores de referencia, haga (string s) => ModelState.AddModelError ("", s) por ejemplo.

Miguel
fuente
Vale la pena mencionar, esto asume que su servicio reside en la misma aplicación, de lo contrario entrarán en juego problemas de serialización.
Michael
El servicio estaría en un dll separado. Pero de todos modos, probablemente tengas razón con respecto al "árbitro". En su otro punto, no importa si myService arroja una excepción. No estoy probando myService: probaría los métodos allí por separado. Estoy hablando de probar puramente la "unidad" de ActionResult con (probablemente) un myService burlado.
LiverpoolsNumber9
¿Tiene un mapeo 1: 1 entre su servicio y su controlador? Si no, ¿algunos controladores usan múltiples llamadas de servicio? Si es así, ¿podrías probar esas interacciones?
Michael
No. Al final del día, los métodos de servicio toman entrada (generalmente un modelo de vista o incluso cadenas / ints), "hacen cosas", luego devuelven un bool / errores si es falso. No hay un enlace "directo" entre los controladores y la capa de servicio. Están completamente separados.
LiverpoolsNumber9
Sí, entiendo que estoy tratando de entender el modelo relacional entre los controladores y la capa de servicio, suponiendo que cada controlador no tenga un método de servicio correspondiente, entonces sería lógico que algunos controladores necesiten hacer uso de ¿Más de un método de servicio?
Michael