Compartir a través de


Habilitar las pruebas unitarias automatizadas

por Microsoft

Descargar PDF

Este es el paso 12 de un tutorial de la aplicación "NerdDinner" gratuito que le guía durante el proceso de compilación de una aplicación web pequeña, pero completa, con ASP.NET MVC 1.

En el paso 12 se muestra cómo desarrollar un conjunto de pruebas unitarias automatizadas que comprueban la funcionalidad de NerdDinner y que nos darán la confianza para realizar cambios y mejoras en la aplicación en el futuro.

Si utiliza ASP.NET MVC 3, le recomendamos que siga los tutoriales Introducción a MVC 3 o Tienda de música de MVC.

NerdDinner Paso 12: Pruebas unitarias

Vamos a desarrollar un conjunto de pruebas unitarias automatizadas que comprueban la funcionalidad de NerdDinner y que nos darán la confianza para realizar cambios y mejoras en la aplicación en el futuro.

El porqué de las pruebas unitarias

En el coche hacia el trabajo, una mañana tiene un repentino flash de inspiración sobre una aplicación en la que está trabajando. Se da cuenta de que hay un cambio que, si lo implementa, mejorará considerablemente la aplicación. Puede ser una refactorización que limpie el código, agregue una nueva característica o corrija un error.

La pregunta que le viene cuando llega al ordenador es: "¿cómo de segura es esta mejora?" ¿Qué ocurre si hacer el cambio tiene efectos secundarios o rompe algo? El cambio puede ser sencillo y tardar solo unos minutos en implementarse, pero ¿qué ocurre si se tardan horas en probar manualmente todos los escenarios de la aplicación? ¿Qué ocurre si olvida cubrir un escenario y una aplicación rota entra en producción? ¿Realmente vale la pena realizar esta mejora?

Las pruebas unitarias automatizadas proporcionan una red de seguridad que permite mejorar continuamente las aplicaciones y evitan el miedo con el código en el que está trabajando. Tener pruebas automatizadas que comprueben rápidamente la funcionalidad permite codificar con confianza y realizar mejoras que, de otro modo, no se hubiera sentido cómodo haciendo. También ayudan a crear soluciones más fáciles de mantener y de mayor duración, lo que conduce a una rentabilidad de la inversión mucho mayor.

El marco ASP.NET MVC facilita y simplifica la funcionalidad de la aplicación de las pruebas unitarias. También habilita un flujo de trabajo de desarrollo controlado por pruebas (TDD) que permite el desarrollo basado en pruebas iniciales.

Proyecto NerdDinner.Tests

Al crear la aplicación NerdDinner al principio de este tutorial, apareció un cuadro de diálogo donde se nos preguntaba si queríamos crear un proyecto de prueba unitaria para continuar con el proyecto de aplicación:

Screenshot of the Create Unit Test Project dialog. Yes, create a unit test project is selected.Nerd Dinner dot Tests is written as the Test project name.

Hemos mantenido el botón de radio "Sí, crear un proyecto de prueba unitaria" seleccionado, lo que supuso que se agregara un proyecto "NerdDinner.Tests" a nuestra solución:

Screenshot of the Solution Explorer navigation tree. Nerd Dinner dot Tests is selected.

El proyecto NerdDinner.Tests hace referencia al ensamblado de proyecto de la aplicación NerdDinner y nos permite agregarle fácilmente pruebas automatizadas que comprueben la funcionalidad de la aplicación.

Creación de pruebas unitarias para nuestra clase de modelo de comida

Vamos a agregar algunas pruebas al proyecto NerdDinner.Tests para comprobar la clase Dinner que creamos al generar nuestra capa de modelo.

Comenzaremos creando una nueva carpeta dentro de nuestro proyecto de prueba denominado "Models", donde colocaremos nuestras pruebas relacionadas con el modelo. A continuación, haremos clic con el botón derecho en la carpeta y elegiremos el comando de menú Agregar>Nueva prueba. Se abrirá el cuadro de diálogo "Agregar nueva prueba".

Elegiremos crear una "Prueba unitaria" y le asignaremos el nombre "DinnerTest.cs":

Screenshot of the Add New Test dialog box. Unit Test is highlighted. Dinner Test dot c s is written as the Test Name.

Al hacer clic en el botón "Aceptar", Visual Studio agregará (y abrirá) un archivo DinnerTest.cs al proyecto:

Screenshot of the Dinner Test dot c s file in Visual Studio.

La plantilla de prueba unitaria predeterminada de Visual Studio tiene un montón de código reutilizable dentro de ella que encuentro un poco desordenado. Vamos a limpiarlo para que solo contenga el código siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NerdDinner.Models;

namespace NerdDinner.Tests.Models {
 
    [TestClass]
    public class DinnerTest {

    }
}

El atributo [TestClass] de la clase DinnerTest anterior lo identifica como una clase que contendrá pruebas, así como código de inicialización y desmontaje de pruebas opcionales. Podemos definir pruebas dentro de ella agregando métodos públicos con un atributo [TestMethod] en ellos.

A continuación se muestran las primeras de dos pruebas que agregaremos a la clase Dinner. La primera comprueba que nuestra comida no sea válida si se crea una nueva comida sin que todas las propiedades se establezcan correctamente. La segunda comprueba que nuestra comida sea válida cuando una comida tenga todas las propiedades establecidas con valores válidos:

[TestClass]
public class DinnerTest {

    [TestMethod]
    public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect() {

        //Arrange
        Dinner dinner = new Dinner() {
            Title = "Test title",
            Country = "USA",
            ContactPhone = "BOGUS"
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsFalse(isValid);
    }

    [TestMethod]
    public void Dinner_Should_Be_Valid_When_All_Properties_Correct() {
        
        //Arrange
        Dinner dinner = new Dinner {
            Title = "Test title",
            Description = "Some description",
            EventDate = DateTime.Now,
            HostedBy = "ScottGu",
            Address = "One Microsoft Way",
            Country = "USA",
            ContactPhone = "425-703-8072",
            Latitude = 93,
            Longitude = -92,
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsTrue(isValid);
    }
}

Observará anteriormente que nuestros nombres de prueba son muy explícitos (y algo detallados). Estamos haciendo esto porque es posible que terminemos creando cientos o miles de pruebas pequeñas, y queremos que sea fácil determinar rápidamente la intención y el comportamiento de cada una de ellas (especialmente cuando vemos una lista de errores en un ejecutor de pruebas). Los nombres de prueba deben denominarse después de la funcionalidad que están probando. Anteriormente usamos un patrón de nomenclatura "Noun_Should_Verb".

Estamos estructurando las pruebas mediante el patrón de pruebas "AAA", del inglés, organizar, actuar, declarar:

  • Organizar: configurar la unidad que se está probando
  • Actuar: hacer funcionar la unidad en prueba y capturar los resultados
  • Declarar: comprobar del comportamiento

Al escribir pruebas, queremos evitar que cada una haga demasiado. En su lugar, cada prueba debe comprobar solo un concepto (lo que facilitará enormemente la identificación de la causa de los errores). Una buena guía es intentar tener una sola instrucción Assert para cada prueba. Si tiene más de una instrucción Assert en un método de prueba, asegúrese de que todas se usan para probar el mismo concepto. Si duda, haga otra prueba.

Ejecutar pruebas

Visual Studio 2008 Professional (y ediciones posteriores) incluye un ejecutor de pruebas integrado para ejecutar proyectos de pruebas unitarias de Visual Studio en el entorno de desarrollo integrado. Podemos seleccionar el comando de menú Prueba->Ejecutar-Todas>Pruebas de la solución (o escribir Ctrl R, A) para ejecutar todas nuestras pruebas unitarias. O bien, también podemos colocar el cursor dentro de una clase de prueba específica o un método de prueba y usar el comando Prueba->Ejecutar->Pruebas en el contexto actual (o escribir Ctrl R, T) para ejecutar un subconjunto de pruebas unitarias.

Vamos a colocar el cursor dentro de la clase DinnerTest y escribir "Ctrl R, T" para ejecutar las dos pruebas que acabamos de definir. Cuando hagamos esto, aparecerá una ventana "Resultados de pruebas" en Visual Studio y veremos los resultados de la ejecución de las pruebas en ella:

Screenshot of the Test Results window in Visual Studio. The results of the test run are listed within.

Nota: La ventana de resultados de pruebas de VS no muestra la columna Nombre de clase de forma predeterminada. Para agregarlo, haga clic con el botón derecho en la ventana Resultados de pruebas y use el comando de menú Agregar o quitar columnas.

Nuestras dos pruebas tomaron solo una fracción de un segundo para ejecutarse y, como puede ver, ambas se superaron. Ahora podemos continuar y hacer más creando otras que comprueben validaciones de reglas específicas, así como que cubran los dos métodos auxiliares : IsUserHost() e IsUserRegistered() que agregamos a la clase Dinner. Tener todas estas pruebas en vigor para la clase Dinner hará que sea mucho más fácil y seguro agregarle nuevas reglas de negocio y validaciones en el futuro. Podemos agregar nuestra nueva lógica de reglas a Dinner y, en segundos, comprobar que no haya roto ninguna de nuestras funcionalidades lógicas anteriores.

Observe que el uso de un nombre de prueba descriptivo agiliza la comprensión de lo que comprueba cada prueba. Recomiendo usar el comando de menú Tools->Options, abrir la pantalla de configuración Herramientas de pruebas->Ejecución de pruebas y marcar la casilla "Al hacer doble clic en el resultado de una prueba unitaria que sea Error o No concluyente, se muestra el punto de error de la prueba". Esto le permitirá hacer doble clic en un error en la ventana de resultados de la prueba y saltar inmediatamente al error de aserción.

Creación de pruebas unitarias de DinnersController

Ahora vamos a crear algunas pruebas unitarias que comprueben la funcionalidad de DinnersController. Empezaremos haciendo clic con el botón derecho en la carpeta "Controllers" del proyecto de prueba y elegiremos el comando de menú Add->New Test. Crearemos una "prueba unitaria" y la denominaremos "DinnersControllerTest.cs".

Crearemos dos métodos de prueba que comprueben el método de acción Details() en DinnersController. La primera comprobará que se devuelve una vista cuando se solicita una comida existente. La segunda comprobará que se devuelve una vista "NotFound" cuando se solicita una comida inexistente:

[TestClass]
public class DinnersControllerTest {

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_ExistingDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(1) as ViewResult;

        // Assert
        Assert.IsNotNull(result, "Expected View");
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    } 
}

El código anterior se compila limpio. Sin embargo, cuando se ejecutan las pruebas, ambas producen un error:

Screenshot of the code. Both tests have failed.

Si examinamos los mensajes de error, veremos que el motivo por el que se produjo un error en las pruebas fue porque la clase DinnersRepository no pudo conectarse a una base de datos. Nuestra aplicación NerdDinner usa una cadena de conexión a un archivo SQL Server Express local que reside en el directorio \App_Data del proyecto de la aplicación NerdDinner. Dado que nuestro proyecto NerdDinner.Tests se compila y se ejecuta en un directorio diferente, el proyecto de aplicación, la ubicación de la ruta de acceso relativa de nuestra cadena de conexión es incorrecta.

Podríamos corregirlo copiando el archivo de base de datos de SQL Express en nuestro proyecto de prueba y agregándole una cadena de conexión de prueba adecuada en el archivo app.config de nuestro proyecto de prueba. Esto obtendría las pruebas anteriores desbloqueadas y en ejecución.

Sin embargo, el código de prueba unitaria que usa una base de datos real incluye una serie de desafíos. Específicamente:

  • Ralentiza significativamente el tiempo de ejecución de las pruebas unitarias. Cuanto más tiempo tarde en ejecutar pruebas, menos probable es que las ejecute con frecuencia. Lo ideal es que las pruebas unitarias puedan ejecutarse en segundos y que sea algo que se haga de forma natural, como compilar el proyecto.
  • Complica la configuración y la lógica de limpieza dentro de las pruebas. Queremos que cada prueba unitaria esté aislada y sea independiente de las demás (sin efectos secundarios ni dependencias). Al trabajar con una base de datos real, debe tener en cuenta el estado y restablecerla entre una prueba y otra.

Echemos un vistazo a un patrón de diseño denominado "inserción de dependencias" que puede ayudarnos a solucionar estos problemas y evitar la necesidad de una base de datos real con nuestras pruebas.

Inserción de dependencias

En este momento DinnersController está estrechamente "acoplado" a la clase DinnerRepository. El "acoplamiento" hace referencia a una situación en la que una clase se basa explícitamente en otra clase para que funcione:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/Details/5

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.FindDinner(id);

        if (dinner == null)
            return View("NotFound");

        return View(dinner);
    }

Dado que la clase DinnerRepository requiere acceso a una base de datos, la dependencia estrechamente acoplada que tiene la clase DinnersController en DinnerRepository termina requiriendo una base de datos para que se prueben los métodos de acción DinnersController.

Podemos solucionarlo mediante un patrón de diseño denominado "inserción de dependencias", que es un enfoque en el que las dependencias (como las clases de repositorio que proporcionan acceso a datos) ya no se crean implícitamente dentro de las clases que las usan. En su lugar, las dependencias se pueden pasar explícitamente a la clase que las usa mediante argumentos de constructor. Si las dependencias se definen mediante interfaces, tenemos la flexibilidad de pasar implementaciones de dependencias "falsas" para escenarios de prueba unitaria. Esto nos permite crear implementaciones de dependencia específicas de prueba que realmente no necesitan acceder a una base de datos.

Para ver esto en acción, vamos a implementar la inserción de dependencias con nuestro DinnersController.

Extracción de una interfaz IDinnerRepository

Nuestro primer paso será crear una nueva interfaz IDinnerRepository que encapsule el contrato del repositorio que requieren nuestros controladores para recuperar y actualizar comidas.

Para definir este contrato de interfaz manualmente, haga clic con el botón derecho en la carpeta \Models, elija el comando de menú Agregar->Nuevo elemento y cree una nueva interfaz denominada IDinnerRepository.cs.

Como alternativa, podemos usar las herramientas de refactorización integradas en Visual Studio Professional (y ediciones posteriores) para que extraigan y creen automáticamente una interfaz desde nuestra clase DinnerRepository existente. Para extraer esta interfaz mediante VS, simplemente coloque el cursor en el editor de texto de la clase DinnerRepository, haga clic con el botón derecho y elija el comando de menú Refactorizar->Extraer Interfaz:

Screenshot that shows Extract Interface selected in the Refactor submenu.

Esto iniciará el cuadro de diálogo "Extraer interfaz" y nos pedirá el nombre de la interfaz que se va a crear. El valor predeterminado es IDinnerRepository y seleccionará automáticamente todos los métodos públicos en la clase DinnerRepository existente para agregarlos a la interfaz:

Screenshot of the Test Results window in Visual Studio.

Al hacer clic en el botón "aceptar", Visual Studio agrega una nueva interfaz IDinnerRepository a nuestra aplicación:

public interface IDinnerRepository {

    IQueryable<Dinner> FindAllDinners();
    IQueryable<Dinner> FindByLocation(float latitude, float longitude);
    IQueryable<Dinner> FindUpcomingDinners();
    Dinner             GetDinner(int id);

    void Add(Dinner dinner);
    void Delete(Dinner dinner);
    
    void Save();
}

Y nuestra clase DinnerRepository existente se actualiza para que implemente la interfaz:

public class DinnerRepository : IDinnerRepository {
   ...
}

Actualización de DinnersController para admitir la inserción de constructores

Ahora actualizaremos la clase DinnersController para usar la nueva interfaz.

Actualmente DinnersController está codificado de forma rígida, de modo que su campo "dinnerRepository" siempre es una clase DinnerRepository:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    ...
}

Lo cambiaremos para que el campo "dinnerRepository" sea de tipo IDinnerRepository en lugar de DinnerRepository. A continuación, agregaremos dos constructores de DinnersController públicos. Uno de los constructores permite pasar IDinnerRepository como argumento. El otro es un constructor predeterminado que usa nuestra implementación de DinnerRepository existente:

public class DinnersController : Controller {

    IDinnerRepository dinnerRepository;

    public DinnersController()
        : this(new DinnerRepository()) {
    }

    public DinnersController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
    ...
}

Dado que ASP.NET MVC crea clases de controlador de forma predeterminada con constructores predeterminados, DinnersController seguirá usando la clase DinnerRepository en tiempo de ejecución para realizar el acceso a datos.

Sin embargo, podemos actualizar nuestras pruebas unitarias para pasar una implementación del repositorio de comidas "falsa" mediante el constructor de parámetros. Este repositorio de comidas "falso" no requerirá acceso a una base de datos real y, en su lugar, usará datos de ejemplo en memoria.

Creación de la clase FakeDinnerRepository

Vamos a crear una clase FakeDinnerRepository.

Comenzaremos creando un directorio "Fakes" en nuestro proyecto NerdDinner.Tests y le agregaremos una nueva clase FakeDinnerRepository (haremos clic con el botón derecho en la carpeta y elegiremos Agregar->Nueva clase):

Screenshot of the Add New Class menu item. Add New Item is highlighted.

Actualizaremos el código para que la clase FakeDinnerRepository implemente la interfaz IDinnerRepository. A continuación, podemos hacer clic con el botón derecho en él y elegir el comando de menú contextual "Implementar interfaz IDinnerRepository":

Screenshot of the Implement interface I Dinner Repository context menu command.

Esto hará que Visual Studio agregue automáticamente todos los miembros de la interfaz IDinnerRepository a nuestra clase FakeDinnerRepository con implementaciones predeterminadas de "código auxiliar":

public class FakeDinnerRepository : IDinnerRepository {

    public IQueryable<Dinner> FindAllDinners() {
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float long){
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        throw new NotImplementedException();
    }

    public Dinner GetDinner(int id) {
        throw new NotImplementedException();
    }

    public void Add(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Delete(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Save() {
        throw new NotImplementedException();
    }
}

A continuación, podemos actualizar la implementación de FakeDinnerRepository para que use una colección List<Dinner> en memoria que se le pasa como argumento de constructor:

public class FakeDinnerRepository : IDinnerRepository {

    private List<Dinner> dinnerList;

    public FakeDinnerRepository(List<Dinner> dinners) {
        dinnerList = dinners;
    }

    public IQueryable<Dinner> FindAllDinners() {
        return dinnerList.AsQueryable();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return (from dinner in dinnerList
                where dinner.EventDate > DateTime.Now
                select dinner).AsQueryable();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float lon) {
        return (from dinner in dinnerList
                where dinner.Latitude == lat && dinner.Longitude == lon
                select dinner).AsQueryable();
    }

    public Dinner GetDinner(int id) {
        return dinnerList.SingleOrDefault(d => d.DinnerID == id);
    }

    public void Add(Dinner dinner) {
        dinnerList.Add(dinner);
    }

    public void Delete(Dinner dinner) {
        dinnerList.Remove(dinner);
    }

    public void Save() {
        foreach (Dinner dinner in dinnerList) {
            if (!dinner.IsValid)
                throw new ApplicationException("Rule violations");
        }
    }
}

Ahora tenemos una implementación falsa de IDinnerRepository que no requiere una base de datos y, en su lugar, podemos trabajar con una lista en memoria de objetos Dinner.

Uso de FakeDinnerRepository con pruebas unitarias

Volvamos a las pruebas unitarias de DinnersController que han fallado anteriormente porque la base de datos no estaba disponible. Podemos actualizar los métodos de prueba de que usen un FakeDinnerRepository rellenado con datos de comidas en memoria de ejemplo a DinnersController mediante el código siguiente:

[TestClass]
public class DinnersControllerTest {

    List<Dinner> CreateTestDinners() {

        List<Dinner> dinners = new List<Dinner>();

        for (int i = 0; i < 101; i++) {

            Dinner sampleDinner = new Dinner() {
                DinnerID = i,
                Title = "Sample Dinner",
                HostedBy = "SomeUser",
                Address = "Some Address",
                Country = "USA",
                ContactPhone = "425-555-1212",
                Description = "Some description",
                EventDate = DateTime.Now.AddDays(i),
                Latitude = 99,
                Longitude = -99
            };
            
            dinners.Add(sampleDinner);
        }
        
        return dinners;
    }

    DinnersController CreateDinnersController() {
        var repository = new FakeDinnerRepository(CreateTestDinners());
        return new DinnersController(repository);
    }

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_Dinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(1);

        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    }
}

Y ahora, cuando ejecutamos estas pruebas, ambas se superan:

Screenshot of the unit tests, both tests have passed.

Lo mejor de todo es que solo tardan una fracción de un segundo en ejecutarse y no requieren ninguna lógica complicada de configuración o limpieza. Ahora podemos probar de forma unitaria todo nuestro código del método de acción DinnersController (incluidas las listas, la paginación, los detalles, la creación, la actualización y la eliminación) sin necesidad de conexión a una base de datos real.

Tema secundario: Marcos de inserción de dependencias
Realizar la inserción manual de dependencias (como hacemos arriba) funciona bien, pero es más difícil de mantener a medida que aumenta el número de dependencias y componentes de una aplicación. Existen varios marcos de inserción de dependencias para .NET que pueden ayudar a proporcionar aún más flexibilidad de administración de dependencias. Estos marcos, también denominados contenedores de "inversión de control" (IoC), proporcionan mecanismos que permiten otro nivel de compatibilidad de configuración para especificar y pasar dependencias a objetos en tiempo de ejecución (con mayor frecuencia, mediante la inserción de constructores). Algunos de los marcos de inserción de dependencias o IOC de software de código abierto más populares en .NET incluyen: AutoFac, Ninject, Spring.NET, StructureMap y Windsor. ASP.NET MVC expone las API de extensibilidad que permiten a los desarrolladores participar en la resolución y creación de instancias de los controladores, y que permiten que los marcos de inserción de dependencias o IoC se integren limpiamente en este proceso. El uso de un marco de DI/IOC también nos permitiría quitar el constructor predeterminado de DinnersController, que eliminaría completamente el acoplamiento entre él y DinnerRepository. No usaremos un marco de inserción de dependencias o IOC con nuestra aplicación NerdDinner. Pero es algo que podríamos considerar para el futuro si las funcionalidades y la base de código NerdDinner crecieran.

Creación de pruebas unitarias de acción de edición

Ahora vamos a crear algunas pruebas unitarias que comprueben la funcionalidad de edición de DinnersController. Comenzaremos probando la versión HTTP-GET de la acción de edición:

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    return View(new DinnerFormViewModel(dinner));
}

Crearemos una prueba que compruebe que se representa una vista respaldada por un objeto DinnerFormViewModel cuando se solicite una comida válida:

[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner() {

    // Arrange
    var controller = CreateDinnersController();

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

Sin embargo, cuando se ejecute la prueba, se producirá un error, ya que se produce una excepción de referencia nula cuando el método Edit accede a la propiedad User.Identity.Name para realizar la comprobación Dinner.IsHostedBy().

El objeto User de la clase base Controller encapsula los detalles sobre el usuario que ha iniciado sesión y se rellena mediante ASP.NET MVC cuando crea el controlador en tiempo de ejecución. Dado que estamos probando DinnersController fuera de un entorno de servidor web, el objeto User no se establece (de ahí la excepción de referencia nula).

Simulación de la propiedad User.Identity.Name

Los marcos ficticios facilitan las pruebas al permitirnos crear dinámicamente versiones falsas de objetos dependientes que admiten nuestras pruebas. Por ejemplo, podemos usar un marco ficticio en nuestra prueba de la acción de edición para crear dinámicamente un objeto User que nuestro DinnersController pueda usar para buscar un nombre de usuario simulado. Esto evitará que se produzca una referencia nula al ejecutar la prueba.

Hay muchos marcos de simulación de .NET que se pueden usar con ASP.NET MVC (puede ver una lista de ellos aquí: http://www.mockframeworks.com/).

Una vez descargado, agregaremos una referencia en nuestro proyecto NerdDinner.Tests al ensamblado Moq.dll:

Screenshot of the Nerd Dinner navigation tree. Moq is highlighted.

A continuación, agregaremos un método auxiliar "CreateDinnersControllerAs(username)" a nuestra clase de prueba que toma un nombre de usuario como parámetro y, a continuación, "simula" la propiedad User.Identity.Name en la instancia de DinnersController:

DinnersController CreateDinnersControllerAs(string userName) {

    var mock = new Mock<ControllerContext>();
    mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
    mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);

    var controller = CreateDinnersController();
    controller.ControllerContext = mock.Object;

    return controller;
}

Anteriormente usamos Moq para crear un objeto ficticio que falsifica un objeto ControllerContext (que es lo que ASP.NET MVC pasa a las clases Controller para exponer objetos en tiempo de ejecución como User, Request, Response y Session). Estamos llamando al método "SetupGet" en el objeto ficticio para indicar que la propiedad HttpContext.User.Identity.Name en ControllerContext debe devolver la cadena de nombre de usuario que pasamos al método auxiliar.

Podemos simular cualquier número de propiedades y métodos de ControllerContext. Para ilustrar esto también he agregado una llamada SetupGet() para la propiedad Request.IsAuthenticated (que no es necesaria realmente para las pruebas siguientes, pero que ayuda a ilustrar cómo se pueden simular propiedades request). Cuando hayamos terminado, asignamos una instancia del objeto ficticio ControllerContext al método auxiliar DinnersController.

Ahora podemos escribir pruebas unitarias que usen este método auxiliar para probar escenarios de edición que impliquen a distintos usuarios:

[TestMethod]
public void EditAction_Should_Return_EditView_When_ValidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("NotOwnerUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.AreEqual(result.ViewName, "InvalidOwner");
}

Y ahora cuando ejecutamos las pruebas, stas se superan:

Screenshot of the unit tests that use helper method. The tests have passed.

Prueba de los escenarios de UpdateModel()

Hemos creado pruebas que cubren la versión HTTP-GET de la acción de edición. Ahora vamos a crear algunas pruebas que comprueben la versión HTTP-POST de la acción de edición:

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit (int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    try {
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
        ModelState.AddModelErrors(dinner.GetRuleViolations());
        
        return View(new DinnerFormViewModel(dinner));
    }
}

El interesante escenario de prueba para que admitamos con este método de acción es su uso del método auxiliar UpdateModel() en la clase base Controller. Usamos este método auxiliar para enlazar valores de publicación de formularios a nuestra instancia del objeto Dinner.

A continuación se muestran dos pruebas que muestran cómo podemos proporcionar valores publicados de formulario para que los use el método auxiliar UpdateModel(). Para ello, crearemos y rellenaremos un objeto FormCollection y lo asignaremos a la propiedad "ValueProvider" en el controlador.

La primera prueba comprueba que, al guardar correctamente, el explorador se redirige a la acción de detalles. La segunda prueba comprueba que cuando se publica una entrada no válida, la acción vuelve a reproducir la vista de edición con un mensaje de error.

[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful() {

    // Arrange      
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "Title", "Another value" },
        { "Description", "Another description" }
    };

    controller.ValueProvider = formValues.ToValueProvider();
    
    // Act
    var result = controller.Edit(1, formValues) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual("Details", result.RouteValues["Action"]);
}

[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "EventDate", "Bogus date value!!!"}
    };

    controller.ValueProvider = formValues.ToValueProvider();

    // Act
    var result = controller.Edit(1, formValues) as ViewResult;

    // Assert
    Assert.IsNotNull(result, "Expected redisplay of view");
    Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
}

Encapsulado de pruebas

Hemos tratado los conceptos básicos implicados en las clases de controlador de pruebas unitarias. Podemos usar estas técnicas para crear fácilmente cientos de pruebas sencillas que comprueben el comportamiento de nuestra aplicación.

Dado que nuestras pruebas de controlador y modelo no requieren una base de datos real, son extremadamente rápidas y fáciles de ejecutar. Podremos ejecutar cientos de pruebas automatizadas en segundos y recibir comentarios inmediatamente sobre si un cambio que hicimos rompió algo. Esto nos ayudará con la confianza que necesitamos para mejorar, refactorizar y refinar continuamente nuestra aplicación.

Tratamos las pruebas como el último tema de este capítulo, pero no porque las pruebas sean algo que deba hacer al final de un proceso de desarrollo. Por el contrario, debe escribir pruebas automatizadas lo antes posible en el proceso de desarrollo. Si lo hace, podrá obtener comentarios inmediatos a medida que desarrolla, le ayudará a pensar cuidadosamente en los escenarios de casos de uso de la aplicación y le guiará para diseñar la aplicación con capas limpias y el acoplamiento en mente.

En un capítulo posterior del libro se analizará el desarrollo controlado por pruebas (TDD) y cómo usarlo con ASP.NET MVC. El TDD es una práctica de codificación iterativa en la que se escriben primero las pruebas que cumplirá el código resultante. Con el TDD, cada característica se empieza mediante la creación de una prueba que comprueba la funcionalidad a punto de implementarse. Escribir primero la prueba unitaria ayuda a garantizar que se comprende claramente la característica y cómo se supone que funciona. Solo después de que se escriba la prueba (y de comprobar que se produce un error), se implementa la funcionalidad real que comprueba la prueba. Dado que ya ha dedicado tiempo a pensar en el caso de uso de cómo se supone que la característica funciona, tendrá una mejor comprensión de los requisitos y cómo implementarlos mejor. Cuando haya terminado con la implementación, puede volver a ejecutar la prueba y obtener comentarios inmediatos sobre si la característica funciona correctamente. Trataremos el TDD más en el capítulo 10.

siguiente paso

Algunos finales encapsulan comentarios.