Iteração nº 5 – criar testes de unidade (C#)
pela Microsoft
Na quinta iteração, facilitamos a manutenção e a modificação do aplicativo adicionando testes de unidade. Simulamos nossas classes de modelo de dados e criamos testes de unidade para nossos controladores e lógica de validação.
Criando um aplicativo MVC ASP.NET gerenciamento de contatos (C#)
Nesta série de tutoriais, criamos um aplicativo de Gerenciamento de Contatos inteiro do início ao fim. O aplicativo Gerenciador de Contatos permite que você armazene informações de contato - nomes, números de telefone e endereços de email - para uma lista de pessoas.
Criamos o aplicativo em várias iterações. A cada iteração, aprimoramos gradualmente o aplicativo. O objetivo dessa abordagem de iteração múltipla é permitir que você entenda o motivo de cada alteração.
Iteração nº 1 – Criar o aplicativo. Na primeira iteração, criamos o Gerenciador de Contatos da maneira mais simples possível. Adicionamos suporte para operações básicas de banco de dados: CRUD (Criar, Ler, Atualizar e Excluir).
Iteração nº 2 – Deixe o aplicativo bonito. Nesta iteração, melhoramos a aparência do aplicativo modificando o modo de exibição padrão ASP.NET MVC master página e a folha de estilos em cascata.
Iteração nº 3 – Adicionar validação de formulário. Na terceira iteração, adicionamos validação de formulário básico. Impedimos que as pessoas enviem um formulário sem concluir os campos de formulário necessários. Também validamos endereços de email e números de telefone.
Iteração nº 4 – acoplar o aplicativo livremente. Nesta quarta iteração, aproveitamos vários padrões de design de software para facilitar a manutenção e a modificação do aplicativo Contact Manager. Por exemplo, refatoramos nosso aplicativo para usar o padrão de repositório e o padrão de injeção de dependência.
Iteração nº 5 – Criar testes de unidade. Na quinta iteração, facilitamos a manutenção e a modificação do aplicativo adicionando testes de unidade. Simulamos nossas classes de modelo de dados e criamos testes de unidade para nossos controladores e lógica de validação.
Iteração nº 6 – Usar o desenvolvimento controlado por teste. Nesta sexta iteração, adicionamos uma nova funcionalidade ao nosso aplicativo escrevendo testes de unidade primeiro e escrevendo código nos testes de unidade. Nesta iteração, adicionamos grupos de contatos.
Iteração nº 7 – Adicionar funcionalidade do Ajax. Na sétima iteração, melhoramos a capacidade de resposta e o desempenho do nosso aplicativo adicionando suporte para o Ajax.
Esta Iteração
Na iteração anterior do aplicativo Contact Manager, refatoramos o aplicativo para ser mais flexível. Separamos o aplicativo em camadas distintas de controlador, serviço e repositório. Cada camada interage com a camada abaixo dela por meio de interfaces.
Refatoramos o aplicativo para facilitar a manutenção e a modificação do aplicativo. Por exemplo, se precisarmos usar uma nova tecnologia de acesso a dados, podemos simplesmente alterar a camada do repositório sem tocar no controlador ou na camada de serviço. Ao tornar o Gerenciador de Contatos flexívelmente acoplado, tornamos o aplicativo mais resiliente a alterações.
Mas o que acontece quando precisamos adicionar um novo recurso ao aplicativo Gerenciador de Contatos? Ou o que acontece quando corrigimos um bug? Uma verdade triste, mas bem comprovada, de escrever código é que sempre que você toca no código, cria o risco de introduzir novos bugs.
Por exemplo, um bom dia, seu gerente pode solicitar que você adicione um novo recurso ao Gerenciador de Contatos. Ela quer que você adicione suporte para Grupos de Contatos. Ela quer que você permita que os usuários organizem seus contatos em grupos como Amigos, Empresas e assim por diante.
Para implementar esse novo recurso, você precisará modificar todas as três camadas do aplicativo Contact Manager. Você precisará adicionar novas funcionalidades aos controladores, à camada de serviço e ao repositório. Assim que você começar a modificar o código, corre o risco de interromper a funcionalidade que funcionou antes.
Refatorar nosso aplicativo em camadas separadas, como fizemos na iteração anterior, foi uma coisa boa. Foi uma coisa boa porque nos permite fazer alterações em camadas inteiras sem tocar no restante do aplicativo. No entanto, se você quiser tornar o código dentro de uma camada mais fácil de manter e modificar, será necessário criar testes de unidade para o código.
Você usa um teste de unidade para testar uma unidade de código individual. Essas unidades de código são menores do que as camadas inteiras do aplicativo. Normalmente, você usa um teste de unidade para verificar se um método específico em seu código se comporta da maneira esperada. Por exemplo, você criaria um teste de unidade para o método CreateContact() exposto pela classe ContactManagerService.
Os testes de unidade para um aplicativo funcionam como uma rede de segurança. Sempre que você modificar o código em um aplicativo, poderá executar um conjunto de testes de unidade para marcar se a modificação interrompe a funcionalidade existente. Os testes de unidade tornam seu código seguro para modificar. Os testes de unidade tornam todo o código em seu aplicativo mais resiliente a alterações.
Nesta iteração, adicionamos testes de unidade ao aplicativo Contact Manager. Dessa forma, na próxima iteração, podemos adicionar Grupos de Contatos ao nosso aplicativo sem nos preocuparmos em interromper a funcionalidade existente.
Observação
Há uma variedade de estruturas de teste de unidade, incluindo NUnit, xUnit.net e MbUnit. Neste tutorial, usamos a estrutura de teste de unidade incluída no Visual Studio. No entanto, você poderia usar facilmente uma dessas estruturas alternativas.
O que é testado
No mundo perfeito, todo o seu código seria coberto por testes de unidade. No mundo perfeito, você teria a rede de segurança perfeita. Você poderá modificar qualquer linha de código em seu aplicativo e saber instantaneamente, executando seus testes de unidade, se a alteração interrompeu a funcionalidade existente.
No entanto, não vivemos em um mundo perfeito. Na prática, ao escrever testes de unidade, você se concentra em escrever testes para sua lógica de negócios (por exemplo, lógica de validação). Em particular, você não escreve testes de unidade para sua lógica de acesso a dados ou sua lógica de exibição.
Para ser útil, os testes de unidade devem ser executados muito rapidamente. Você pode facilmente acumular centenas (ou até milhares) de testes de unidade para um aplicativo. Se os testes de unidade levarem muito tempo para serem executados, você evitará executá-los. Em outras palavras, testes de unidade de execução prolongada são inúteis para fins de codificação diária.
Por esse motivo, você normalmente não escreve testes de unidade para código que interage com um banco de dados. Executar centenas de testes de unidade em um banco de dados dinâmico seria muito lento. Em vez disso, você simula seu banco de dados e escreve um código que interage com o banco de dados fictício (discutimos a simulação de um banco de dados abaixo).
Por um motivo semelhante, você normalmente não escreve testes de unidade para exibições. Para testar um modo de exibição, você deve criar um servidor Web. Como a rotação de um servidor Web é um processo relativamente lento, não é recomendável criar testes de unidade para seus modos de exibição.
Se o modo de exibição contiver lógica complicada, considere mover a lógica para os métodos auxiliares. Você pode escrever testes de unidade para métodos auxiliares que são executados sem girar um servidor Web.
Observação
Embora escrever testes para lógica de acesso a dados ou lógica de exibição não seja uma boa ideia ao escrever testes de unidade, esses testes podem ser muito valiosos ao criar testes funcionais ou de integração.
Observação
ASP.NET MVC é o Mecanismo de Exibição Web Forms. Embora o mecanismo de exibição Web Forms dependa de um servidor Web, outros mecanismos de exibição podem não ser.
Usando uma estrutura de objeto fictícia
Ao criar testes de unidade, você quase sempre precisa aproveitar uma estrutura de Objeto Fictício. Uma estrutura de Objeto Fictício permite que você crie simulações e stubs para as classes em seu aplicativo.
Por exemplo, você pode usar uma estrutura de Objeto Fictício para gerar uma versão fictícia da classe do repositório. Dessa forma, você pode usar a classe de repositório simulada em vez da classe real do repositório em seus testes de unidade. O uso do repositório fictício permite evitar a execução do código do banco de dados ao executar um teste de unidade.
O Visual Studio não inclui uma estrutura de Objeto Fictício. No entanto, há várias estruturas comerciais e código aberto Objeto Fictício disponíveis para o .NET Framework:
- Moq – essa estrutura está disponível na licença código aberto BSD. Você pode baixar o Moq de https://code.google.com/p/moq/.
- Rhino Mocks – essa estrutura está disponível sob a licença BSD do código aberto. Você pode baixar o Rhino Mocks de http://ayende.com/projects/rhino-mocks.aspx.
- Isolador typemock – essa é uma estrutura comercial. Você pode baixar uma versão de avaliação do http://www.typemock.com/.
Neste tutorial, decidi usar o Moq. No entanto, você poderia usar facilmente o Rhino Mocks ou o Typemock Isolator para criar os objetos Mock para o aplicativo Contact Manager.
Antes de usar o Moq, você precisa concluir as seguintes etapas:
- .
- Antes de descompactar o download, clique com o botão direito do mouse no arquivo e clique no botão rotulado Desbloquear (consulte a Figura 1).
- Descompacte o download.
- Adicione uma referência ao assembly Moq clicando com o botão direito do mouse na pasta Referências no projeto ContactManager.Tests e selecionando Adicionar Referência. Na guia Procurar, navegue até a pasta em que você descompactou o Moq e selecione o assembly Moq.dll. Clique no botão OK.
- Depois de concluir essas etapas, sua pasta Referências deverá ser semelhante à Figura 2.
Figura 01: Desbloqueio de Moq(Clique para exibir a imagem em tamanho real)
Figura 02: Referências depois de adicionar Moq(Clique para exibir a imagem em tamanho real)
Criando testes de unidade para a camada de serviço
Vamos começar criando um conjunto de testes de unidade para nossa camada de serviço de aplicativo do Contact Manager. Usaremos esses testes para verificar nossa lógica de validação.
Crie uma nova pasta chamada Models no projeto ContactManager.Tests. Em seguida, clique com o botão direito do mouse na pasta Modelos e selecione Adicionar, Novo Teste. A caixa de diálogo Adicionar Novo Teste mostrada na Figura 3 é exibida. Selecione o modelo teste de unidade e nomeie seu novo teste ContactManagerServiceTest.cs. Clique no botão OK para adicionar seu novo teste ao projeto de teste.
Observação
Em geral, você deseja que a estrutura de pastas do projeto de teste corresponda à estrutura de pastas do seu projeto ASP.NET MVC. Por exemplo, você coloca testes de controlador em uma pasta Controladores, testes de modelo em uma pasta Modelos e assim por diante.
Figura 03: Models\ContactManagerServiceTest.cs(Clique para exibir a imagem em tamanho real)
Inicialmente, queremos testar o método CreateContact() exposto pela classe ContactManagerService. Criaremos os cinco testes a seguir:
- CreateContact() – Testes que CreateContact() retorna o valor true quando um Contato válido é passado para o método .
- CreateContactRequiredFirstName() – testa se uma mensagem de erro é adicionada ao estado do modelo quando um Contato com um nome ausente é passado para o método CreateContact().
- CreateContactRequiredLastName() – testa se uma mensagem de erro é adicionada ao estado do modelo quando um Contato com um sobrenome ausente é passado para o método CreateContact().
- CreateContactInvalidPhone() – Testa se uma mensagem de erro é adicionada ao estado do modelo quando um Contato com um número de telefone inválido é passado para o método CreateContact().
- CreateContactInvalidEmail() – testa se uma mensagem de erro é adicionada ao estado do modelo quando um Contato com um endereço de email inválido é passado para o método CreateContact().
O primeiro teste verifica se um Contato válido não gera um erro de validação. Os testes restantes marcar cada uma das regras de validação.
O código para esses testes está contido na Listagem 1.
Listagem 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 a classe Contact na Listagem 1, precisamos adicionar uma referência ao Microsoft Entity Framework ao nosso projeto de teste. Adicione uma referência ao assembly System.Data.Entity.
A listagem 1 contém um método chamado Initialize() decorado com o atributo [TestInitialize]. Esse método é chamado automaticamente antes de cada um dos testes de unidade ser executado (ele é chamado cinco vezes antes de cada um dos testes de unidade). O método Initialize() cria um repositório fictício com a seguinte linha de código:
_mockRepository = new Mock<IContactManagerRepository>();
Essa linha de código usa a estrutura Moq para gerar um repositório fictício da interface IContactManagerRepository. O repositório fictício é usado em vez do EntityContactManagerRepository real para evitar o acesso ao banco de dados quando cada teste de unidade é executado. O repositório fictício implementa os métodos da interface IContactManagerRepository, mas os métodos não fazem nada.
Observação
Ao usar a estrutura Moq, há uma distinção entre _mockRepository e _mockRepository.Object. O primeiro refere-se à classe Mock<IContactManagerRepository> que contém métodos para especificar como o repositório fictício se comportará. Este último refere-se ao repositório fictício real que implementa a interface IContactManagerRepository.
O repositório fictício é usado no método Initialize() ao criar uma instância da classe ContactManagerService. Todos os testes de unidade individuais usam essa instância da classe ContactManagerService.
A listagem 1 contém cinco métodos que correspondem a cada um dos testes de unidade. Cada um desses métodos é decorado com o atributo [TestMethod]. Quando você executa os testes de unidade, qualquer método que tenha esse atributo é chamado. Em outras palavras, qualquer método decorado com o atributo [TestMethod] é um teste de unidade.
O primeiro teste de unidade, chamado CreateContact(), verifica se chamar CreateContact() retorna o valor true quando uma instância válida da classe Contact é passada para o método . O teste cria uma instância da classe Contact, chama o método CreateContact() e verifica se CreateContact() retorna o valor true.
Os testes restantes verificam se quando o método CreateContact() é chamado com um Contato inválido, o método retorna false e a mensagem de erro de validação esperada é adicionada ao estado do modelo. Por exemplo, o teste CreateContactRequiredFirstName() cria uma instância da classe Contact com uma cadeia de caracteres vazia para sua propriedade FirstName. Em seguida, o método CreateContact() é chamado com o Contato inválido. Por fim, o teste verifica se CreateContact() retorna false e que o estado do modelo contém a mensagem de erro de validação esperada "O nome é necessário".
Você pode executar os testes de unidade na Listagem 1 selecionando a opção de menu Testar, Executar, Todos os Testes na Solução (CTRL+R, A). Os resultados dos testes são exibidos na janela Resultados do Teste (consulte a Figura 4).
Figura 04: Resultados do teste (clique para exibir a imagem em tamanho real)
Criando testes de unidade para controladores
ASP. O aplicativo NETMVC controla o fluxo de interação do usuário. Ao testar um controlador, você deseja testar se o controlador retorna o resultado correto da ação e exibir dados. Talvez você também queira testar se um controlador interage com classes de modelo da maneira esperada.
Por exemplo, Listagem 2 contém dois testes de unidade para o método Create() do controlador de contato. O primeiro teste de unidade verifica se quando um Contato válido é passado para o método Create() e, em seguida, o método Create() redireciona para a ação Index. Em outras palavras, quando passado um Contato válido, o método Create() deve retornar um RedirectToRouteResult que representa a ação Index.
Não queremos testar a camada de serviço ContactManager quando estamos testando a camada do controlador. Portanto, simulamos a camada de serviço com o seguinte código no método Initialize:
_service = new Mock();
No teste de unidade CreateValidContact(), simulamos o comportamento de chamar o método CreateContact() da camada de serviço com a seguinte linha de código:
_service.Expect(s => s.CreateContact(contact)).Returns(true);
Essa linha de código faz com que o serviço ContactManager fictício retorne o valor true quando seu método CreateContact() for chamado. Simulando a camada de serviço, podemos testar o comportamento do controlador sem a necessidade de executar nenhum código na camada de serviço.
O segundo teste de unidade verifica se a ação Create() retorna a exibição Criar quando um contato inválido é passado para o método . Fazemos com que o método CreateContact() da camada de serviço retorne o valor false com a seguinte linha de código:
_service.Expect(s => s.CreateContact(contact)).Returns(false);
Se o método Create() se comportar como esperado, ele deverá retornar a exibição Criar quando a camada de serviço retornar o valor false. Dessa forma, o controlador pode exibir as mensagens de erro de validação no modo de exibição Criar e o usuário tem a chance de corrigir essas propriedades de Contato inválidas.
Se você planeja criar testes de unidade para seus controladores, precisará retornar nomes de exibição explícitos de suas ações do controlador. Por exemplo, não retorne uma exibição como esta:
return View();
Em vez disso, retorne a exibição desta forma:
return View("Create");
Se você não for explícito ao retornar uma exibição, a propriedade ViewResult.ViewName retornará uma cadeia de caracteres vazia.
Listagem 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);
}
}
}
Resumo
Nesta iteração, criamos testes de unidade para nosso aplicativo do Contact Manager. Podemos executar esses testes de unidade a qualquer momento para verificar se nosso aplicativo ainda se comporta da maneira esperada. Os testes de unidade atuam como uma rede de segurança para nosso aplicativo, permitindo que modifiquemos nosso aplicativo com segurança no futuro.
Criamos dois conjuntos de testes de unidade. Primeiro, testamos nossa lógica de validação criando testes de unidade para nossa camada de serviço. Em seguida, testamos nossa lógica de controle de fluxo criando testes de unidade para nossa camada de controlador. Ao testar nossa camada de serviço, isolamos nossos testes para nossa camada de serviço de nossa camada de repositório zombando da camada do repositório. Ao testar a camada do controlador, isolamos nossos testes para nossa camada de controlador zombando da camada de serviço.
Na próxima iteração, modificamos o aplicativo Gerenciador de Contatos para que ele dê suporte a Grupos de Contatos. Adicionaremos essa nova funcionalidade ao nosso aplicativo usando um processo de design de software chamado desenvolvimento controlado por teste.