Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
por Microsoft
En la quinta iteración, hacemos que la aplicación sea más fácil de mantener y modificar agregando pruebas unitarias. Simulamos nuestras clases de modelo de datos y compilamos pruebas unitarias para nuestros controladores y lógica de validación.
Creación de una aplicación de administración de contactos ASP.NET MVC (C#)
En esta serie de tutoriales, creamos una aplicación completa de administración de contactos de principio a fin. La aplicación Contact Manager le permite almacenar información de contacto (nombres, números de teléfono y direcciones de correo electrónico) para obtener una lista de personas.
Compilamos la aplicación en varias iteraciones. Con cada iteración, mejoramos gradualmente la aplicación. El objetivo de este enfoque de iteración múltiple es permitirle comprender el motivo de cada cambio.
Iteración n.º 1: Crear la aplicación. En la primera iteración, creamos el Administrador de contactos de la manera más sencilla posible. Agregamos compatibilidad con operaciones básicas de base de datos: Crear, Leer, Actualizar y Eliminar (CRUD).
Iteración n.º 2: Hacer que la aplicación tenga un buen aspecto. En esta iteración, mejoramos la apariencia de la aplicación modificando la página maestra de vista predeterminada de ASP.NET MVC y la hoja de estilos en cascada.
Iteración n.º 3: Añadir una validación de formulario. En la tercera iteración, añadimos validación básica de formularios. Evitamos que los usuarios envíen un formulario sin completar los campos de formulario obligatorios. También validamos las direcciones de correo electrónico y los números de teléfono.
Iteración n.º 4: Hacer que la aplicación tenga un acoplamiento flexible. En esta cuarta iteración, aprovechamos varios patrones de diseño de software para facilitar el mantenimiento y modificación de la aplicación Contact Manager. Por ejemplo, refactorizamos la aplicación para usar el patrón Repositorio y el patrón de inserción de dependencias.
Iteración n.º 5: Crear pruebas unitarias. En la quinta iteración, hacemos que la aplicación sea más fácil de mantener y modificar agregando pruebas unitarias. Simulamos nuestras clases de modelo de datos y compilamos pruebas unitarias para nuestros controladores y lógica de validación.
Iteración n.º 6: Utilizar el desarrollo mediante pruebas. En esta sexta iteración, añadimos una nueva funcionalidad a nuestra aplicación escribiendo primero pruebas unitarias y escribiendo código en las pruebas unitarias. En esta iteración, agregamos grupos de contactos.
Iteración n.º 7: Agregar funcionalidad de Ajax. En la séptima iteración, mejoramos la capacidad de respuesta y el rendimiento de nuestra aplicación agregando compatibilidad con Ajax.
Esta iteración
En la iteración anterior de la aplicación Contact Manager, refactorizamos la aplicación para que esté acoplada de forma más flexible. Separamos la aplicación en distintas capas de controlador, servicios y repositorios. Cada capa interactúa con la capa inferior a través de interfaces.
Hemos refactorizado la aplicación para que sea más fácil mantenerla y modificarla. Por ejemplo, si necesitamos usar una nueva tecnología de acceso a los datos, podemos simplemente cambiar la capa del repositorio sin tocar la capa de los controlador o de los servicios. Al hacer que el Administrador de contactos tenga un acoplamiento flexible, hemos conseguido que la aplicación sea más resistente a los cambios.
Pero, ¿qué ocurre cuando necesitamos agregar una nueva característica a la aplicación Contact Manager? O, ¿qué ocurre cuando corregimos un error? Una realidad triste, pero bien demostrada, de la escritura de código es que siempre que se toca el código se crea el riesgo de introducir nuevos fallos.
Por ejemplo, un buen día, su administrador puede pedirle que agregue una nueva característica al Administrador de contactos. Quiere que agregue compatibilidad con los grupos de contactos. Quiere que habilite a los usuarios para organizar sus contactos en grupos como Amigos, Negocios, etc.
Para implementar esta nueva característica, tendrá que modificar las tres capas de la aplicación Contact Manager. Tendrá que agregar nuevas funcionalidades a los controladores, a la capa de servicios y al repositorio. En cuanto empiece a modificar el código, corre el riesgo de romper la funcionalidad que funcionaba antes.
Refactorizar nuestra aplicación en capas separadas, como hicimos en la iteración anterior, fue algo positivo. Fue algo positivo porque nos permite realizar cambios en capas enteras sin tocar el resto de la aplicación. Sin embargo, si quiere que el código dentro de una capa sea más fácil de mantener y modificar, necesita crear pruebas unitarias para el código.
Se usa una prueba unitaria para probar una unidad de código individual. Estas unidades de código son más pequeñas que capas enteras de aplicación. Normalmente, se usa una prueba unitaria para comprobar si un método determinado en el código se comporta de la manera esperada. Por ejemplo, crearía una prueba unitaria para el método CreateContact() expuesto por la clase ContactManagerService.
Las pruebas unitarias de una aplicación funcionan solo como una red de seguridad. Siempre que modifique el código de una aplicación, puede ejecutar un conjunto de pruebas unitarias para comprobar si la modificación interrumpe la funcionalidad existente. Las pruebas unitarias hacen que sea seguro modificar su código. Las pruebas unitarias hacen que todo el código de su aplicación sea más resistente a los cambios.
En esta iteración, agregamos pruebas unitarias a nuestra aplicación Contact Manager. De este modo, en la próxima iteración, podremos agregar grupos de contactos a nuestra aplicación sin preocuparnos de romper la funcionalidad existente.
Nota:
Existe una gran variedad de marcos de pruebas unitarias, como NUnit, xUnit.net y MbUnit. En este tutorial, usamos el marco de pruebas unitarias incluido con Visual Studio. Sin embargo, podría usar con la misma facilidad uno de estos marcos alternativos.
Qué se prueba
En un mundo perfecto, todo su código estaría cubierto por pruebas unitarias. En un mundo perfecto, tendría la red de seguridad perfecta. Podría modificar cualquier línea de código de su aplicación y saber al instante, mediante la ejecución de sus pruebas unitarias, si el cambio ha roto la funcionalidad existente.
Sin embargo, no vivimos en un mundo perfecto. En la práctica, cuando escriba pruebas unitarias, concéntrese en escribir pruebas para su lógica de negocios (por ejemplo, la lógica de validación). En concreto, no escriba pruebas unitarias para su lógica de acceso a datos o su lógica de vistas.
Para ser útiles, las pruebas unitarias deben ejecutarse muy rápidamente. Puede acumular fácilmente cientos (o incluso miles) de pruebas unitarias para una aplicación. Si las pruebas unitarias tardan mucho en ejecutarse, evitará ejecutarlas. En otras palabras, las pruebas unitarias de larga duración son inútiles para la codificación cotidiana.
Por esta razón, normalmente no se escriben pruebas unitarias para el código que interactúa con una base de datos. La ejecución de cientos de pruebas unitarias en una base de datos activa sería demasiado lenta. En su lugar, simule su base de datos y escriba código que interactúe con la base de datos simulada (más adelante hablaremos de la simulación de una base de datos).
Por una razón similar, normalmente no se escriben pruebas unitarias para las vistas. Para probar una vista, debe poner en marcha un servidor web. Dado que poner en marcha un servidor web es un proceso relativamente lento, no se recomienda crear pruebas unitarias para sus vistas.
Si su vista contiene lógica complicada, debería considerar mover la lógica a métodos asistentes. Puede escribir pruebas unitarias para los métodos asistentes que se ejecutan sin necesidad de poner en marcha un servidor web.
Nota:
Aunque escribir pruebas para la lógica de acceso a datos o la lógica de las vistas no es una buena idea cuando se escriben pruebas unitarias, estas pruebas pueden ser muy valiosas cuando se desarrollan pruebas funcionales o de integración.
Nota:
ASP.NET MVC es el motor de visualización de Web Forms. Mientras que el motor de visualización de Web Forms depende de un servidor web, otros motores de visualización pueden no hacerlo.
Uso de un marco de objetos ficticios
Cuando desarrolle pruebas unitarias, casi siempre necesitará aprovechar un marco de objetos ficticios. Un marco de objetos ficticios permite crear objetos ficticios y códigos auxiliares para las clases de la aplicación.
Por ejemplo, puede usar un marco de objetos ficticios para generar una versión simulada de su clase de repositorio. De esta forma, puede usar la clase de repositorio ficticia en lugar de la clase de repositorio real en sus pruebas unitarias. El uso del repositorio ficticio le permite evitar ejecutar código de base de datos al ejecutar una prueba unitaria.
Visual Studio no incluye un marco de objetos ficticios. Sin embargo, existen varios marcos de objetos ficticios comerciales y de código abierto disponibles para el marco .NET:
- Moq: este marco está disponible en la licencia BSD de código abierto. Puede descargar Moq desde https://code.google.com/p/moq/.
- Rhino Mocks: este marco está disponible bajo la licencia BSD de código abierto. Puede descargar Rhino Mocks desde http://ayende.com/projects/rhino-mocks.aspx.
- Typemock Isolator: se trata de un marco comercial. Puede descargar una versión de prueba desde http://www.typemock.com/.
En este tutorial, he decidido usar Moq. Sin embargo, podría usar con la misma facilidad Rhino Mocks o Typemock Isolator para crear los objetos ficticios de la aplicación Contact Manager.
Antes de usar Moq, deberá finalizar los siguientes pasos:
- .
- Antes de descomprimir la descarga, asegúrese de hacer clic con el botón derecho del ratón en el archivo y pulse el botón etiquetado como Desbloquear (vea la figura 1).
- Descomprima la descarga.
- Agregue una referencia al ensamblado de Moq haciendo clic con el botón derecho del ratón en la carpeta References del proyecto ContactManager.Tests y seleccionando Agregar referencia. En la pestaña Examinar, vaya a la carpeta donde descomprimió Moq y seleccione el ensamblado Moq.dll. Haga clic en el botón Aceptar .
- Una vez finalizados estos pasos, su carpeta References debería tener el aspecto de la figura 2.
Figura 01: Desbloqueo de Moq (haga clic para ver la imagen a tamaño completo)
Figura 02: Referencias después de añadir Moq (haga clic para ver la imagen a tamaño completo)
Creación de pruebas unitarias para la capa de servicio
Empecemos por crear un conjunto de pruebas unitarias para la capa de servicios de nuestra aplicación Contact Manager. Usaremos estas pruebas para verificar nuestra lógica de validación.
Cree una nueva carpeta llamada Models en el proyecto ContactManager.Tests. A continuación, haga clic con el botón derecho del ratón en la carpeta Models y seleccione Agregar, Nueva prueba. Aparecerá el cuadro de diálogo Agregar nueva prueba que se muestra en la figura 3. Seleccione la plantilla Prueba unitaria y nombre su nueva prueba ContactManagerServiceTest.cs. Haga clic en el botón Aceptar para agregar su nueva prueba a su proyecto de prueba.
Nota:
En general, quiere que la estructura de carpetas de su proyecto de prueba coincida con la estructura de carpetas de su proyecto de ASP.NET MVC. Por ejemplo, las pruebas de controlador se colocan en una carpeta Controllers, las pruebas de modelos en una carpeta Models, etc.
Figura 03: Models\ContactManagerServiceTest.cs (haga clic para ver la imagen a tamaño completo)
Inicialmente, queremos probar el método CreateContact() expuesto por la clase ContactManagerService. Crearemos las cinco pruebas siguientes:
- CreateContact(): comprueba que CreateContact() devuelve el valor verdadero cuando se pasa un Contacto válido al método.
- CreateContactRequiredFirstName(): comprueba que se agrega un mensaje de error al estado del modelo cuando se pasa al método CreateContact() un contacto al que le falta el nombre.
- CreateContactRequiredLastName(): comprueba que se agrega un mensaje de error al estado del modelo cuando se pasa al método CreateContact() un contacto al que le falta el apellido.
- CreateContactInvalidPhone(): comprueba que se agrega un mensaje de error al estado del modelo cuando se pasa un contacto con un número de teléfono no válido al método CreateContact().
- CreateContactInvalidEmail(): comprueba que se agrega un mensaje de error al estado del modelo cuando se pasa un contacto con una dirección de correo electrónico no válida al método CreateContact().
La primera prueba verifica que un Contacto válido no genera un error de validación. Las pruebas restantes comprueban cada una de las reglas de validación.
El código de estas pruebas figura en la lista 1.
Lista 1: Models\ContactManagerServiceTest.cs
using System.Web.Mvc;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace ContactManager.Tests.Models
{
[TestClass]
public class ContactManagerServiceTest
{
private Mock<IContactManagerRepository> _mockRepository;
private ModelStateDictionary _modelState;
private IContactManagerService _service;
[TestInitialize]
public void Initialize()
{
_mockRepository = new Mock<IContactManagerRepository>();
_modelState = new ModelStateDictionary();
_service = new ContactManagerService(new ModelStateWrapper(_modelState), _mockRepository.Object);
}
[TestMethod]
public void CreateContact()
{
// Arrange
var contact = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "steve@somewhere.com");
// Act
var result = _service.CreateContact(contact);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
public void CreateContactRequiredFirstName()
{
// Arrange
var contact = Contact.CreateContact(-1, string.Empty, "Walther", "555-5555", "steve@somewhere.com");
// Act
var result = _service.CreateContact(contact);
// Assert
Assert.IsFalse(result);
var error = _modelState["FirstName"].Errors[0];
Assert.AreEqual("First name is required.", error.ErrorMessage);
}
[TestMethod]
public void CreateContactRequiredLastName()
{
// Arrange
var contact = Contact.CreateContact(-1, "Stephen", string.Empty, "555-5555", "steve@somewhere.com");
// Act
var result = _service.CreateContact(contact);
// Assert
Assert.IsFalse(result);
var error = _modelState["LastName"].Errors[0];
Assert.AreEqual("Last name is required.", error.ErrorMessage);
}
[TestMethod]
public void CreateContactInvalidPhone()
{
// Arrange
var contact = Contact.CreateContact(-1, "Stephen", "Walther", "apple", "steve@somewhere.com");
// Act
var result = _service.CreateContact(contact);
// Assert
Assert.IsFalse(result);
var error = _modelState["Phone"].Errors[0];
Assert.AreEqual("Invalid phone number.", error.ErrorMessage);
}
[TestMethod]
public void CreateContactInvalidEmail()
{
// Arrange
var contact = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "apple");
// Act
var result = _service.CreateContact(contact);
// Assert
Assert.IsFalse(result);
var error = _modelState["Email"].Errors[0];
Assert.AreEqual("Invalid email address.", error.ErrorMessage);
}
}
}
Como usamos la clase Contact en la lista 1, tenemos que agregar una referencia a Microsoft Entity Framework a nuestro proyecto de prueba. Añada una referencia al ensamblado System.Data.Entity.
La lista 1 contiene un método denominado Initialize() que está decorado con el atributo [TestInitialize]. Este método se llama automáticamente antes de ejecutar cada una de las pruebas unitarias (se llama 5 veces justo antes de cada una de las pruebas unitarias). El método Initialize() crea un repositorio ficticio con la siguiente línea de código:
_mockRepository = new Mock<IContactManagerRepository>();
Esta línea de código usa el marco Moq para generar un repositorio ficticio a partir de la interfaz IContactManagerRepository. El repositorio ficticio se usa en lugar del EntityContactManagerRepository real para evitar acceder a la base de datos cuando se ejecuta cada prueba unitaria. El repositorio ficticio implementa los métodos de la interfaz IContactManagerRepository, pero los métodos en realidad no hacen nada.
Nota:
Al usar el marco Moq, existe una distinción entre _mockRepository y _mockRepository.Object. El primero hace referencia a la clase Mock<IContactManagerRepository> que contiene métodos para especificar cómo se comportará el repositorio ficticio. El último hace referencia al repositorio ficticio real que implementa la interfaz IContactManagerRepository.
El repositorio ficticio se usa en el método Initialize() cuando se crea una instancia de la clase ContactManagerService. Todas las pruebas unitarias individuales usan esta instancia de la clase ContactManagerService.
La lista 1 contiene cinco métodos que corresponden a cada una de las pruebas unitarias. Cada uno de estos métodos está decorado con el atributo [TestMethod]. Cuando ejecute las pruebas unitarias, se llamará a cualquier método que tenga este atributo. En otras palabras, cualquier método que esté decorado con el atributo [TestMethod] es una prueba unitaria.
La primera prueba unitaria, denominada CreateContact(), verifica que la llamada a CreateContact() devuelve el valor verdadero cuando se pasa al método una instancia válida de la clase Contact. La prueba crea una instancia de la clase Contact, llama al método CreateContact() y verifica que CreateContact() devuelve el valor verdadero.
Las pruebas restantes verifican que cuando se llama al método CreateContact() con un contacto no válido, el método devuelve false y se agrega al estado del modelo el mensaje de error de validación esperado. Por ejemplo, la prueba CreateContactRequiredFirstName() crea una instancia de la clase Contact con una cadena vacía para su propiedad FirstName. A continuación, se llama al método CreateContact() con el Contacto no válido. Por último, la prueba verifica que CreateContact() devuelve false y que el estado del modelo contiene el mensaje de error de validación esperado "El nombre es obligatorio."
Puede ejecutar las pruebas unitarias en la lista 1 seleccionando la opción de menú Prueba, Ejecutar, Todas las pruebas en la solución (CTRL+R, A). Los resultados de las pruebas se verán en la ventana de resultados de las pruebas (consulte la figura 4).
Figura 04: Resultados de pruebas (haga clic para ver la imagen a tamaño completo)
Creación de pruebas unitarias para controladores
La aplicación ASP.NET MVC controla el flujo de interacción con el usuario. Al probar un controlador, quiere comprobar si el controlador devuelve el resultado de la acción y los datos de la vista correctos. También es posible que quiera comprobar si un controlador interactúa con las clases de modelo de la forma esperada.
Por ejemplo, la lista 2 contiene dos pruebas unitarias para el método Create() del controlador Contact. La primera prueba unitaria verifica que, cuando se pasa un Contacto válido al método Create(), este redirige a la acción Index. En otras palabras, cuando se pasa un Contacto válido, el método Create() debería devolver un RedirectToRouteResult que represente la acción Index.
No queremos probar la capa de servicio ContactManager cuando estamos probando la capa de controlador. Por lo tanto, creamos una versión ficticia de la capa de servicio con el siguiente código en el método Initialize:
_service = new Mock();
En la prueba unitaria CreateValidContact(), simulamos el comportamiento de la llamada al método CreateContact() de la capa de servicio con la siguiente línea de código:
_service.Expect(s => s.CreateContact(contact)).Returns(true);
Esta línea de código hace que el servicio ContactManager ficticio devuelva el valor true cuando se llama a su método CreateContact(). Al imitar la capa de servicio, podemos probar el comportamiento de nuestro controlador sin necesidad de ejecutar ningún código en la capa de servicio.
La segunda prueba unitaria verifica que la acción Create() devuelve la vista Create cuando se pasa un contacto no válido al método. Hacemos que el método CreateContact() de la capa de servicio devuelva el valor false con la siguiente línea de código:
_service.Expect(s => s.CreateContact(contact)).Returns(false);
Si el método Create() se comporta como esperamos, debería devolver la vista Create cuando la capa de servicio devuelva el valor false. De ese modo, el controlador puede mostrar los mensajes de error de validación en la vista Create y el usuario tiene la oportunidad de corregir esas propiedades de Contacto no válidas.
Si planea desarrollar pruebas unitarias para sus controlador, tendrá que devolver nombres de vistas explícitos de las acciones de sus controlador. Por ejemplo, no devuelva una vista como esta:
return View();
En su lugar, devuelva la vista así:
return View("Create");
Si no es explícito al devolver una vista, la propiedad ViewResult.ViewName devuelve una cadena vacía.
Lista 2: Controllers\ContactControllerTest.cs
using System.Web.Mvc;
using ContactManager.Controllers;
using ContactManager.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace ContactManager.Tests.Controllers
{
[TestClass]
public class ContactControllerTest
{
private Mock<IContactManagerService> _service;
[TestInitialize]
public void Initialize()
{
_service = new Mock<IContactManagerService>();
}
[TestMethod]
public void CreateValidContact()
{
// Arrange
var contact = new Contact();
_service.Expect(s => s.CreateContact(contact)).Returns(true);
var controller = new ContactController(_service.Object);
// Act
var result = (RedirectToRouteResult)controller.Create(contact);
// Assert
Assert.AreEqual("Index", result.RouteValues["action"]);
}
[TestMethod]
public void CreateInvalidContact()
{
// Arrange
var contact = new Contact();
_service.Expect(s => s.CreateContact(contact)).Returns(false);
var controller = new ContactController(_service.Object);
// Act
var result = (ViewResult)controller.Create(contact);
// Assert
Assert.AreEqual("Create", result.ViewName);
}
}
}
Resumen
En esta iteración, creamos pruebas unitarias para nuestra aplicación Contact Manager. Podemos ejecutar estas pruebas unitarias en cualquier momento para verificar que nuestra aplicación sigue comportándose de la manera que esperamos. Las pruebas unitarias actúan como una red de seguridad para nuestra aplicación, lo que nos permite modificar de forma segura nuestra aplicación en el futuro.
Hemos establecido dos conjuntos de pruebas unitarias. En primer lugar, probamos nuestra lógica de validación creando pruebas unitarias para nuestra capa de servicio. A continuación, probamos nuestra lógica de control de flujo creando pruebas unitarias para nuestra capa de controlador. Cuando probamos nuestra capa de servicio, aislamos nuestras pruebas para nuestra capa de servicio de nuestra capa de repositorio mediante una simulación de nuestra capa de repositorio. Al probar la capa de controlador, aislamos nuestras pruebas para nuestra capa de controlador mediante una simulación de la capa de servicios.
En la siguiente iteración, modificamos la aplicación Contact Manager para que sea compatible con los grupos de contactos. Agregaremos esta nueva funcionalidad a nuestra aplicación usando un proceso de diseño de software llamado desarrollo controlado por pruebas.