Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Por Steve Smith
Os testes de unidade envolvem o teste de uma parte de um aplicativo isoladamente de sua infraestrutura e dependências. Quando se realiza um teste unitário da lógica do controlador, apenas o conteúdo de uma única ação é testado, não o comportamento das suas dependências ou do próprio framework.
Controladores de teste unitário
Crie testes de unidade para ações do controlador para se concentrar no comportamento do controlador. Um teste de unidade de controlador evita cenários como filtros, roteamento e vinculação de modelo. Os testes que abrangem as interações entre componentes que respondem coletivamente a uma solicitação são manipulados por testes de integração. Para obter mais informações sobre testes de integração, consulte Testes de integração no ASP.NET Core.
Se você estiver escrevendo filtros e rotas personalizados, teste-os isoladamente, não como parte de testes em uma ação específica do controlador.
Para demonstrar os testes de unidade do controlador, revise o controlador a seguir no aplicativo de exemplo.
Visualizar ou descarregar amostra de código (como descarregar)
O Home controlador exibe uma lista de sessões de brainstorming e permite a criação de novas sessões de brainstorming com uma solicitação POST:
public class HomeController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public HomeController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index()
{
var sessionList = await _sessionRepository.ListAsync();
var model = sessionList.Select(session => new StormSessionViewModel()
{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});
return View(model);
}
public class NewSessionModel
{
[Required]
public string SessionName { get; set; }
}
[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}
return RedirectToAction(actionName: nameof(Index));
}
}
O controlador anterior:
- Segue o princípio das dependências explícitas.
- Espera que a injeção de dependência (DI) forneça uma instância de
IBrainstormSessionRepository. - Pode ser testado com um serviço simulado usando uma estrutura de objeto fictício
IBrainstormSessionRepository, como o Moq. Um objeto simulado é um objeto fabricado com um conjunto predeterminado de comportamentos de propriedade e método usados para teste. Para obter mais informações, consulte Introdução aos testes de integração.
O HTTP GET Index método não tem looping ou ramificação e chama apenas um método. O teste de unidade para esta ação:
- Simula o
IBrainstormSessionRepositoryserviço usando oGetTestSessionsmétodo.GetTestSessionsCria duas sessões de geração de ideias simuladas com datas e nomes de sessão. - Executa o
Indexmétodo. - Faz asserções sobre o resultado retornado pelo método:
- A ViewResult é devolvido.
- O ViewDataDictionary.Model é um
StormSessionViewModel. - Há duas sessões de brainstorming armazenadas no
ViewDataDictionary.Model.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
// Act
var result = await controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}
private List<BrainstormSession> GetTestSessions()
{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
O método do Home controlador HTTP POST Index testa e verifica se:
- Quando ModelState.IsValid é
false, o método action retorna um 400 Bad RequestViewResult com os dados apropriados. - Quando
ModelState.IsValidétrue:- O método
Addé chamado no repositório. - A RedirectToActionResult é retornado com os argumentos corretos.
- O método
Um estado de modelo inválido é testado adicionando erros usando AddModelError como mostrado no primeiro teste abaixo:
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();
// Act
var result = await controller.Index(newSession);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel()
{
SessionName = "Test Name"
};
// Act
var result = await controller.Index(newSession);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
Quando ModelState não é válido, o mesmo ViewResult é retornado como para uma solicitação GET. O teste não tenta passar em um modelo inválido. Passar um modelo inválido não é uma abordagem válida, uma vez que a vinculação de modelo não está em execução (embora um teste de integração use a vinculação de modelo). Nesse caso, a vinculação de modelo não é testada. Esses testes de unidade estão testando apenas o código no método de ação.
O segundo teste verifica que, quando o ModelState é válido:
- Um novo
BrainstormSessioné adicionado (através do repositório). - O método retorna a
RedirectToActionResultcom as propriedades esperadas.
As chamadas simuladas que não são chamadas são normalmente ignoradas, mas a chamada Verifiable no final da chamada de configuração permite a validação simulada no teste. Isso é realizado com a chamada para mockRepo.Verify, que falha no teste se o método esperado não foi chamado.
Note
A biblioteca Moq usada neste exemplo torna possível misturar simulações verificáveis, ou "estritas", com simulações não verificáveis (também chamadas de simulações ou esboços "soltos"). Saiba mais sobre como personalizar o comportamento simulado com o Moq.
SessionController no aplicativo de exemplo exibe informações relacionadas a uma sessão de brainstorming específica. O controlador inclui lógica para lidar com valores inválidos id (há dois return cenários no exemplo a seguir para cobrir esses cenários). A instrução final return retorna um novo StormSessionViewModel para a visão (Controllers/SessionController.cs):
public class SessionController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public SessionController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index(int? id)
{
if (!id.HasValue)
{
return RedirectToAction(actionName: nameof(Index),
controllerName: "Home");
}
var session = await _sessionRepository.GetByIdAsync(id.Value);
if (session == null)
{
return Content("Session not found.");
}
var viewModel = new StormSessionViewModel()
{
DateCreated = session.DateCreated,
Name = session.Name,
Id = session.Id
};
return View(viewModel);
}
}
Os testes de unidade incluem um teste para cada return cenário na ação do controlador de Sessão Index:
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);
// Act
var result = await controller.Index(id: null);
// Assert
var redirectToActionResult =
Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Home", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}
[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal("Session not found.", contentResult.Content);
}
[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSessions().FirstOrDefault(
s => s.Id == testSessionId));
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<StormSessionViewModel>(
viewResult.ViewData.Model);
Assert.Equal("Test One", model.Name);
Assert.Equal(2, model.DateCreated.Day);
Assert.Equal(testSessionId, model.Id);
}
Ao passar para o controlador Ideas, o aplicativo expõe funcionalidade como uma API da Web na rota api/ideas.
- Uma lista de ideias (
IdeaDTO) associadas a uma sessão de brainstorming é retornada pelo métodoForSession. - O
Createmétodo adiciona novas ideias a uma sessão.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return Ok(result);
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return Ok(session);
}
Evite retornar entidades de domínio de negócios diretamente por meio de chamadas de API. Entidades de domínio:
- Muitas vezes incluem mais dados do que o cliente exige.
- Associe desnecessariamente o modelo de domínio interno do aplicativo à API exposta publicamente.
O mapeamento entre entidades de domínio e os tipos retornados ao cliente pode ser executado:
- Manualmente usando LINQ
Select, como utiliza o aplicativo de exemplo. Para obter mais informações, consulte LINQ (Language Integrated Query). - Automaticamente com uma biblioteca, como AutoMapper.
Em seguida, o aplicativo de exemplo demonstra testes de unidade para os métodos API Create e ForSession do controlador Ideas.
O aplicativo de exemplo contém dois ForSession testes. O primeiro teste determina se ForSession retorna um NotFoundObjectResult (HTTP não encontrado) para uma sessão inválida:
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}
O segundo ForSession teste determina se ForSession retorna uma lista de ideias de sessão (<List<IdeaDTO>>) para uma sessão válida. As verificações também examinam a primeira ideia para confirmar que sua Name propriedade está correta:
[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
Para testar o Create comportamento do método quando o ModelState é inválido, o aplicativo de exemplo adiciona um erro de modelo ao controlador como parte do teste. Não tente efetuar testes de validação ou associação do modelo em testes de unidade; apenas teste o comportamento do método de ação quando se depara com um ModelState inválido.
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.Create(model: null);
// Assert
Assert.IsType<BadRequestObjectResult>(result);
}
O segundo teste de Create depende do retorno do null repositório, de modo que o repositório simulado está configurado para retornar null. Não há necessidade de criar um banco de dados de teste (na memória ou de outra forma) e construir uma consulta que retorna esse resultado. O teste pode ser realizado em uma única instrução, como o código de exemplo ilustra:
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(new NewIdeaModel());
// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
O terceiro Create teste, Create_ReturnsNewlyCreatedIdeaForSession, verifica se o método do UpdateAsync repositório é chamado. O mock é chamado com Verifiable, e o método do repositório simulado Verify é chamado para confirmar que o método verificável é executado. Não é responsabilidade do teste de unidade garantir que o UpdateAsync método salvou os dados — isso pode ser verificado com um teste de integração.
[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.Create(newIdea);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
Teste ActionResult<T>
ActionResult<T> (ActionResult<TValue>) pode retornar um tipo derivado de ActionResult ou retornar um tipo específico.
O aplicativo de exemplo inclui um método que retorna um List<IdeaDTO> para uma determinada sessão id. Se a sessão id não existir, o controlador retornará NotFound:
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return result;
}
Dois testes do ForSessionActionResult controlador estão incluídos no ApiIdeasControllerTests.
O primeiro teste confirma que o controlador retorna um ActionResult, mas não uma lista inexistente de ideias para uma sessão id inexistente.
- O
ActionResulttipo éActionResult<List<IdeaDTO>>. - O Result é um NotFoundObjectResult.
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var nonExistentSessionId = 999;
// Act
var result = await controller.ForSessionActionResult(nonExistentSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
Para uma sessão idválida, o segundo teste confirma que o método retorna:
- Um
ActionResultdo tipoList<IdeaDTO>. - O ActionResult<T>.Value é do
List<IdeaDTO>tipo. - O primeiro item da lista é uma ideia válida correspondente à ideia armazenada na sessão simulada (obtida por chamada
GetTestSession).
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSessionActionResult(testSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
O aplicativo de exemplo também inclui um método para criar um novo Idea para uma determinada sessão. O controlador retorna:
- BadRequest para um modelo inválido.
- NotFound se a sessão não existir.
- CreatedAtAction quando a sessão é atualizada para incluir a nova ideia.
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);
}
Três testes de CreateActionResult estão incluídos no ApiIdeasControllerTests.
O primeiro texto confirma que um BadRequest é devolvido para um modelo inválido.
[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.CreateActionResult(model: null);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}
O segundo teste verifica se a NotFound se retorna caso a sessão não exista.
[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var nonExistentSessionId = 999;
string testName = "test name";
string testDescription = "test description";
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = nonExistentSessionId
};
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
Para uma sessão idválida, o teste final confirma que:
- O método retorna um
ActionResultcom umBrainstormSessiontipo. - O ActionResult<T>.Result é um CreatedAtActionResult.
CreatedAtActionResulté análogo a uma resposta 201 Created com umLocationcabeçalho. - O ActionResult<T>.Value é do
BrainstormSessiontipo. - A chamada simulada para atualizar a sessão,
UpdateAsync(testSession), foi invocada. A chamada de métodoVerifiableé verificada executandomockRepo.Verify()nas afirmações. - Dois
Ideaobjetos são retornados para a sessão. - O último item (o
Ideaadicionado pela chamada simulada paraUpdateAsync) corresponde aonewIdeaadicionado à sessão no teste.
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
Os controladores desempenham um papel central em qualquer aplicativo MVC ASP.NET Core. Como tal, deve ter confiança de que os controladores se comportam como pretendido. Os testes automatizados podem detetar erros antes que o aplicativo seja implantado em um ambiente de produção.
Visualizar ou descarregar amostra de código (como descarregar)
Testes unitários da lógica do controlador
Os testes de unidade envolvem o teste de uma parte de um aplicativo isoladamente de sua infraestrutura e dependências. Quando se realiza um teste unitário da lógica do controlador, apenas o conteúdo de uma única ação é testado, não o comportamento das suas dependências ou do próprio framework.
Crie testes de unidade para ações do controlador para se concentrar no comportamento do controlador. Um teste de unidade de controlador evita cenários como filtros, roteamento e vinculação de modelo. Os testes que abrangem as interações entre componentes que respondem coletivamente a uma solicitação são manipulados por testes de integração. Para obter mais informações sobre testes de integração, consulte Testes de integração no ASP.NET Core.
Se você estiver escrevendo filtros e rotas personalizados, teste-os isoladamente, não como parte de testes em uma ação específica do controlador.
Para demonstrar os testes de unidade do controlador, revise o controlador a seguir no aplicativo de exemplo. O Home controlador exibe uma lista de sessões de brainstorming e permite a criação de novas sessões de brainstorming com uma solicitação POST:
public class HomeController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public HomeController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index()
{
var sessionList = await _sessionRepository.ListAsync();
var model = sessionList.Select(session => new StormSessionViewModel()
{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});
return View(model);
}
public class NewSessionModel
{
[Required]
public string SessionName { get; set; }
}
[HttpPost]
public async Task<IActionResult> Index(NewSessionModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
else
{
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
}
return RedirectToAction(actionName: nameof(Index));
}
}
O controlador anterior:
- Segue o princípio das dependências explícitas.
- Espera que a injeção de dependência (DI) forneça uma instância de
IBrainstormSessionRepository. - Pode ser testado com um serviço simulado usando uma estrutura de objeto fictício
IBrainstormSessionRepository, como o Moq. Um objeto simulado é um objeto fabricado com um conjunto predeterminado de comportamentos de propriedade e método usados para teste. Para obter mais informações, consulte Introdução aos testes de integração.
O HTTP GET Index método não tem looping ou ramificação e chama apenas um método. O teste de unidade para esta ação:
- Simula o
IBrainstormSessionRepositoryserviço usando oGetTestSessionsmétodo.GetTestSessionsCria duas sessões de geração de ideias simuladas com datas e nomes de sessão. - Executa o
Indexmétodo. - Faz asserções sobre o resultado retornado pelo método:
- A ViewResult é devolvido.
- O ViewDataDictionary.Model é um
StormSessionViewModel. - Há duas sessões de brainstorming armazenadas no
ViewDataDictionary.Model.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
// Act
var result = await controller.Index();
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
viewResult.ViewData.Model);
Assert.Equal(2, model.Count());
}
private List<BrainstormSession> GetTestSessions()
{
var sessions = new List<BrainstormSession>();
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
});
sessions.Add(new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 1),
Id = 2,
Name = "Test Two"
});
return sessions;
}
O método do Home controlador HTTP POST Index testa e verifica se:
- Quando ModelState.IsValid é
false, o método action retorna um 400 Bad RequestViewResult com os dados apropriados. - Quando
ModelState.IsValidétrue:- O método
Addé chamado no repositório. - A RedirectToActionResult é retornado com os argumentos corretos.
- O método
Um estado de modelo inválido é testado adicionando erros usando AddModelError como mostrado no primeiro teste abaixo:
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync())
.ReturnsAsync(GetTestSessions());
var controller = new HomeController(mockRepo.Object);
controller.ModelState.AddModelError("SessionName", "Required");
var newSession = new HomeController.NewSessionModel();
// Act
var result = await controller.Index(newSession);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
Assert.IsType<SerializableError>(badRequestResult.Value);
}
[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
.Returns(Task.CompletedTask)
.Verifiable();
var controller = new HomeController(mockRepo.Object);
var newSession = new HomeController.NewSessionModel()
{
SessionName = "Test Name"
};
// Act
var result = await controller.Index(newSession);
// Assert
var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
Assert.Null(redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
mockRepo.Verify();
}
Quando ModelState não é válido, o mesmo ViewResult é retornado como para uma solicitação GET. O teste não tenta passar em um modelo inválido. Passar um modelo inválido não é uma abordagem válida, uma vez que a vinculação de modelo não está em execução (embora um teste de integração use a vinculação de modelo). Nesse caso, a vinculação de modelo não é testada. Esses testes de unidade estão testando apenas o código no método de ação.
O segundo teste verifica que, quando o ModelState é válido:
- Um novo
BrainstormSessioné adicionado (através do repositório). - O método retorna a
RedirectToActionResultcom as propriedades esperadas.
As chamadas simuladas que não são chamadas são normalmente ignoradas, mas a chamada Verifiable no final da chamada de configuração permite a validação simulada no teste. Isso é realizado com a chamada para mockRepo.Verify, que falha no teste se o método esperado não foi chamado.
Note
A biblioteca Moq usada neste exemplo torna possível misturar simulações verificáveis, ou "estritas", com simulações não verificáveis (também chamadas de simulações ou esboços "soltos"). Saiba mais sobre como personalizar o comportamento simulado com o Moq.
SessionController no aplicativo de exemplo exibe informações relacionadas a uma sessão de brainstorming específica. O controlador inclui lógica para lidar com valores inválidos id (há dois return cenários no exemplo a seguir para cobrir esses cenários). A instrução final return retorna um novo StormSessionViewModel para a visão (Controllers/SessionController.cs):
public class SessionController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public SessionController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index(int? id)
{
if (!id.HasValue)
{
return RedirectToAction(actionName: nameof(Index),
controllerName: "Home");
}
var session = await _sessionRepository.GetByIdAsync(id.Value);
if (session == null)
{
return Content("Session not found.");
}
var viewModel = new StormSessionViewModel()
{
DateCreated = session.DateCreated,
Name = session.Name,
Id = session.Id
};
return View(viewModel);
}
}
Os testes de unidade incluem um teste para cada return cenário na ação do controlador de Sessão Index:
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);
// Act
var result = await controller.Index(id: null);
// Assert
var redirectToActionResult =
Assert.IsType<RedirectToActionResult>(result);
Assert.Equal("Home", redirectToActionResult.ControllerName);
Assert.Equal("Index", redirectToActionResult.ActionName);
}
[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var contentResult = Assert.IsType<ContentResult>(result);
Assert.Equal("Session not found.", contentResult.Content);
}
[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSessions().FirstOrDefault(
s => s.Id == testSessionId));
var controller = new SessionController(mockRepo.Object);
// Act
var result = await controller.Index(testSessionId);
// Assert
var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsType<StormSessionViewModel>(
viewResult.ViewData.Model);
Assert.Equal("Test One", model.Name);
Assert.Equal(2, model.DateCreated.Day);
Assert.Equal(testSessionId, model.Id);
}
Ao passar para o controlador Ideas, o aplicativo expõe funcionalidade como uma API da Web na rota api/ideas.
- Uma lista de ideias (
IdeaDTO) associadas a uma sessão de brainstorming é retornada pelo métodoForSession. - O
Createmétodo adiciona novas ideias a uma sessão.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return Ok(result);
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return Ok(session);
}
Evite retornar entidades de domínio de negócios diretamente por meio de chamadas de API. Entidades de domínio:
- Muitas vezes incluem mais dados do que o cliente exige.
- Associe desnecessariamente o modelo de domínio interno do aplicativo à API exposta publicamente.
O mapeamento entre entidades de domínio e os tipos retornados ao cliente pode ser executado:
- Manualmente usando LINQ
Select, como utiliza o aplicativo de exemplo. Para obter mais informações, consulte LINQ (Language Integrated Query). - Automaticamente com uma biblioteca, como AutoMapper.
Em seguida, o aplicativo de exemplo demonstra testes de unidade para os métodos API Create e ForSession do controlador Ideas.
O aplicativo de exemplo contém dois ForSession testes. O primeiro teste determina se ForSession retorna um NotFoundObjectResult (HTTP não encontrado) para uma sessão inválida:
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}
O segundo ForSession teste determina se ForSession retorna uma lista de ideias de sessão (<List<IdeaDTO>>) para uma sessão válida. As verificações também examinam a primeira ideia para confirmar que sua Name propriedade está correta:
[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
Para testar o Create comportamento do método quando o ModelState é inválido, o aplicativo de exemplo adiciona um erro de modelo ao controlador como parte do teste. Não tente efetuar testes de validação ou associação do modelo em testes de unidade; apenas teste o comportamento do método de ação quando se depara com um ModelState inválido.
[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.Create(model: null);
// Assert
Assert.IsType<BadRequestObjectResult>(result);
}
O segundo teste de Create depende do retorno do null repositório, de modo que o repositório simulado está configurado para retornar null. Não há necessidade de criar um banco de dados de teste (na memória ou de outra forma) e construir uma consulta que retorna esse resultado. O teste pode ser realizado em uma única instrução, como o código de exemplo ilustra:
[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync((BrainstormSession)null);
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.Create(new NewIdeaModel());
// Assert
Assert.IsType<NotFoundObjectResult>(result);
}
O terceiro Create teste, Create_ReturnsNewlyCreatedIdeaForSession, verifica se o método do UpdateAsync repositório é chamado. O mock é chamado com Verifiable, e o método do repositório simulado Verify é chamado para confirmar que o método verificável é executado. Não é responsabilidade do teste de unidade garantir que o UpdateAsync método salvou os dados — isso pode ser verificado com um teste de integração.
[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.Create(newIdea);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
Teste ActionResult<T>
No ASP.NET Core 2.1 ou posterior, ActionResult<T> (ActionResult<TValue>) permite devolver um tipo que deriva de ActionResult ou um tipo específico.
O aplicativo de exemplo inclui um método que retorna um List<IdeaDTO> para uma determinada sessão id. Se a sessão id não existir, o controlador retornará NotFound:
[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return result;
}
Dois testes do ForSessionActionResult controlador estão incluídos no ApiIdeasControllerTests.
O primeiro teste confirma que o controlador retorna um ActionResult, mas não uma lista inexistente de ideias para uma sessão id inexistente.
- O
ActionResulttipo éActionResult<List<IdeaDTO>>. - O Result é um NotFoundObjectResult.
[Fact]
public async Task ForSessionActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var nonExistentSessionId = 999;
// Act
var result = await controller.ForSessionActionResult(nonExistentSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
Para uma sessão idválida, o segundo teste confirma que o método retorna:
- Um
ActionResultdo tipoList<IdeaDTO>. - O ActionResult<T>.Value é do
List<IdeaDTO>tipo. - O primeiro item da lista é uma ideia válida correspondente à ideia armazenada na sessão simulada (obtida por chamada
GetTestSession).
[Fact]
public async Task ForSessionActionResult_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(GetTestSession());
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSessionActionResult(testSessionId);
// Assert
var actionResult = Assert.IsType<ActionResult<List<IdeaDTO>>>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(actionResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
O aplicativo de exemplo também inclui um método para criar um novo Idea para uma determinada sessão. O controlador retorna:
- BadRequest para um modelo inválido.
- NotFound se a sessão não existir.
- CreatedAtAction quando a sessão é atualizada para incluir a nova ideia.
[HttpPost("createactionresult")]
[ProducesResponseType(201)]
[ProducesResponseType(400)]
[ProducesResponseType(404)]
public async Task<ActionResult<BrainstormSession>> CreateActionResult([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return CreatedAtAction(nameof(CreateActionResult), new { id = session.Id }, session);
}
Três testes de CreateActionResult estão incluídos no ApiIdeasControllerTests.
O primeiro texto confirma que um BadRequest é devolvido para um modelo inválido.
[Fact]
public async Task CreateActionResult_ReturnsBadRequest_GivenInvalidModel()
{
// Arrange & Act
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
controller.ModelState.AddModelError("error", "some error");
// Act
var result = await controller.CreateActionResult(model: null);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}
O segundo teste verifica se a NotFound se retorna caso a sessão não exista.
[Fact]
public async Task CreateActionResult_ReturnsNotFoundObjectResultForNonexistentSession()
{
// Arrange
var nonExistentSessionId = 999;
string testName = "test name";
string testDescription = "test description";
var mockRepo = new Mock<IBrainstormSessionRepository>();
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = nonExistentSessionId
};
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
Assert.IsType<NotFoundObjectResult>(actionResult.Result);
}
Para uma sessão idválida, o teste final confirma que:
- O método retorna um
ActionResultcom umBrainstormSessiontipo. - O ActionResult<T>.Result é um CreatedAtActionResult.
CreatedAtActionResulté análogo a uma resposta 201 Created com umLocationcabeçalho. - O ActionResult<T>.Value é do
BrainstormSessiontipo. - A chamada simulada para atualizar a sessão,
UpdateAsync(testSession), foi invocada. A chamada de métodoVerifiableé verificada executandomockRepo.Verify()nas afirmações. - Dois
Ideaobjetos são retornados para a sessão. - O último item (o
Ideaadicionado pela chamada simulada paraUpdateAsync) corresponde aonewIdeaadicionado à sessão no teste.
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
Recursos adicionais
- Testes de integração no ASP.NET Core
- Criar e executar testes de unidade com o Visual Studio
- MyTested.AspNetCore.Mvc - Fluent Testing Library for ASP.NET Core MVC: Biblioteca de testes de unidade fortemente tipada, fornecendo uma interface fluente para testar aplicativos MVC e API da Web. (Não mantido ou suportado pela Microsoft.)
- JustMockLite: Um framework de simulação para desenvolvedores .NET. (Não mantido ou suportado pela Microsoft.)