Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Par Steve Smith
Les tests unitaires impliquent le test d’une partie d’une application de façon isolée par rapport à son infrastructure et à ses dépendances. Lors du test de la logique d’un contrôleur, seuls les contenus d’une action sont testés, et non pas le comportement de ses dépendances ou du framework lui-même.
Tests unitaires des contrôleurs
Configurez des tests unitaires d’actions de contrôleur pour qu’ils s’attachent uniquement au comportement du contrôleur. Un test unitaire d’un contrôleur ne couvre pas des scénarios tels que les filtres, le routage et la liaison de modèles. Les tests qui couvrent les interactions entre les composants qui répondent collectivement à une requête relèvent des tests d’intégration. Pour plus d’informations sur les tests d’intégration, consultez Tests d’intégration dans ASP.NET Core.
Si vous écrivez des routes et des filtres personnalisés, testez-les de manière isolée avec des tests unitaires plutôt que dans le cadre de tests exécutés sur une action de contrôleur spécifique.
Pour illustrer les tests unitaires de contrôleur, examinez de plus près le contrôleur suivant dans l’exemple d’application.
Afficher ou télécharger le code d’exemple (comment télécharger)
Le contrôleur Home affiche une liste de sessions de brainstorming et permet la création de nouvelles sessions avec une requête 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));
}
}
Le contrôleur précédent :
- Suit le Principe des dépendances explicites.
- Attend que l’injection de dépendance fournisse une instance de
IBrainstormSessionRepository. - Peut être testé avec un service
IBrainstormSessionRepositoryfictif au moyen d’un framework d’objets fictifs, tel que Moq. Un objet simulé est un objet factice doté d’un ensemble prédéfini de comportements pour ses propriétés et ses méthodes, utilisé à des fins de test. Pour plus d’informations, consultez Introduction aux tests d’intégration.
La méthode HTTP GET Index n’a pas de boucle ni de branchement, et elle appelle seulement une méthode. Le test unitaire pour cette action :
- Simule le service
IBrainstormSessionRepositoryà l’aide de la méthodeGetTestSessions.GetTestSessionscrée deux sessions de brainstorming fictives avec des dates et des noms de session. - Exécute la méthode nommée
Index. - Fait des assertions sur le résultat retourné par la méthode :
- Un ViewResult est renvoyé.
- Le ViewDataDictionary.Model est un
StormSessionViewModel. - Deux sessions de brainstorming sont stockées dans le
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;
}
Le test de la méthode HomeHTTP POST Index du contrôleur Home vérifie que :
- Lorsque ModelState.IsValid est
false, la méthode d’action retourne Requête incorrecte 400ViewResult avec les données appropriées. - Lorsque
ModelState.IsValidesttrue:- La méthode
Addsur le dépôt est appelée. - Un RedirectToActionResult est retourné avec les arguments corrects.
- La méthode
Un état de modèle non valide est testé en ajoutant des erreurs avec AddModelError, comme le montre le premier test ci-dessous :
[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();
}
Lorsque ModelState n’est pas valide, le même ViewResult est retourné, comme pour une requête GET. Le test ne tente pas de passer un modèle non valide. Le passage d’un modèle non valide n’est pas une approche valable, car la liaison de modèle n’est pas en cours d’exécution (même si un test d’intégration utilise bien la liaison de modèle). Dans ce cas, la liaison de modèle n’est pas testée. Ces tests unitaires ne testent que le code de la méthode d’action.
Le second test vérifie cela quand le ModelState est valide :
- Un nouveau
BrainstormSessionest ajouté (par le biais du référentiel). - La méthode renvoie un
RedirectToActionResultavec les propriétés attendues.
Les appels simulés qui ne sont pas appelés sont normalement ignorés, mais l’appel de Verifiable à la fin de l’appel de configuration autorise la validation des mocks dans le test. Ceci est effectué avec l’appel à mockRepo.Verify, qui fait échouer le test si la méthode attendue n’a pas été appelée.
Note
La bibliothèque Moq utilisée dans cet exemple permet la combinaison d’éléments fictifs vérifiables (ou « stricts ») avec des éléments fictifs non vérifiables (également appelés stubs ou éléments « lâches »). Découvrez plus d’informations sur la personnalisation du comportement des mocks avec Moq.
Dans l’exemple d’application, SessionController affiche des informations relatives à une session de brainstorming. Le contrôleur inclut la logique pour traiter des valeurs id non valides (il existe deux scénarios return dans l’exemple suivant pour couvrir ces scénarios). La dernière instruction return retourne un nouveau StormSessionViewModel dans la vue (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);
}
}
Les tests unitaires comportent un test pour chaque scénario return dans l’action Index du contrôleur Session :
[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);
}
En passant au contrôleur Ideas, l’application expose des fonctionnalités, comme une API web sur la route api/ideas :
- Une liste d’idées (
IdeaDTO) associées à une session de brainstorming est retournée par la méthodeForSession. - La méthode
Createajoute de nouvelles idées à une session.
[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);
}
Évitez de retourner des entités de domaine métier directement via des appels d’API. Les entités de domaine :
- Incluent souvent plus de données que ce dont a besoin le client.
- Couplent inutilement le modèle de domaine interne de l’application à l’API exposée publiquement.
Le mappage entre des entités de domaine et les types retournés au client peut être effectué :
- Manuellement avec un
SelectLINQ, comme l’utilise l’exemple d’application. Pour plus d’informations, consultez LINQ (Language-Integrated Query). - Automatiquement avec une bibliothèque, telle que AutoMapper.
Ensuite, l’exemple d’application décrit des tests unitaires pour les méthodes d’API Create et ForSession du contrôleur Ideas.
L’exemple d’application contient deux tests ForSession. Le premier test détermine si ForSession retourne un NotFoundObjectResult (HTTP Non trouvé) pour une session non valide :
[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);
}
La deuxième test ForSession détermine si ForSession retourne une liste d’idées de session (<List<IdeaDTO>>) pour une session valide. Les vérifications contrôlent également la première idée pour confirmer que sa propriété Name est correcte :
[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);
}
Pour tester le comportement de la méthode Create quand le ModelState n’est pas valide, l’exemple d’application ajoute une erreur de modèle au contrôleur dans le cadre de ce test. N’essayez pas de tester la validation de modèle ou la liaison de modèle dans des tests unitaires. Testez simplement le comportement de votre méthode d’action quand elle est confrontée à un ModelState non valide :
[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);
}
Le deuxième test de Create dépend du retour de null par le dépôt, le dépôt fictif est donc configuré pour retourner null. Il est inutile de créer une base de données de test (dans la mémoire ou ailleurs) et de construire une requête qui retourne ce résultat. Le test peut être effectué en une seule instruction, comme le montre l’exemple de code :
[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);
}
Le troisième test Create, Create_ReturnsNewlyCreatedIdeaForSession, vérifie que la méthode UpdateAsync du dépôt est appelée. L’élément fictif est appelé avec Verifiable, puis la méthode Verify du dépôt fictif est appelée pour confirmer que la méthode vérifiable est exécutée. La garantie que la méthode UpdateAsync a enregistré les données ne relève pas de la responsabilité du test unitaire. Cette vérification peut être effectuée au moyen d’un test d’intégration.
[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);
}
Tester ActionResult<T>
ActionResult<T> (ActionResult<TValue>) peut retourner un type dérivant de ActionResult ou retourner un type spécifique.
L’exemple d’application comprend une méthode qui retourne un List<IdeaDTO> pour un id de session donné. Si la session id n’existe pas, le contrôleur retourne 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;
}
Deux tests du contrôleur ForSessionActionResult sont inclus dans ApiIdeasControllerTests.
Le premier test confirme que le contrôleur retourne un ActionResult, et non une liste inexistante d’idées pour une session inexistante id :
- Le type
ActionResultestActionResult<List<IdeaDTO>>. - Le Result est un 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);
}
Pour un id de session valide, le deuxième test confirme que la méthode retourne :
- Un
ActionResultavec un typeList<IdeaDTO>. -
ActionResult<T>.Value est un type
List<IdeaDTO>. - Le premier élément de la liste est une idée valide qui correspond à l’idée stockée dans la session simulée (obtenue en appelant
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);
}
L’exemple d’application comporte également une méthode pour créer un nouveau Idea pour une session donnée. Le contrôleur retourne :
- BadRequest pour un modèle non valide.
- NotFound si la session n’existe pas.
- CreatedAtAction lorsque la session est mise à jour avec la nouvelle idée.
[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);
}
Trois tests de CreateActionResult sont inclus dans ApiIdeasControllerTests.
Le premier texte confirme qu’un BadRequest est retourné pour un modèle non valide.
[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);
}
Le deuxième test vérifie qu’un NotFound est renvoyé si la session n’existe pas.
[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);
}
Pour une session valide id, le test final confirme que :
- La méthode retourne un
ActionResultavec un typeBrainstormSession. -
ActionResult<T>.Result est un CreatedAtActionResult.
CreatedAtActionResults’apparente à une réponse 201 Created avec un en-têteLocation. -
ActionResult<T>.Value est de type
BrainstormSession. - L’appel fictif pour mettre à jour la session,
UpdateAsync(testSession), a été invoqué. L’appel de la méthodeVerifiableest contrôlé en exécutantmockRepo.Verify()dans les assertions. - Deux objets
Ideasont retournés pour la session. - Le dernier élément (
Ideaajouté par l’appel fictif àUpdateAsync) correspond ànewIdeaajouté à la session dans le test.
[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);
}
Les contrôleurs jouent un rôle essentiel dans une application ASP.NET Core MVC. En tant que tel, vous devez être sûr qu’ils se comportent comme prévu. Les tests automatisés peuvent détecter des erreurs avant le déploiement de l’application dans un environnement de production.
Voir ou télécharger exemple de code (comment télécharger)
Tests unitaires de la logique de contrôleur
Les tests unitaires impliquent le test d’une partie d’une application de façon isolée par rapport à son infrastructure et à ses dépendances. Lors du test de la logique d’un contrôleur, seuls les contenus d’une action sont testés, et non pas le comportement de ses dépendances ou du framework lui-même.
Configurez des tests unitaires d’actions de contrôleur pour qu’ils s’attachent uniquement au comportement du contrôleur. Un test unitaire d’un contrôleur ne couvre pas des scénarios tels que les filtres, le routage et la liaison de modèles. Les tests qui couvrent les interactions entre les composants qui répondent collectivement à une requête relèvent des tests d’intégration. Pour plus d’informations sur les tests d’intégration, consultez Tests d’intégration dans ASP.NET Core.
Si vous écrivez des routes et des filtres personnalisés, testez-les de manière isolée avec des tests unitaires plutôt que dans le cadre de tests exécutés sur une action de contrôleur spécifique.
Pour illustrer les tests unitaires de contrôleur, examinez de plus près le contrôleur suivant dans l’exemple d’application. Le contrôleur Home affiche une liste de sessions de brainstorming et permet la création de nouvelles sessions avec une requête 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));
}
}
Le contrôleur précédent :
- Suit le Principe des dépendances explicites.
- Attend que l’injection de dépendance fournisse une instance de
IBrainstormSessionRepository. - Peut être testé avec un service
IBrainstormSessionRepositoryfictif au moyen d’un framework d’objets fictifs, tel que Moq. Un objet fictif est un objet fabriqué avec un ensemble prédéterminé de comportements de propriété et de méthode utilisés pour les tests. Pour plus d’informations, consultez Introduction aux tests d’intégration.
La méthode HTTP GET Index n’a pas de boucle ni de branchement, et elle appelle seulement une méthode. Le test unitaire pour cette action :
- Simule le service
IBrainstormSessionRepositoryà l’aide de la méthodeGetTestSessions.GetTestSessionscrée deux sessions de brainstorming fictives avec des dates et des noms de session. - Exécute la méthode nommée
Index. - Fait des assertions sur le résultat retourné par la méthode :
- Un ViewResult est renvoyé.
- Le ViewDataDictionary.Model est un
StormSessionViewModel. - Deux sessions de brainstorming sont stockées dans le
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;
}
Le test de la méthode HomeHTTP POST Index du contrôleur Home vérifie que :
- Quand ModelState.IsValid est
false, la méthode d’action retourne un 400 Bad RequestViewResult avec les données appropriées. - Lorsque
ModelState.IsValidesttrue:- La méthode
Addsur le dépôt est appelée. - Un RedirectToActionResult est retourné avec les arguments corrects.
- La méthode
Un état de modèle non valide est testé en ajoutant des erreurs avec AddModelError, comme le montre le premier test ci-dessous :
[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();
}
Lorsque ModelState n’est pas valide, le même ViewResult est retourné, comme pour une requête GET. Le test ne tente pas de passer un modèle non valide. Le passage d’un modèle non valide n’est pas une approche valable, car la liaison de modèle n’est pas en cours d’exécution (même si un test d’intégration utilise bien la liaison de modèle). Dans ce cas, la liaison de modèle n’est pas testée. Ces tests unitaires ne testent que le code de la méthode d’action.
Le second test vérifie cela quand le ModelState est valide :
- Un nouveau
BrainstormSessionest ajouté (par le biais du référentiel). - La méthode renvoie un
RedirectToActionResultavec les propriétés attendues.
Les appels simulés qui ne sont pas appelés sont normalement ignorés, mais l’appel de Verifiable à la fin de l’appel de configuration autorise la validation des mocks dans le test. Ceci est effectué avec l’appel à mockRepo.Verify, qui fait échouer le test si la méthode attendue n’a pas été appelée.
Note
La bibliothèque Moq utilisée dans cet exemple permet la combinaison d’éléments fictifs vérifiables (ou « stricts ») avec des éléments fictifs non vérifiables (également appelés stubs ou éléments « lâches »). Découvrez plus d’informations sur la personnalisation du comportement des mocks avec Moq.
Dans l’exemple d’application, SessionController affiche des informations relatives à une session de brainstorming. Le contrôleur inclut la logique pour traiter des valeurs id non valides (il existe deux scénarios return dans l’exemple suivant pour couvrir ces scénarios). La dernière instruction return retourne un nouveau StormSessionViewModel dans l’affichage (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);
}
}
Les tests unitaires comprennent un test pour chaque scénario return de l’action Index du contrôleur de session :
[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);
}
En passant au contrôleur Ideas, l’application expose des fonctionnalités, comme une API web sur la route api/ideas :
- Une liste d’idées (
IdeaDTO) associées à une session de brainstorming est renvoyée par la méthodeForSession. - La méthode
Createajoute de nouvelles idées à une session.
[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);
}
Évitez de retourner des entités de domaine métier directement via des appels d’API. Entités de domaine :
- Incluent souvent plus de données que ce dont a besoin le client.
- Coupler inutilement le modèle de domaine interne de l’application à l’API exposée publiquement.
La mise en correspondance entre les entités du domaine et les types renvoyés au client peut être effectuée :
- Manuellement avec un LINQ
Select, comme dans l’application exemple. Pour plus d’informations, consultez LINQ (Language-Integrated Query). - Automatiquement avec une bibliothèque, telle que AutoMapper.
Ensuite, l’exemple d’application présente des tests unitaires pour les méthodes API Create et ForSession du contrôleur Ideas.
L’exemple d’application contient deux tests ForSession. Le premier test détermine si ForSession retourne un NotFoundObjectResult (HTTP Non trouvé) pour une session non valide :
[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);
}
La deuxième test ForSession détermine si ForSession retourne une liste d’idées de session (<List<IdeaDTO>>) pour une session valide. Les vérifications contrôlent également la première idée pour confirmer que sa propriété Name est correcte :
[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);
}
Pour tester le comportement de la méthode Create quand le ModelState n’est pas valide, l’exemple d’application ajoute une erreur de modèle au contrôleur dans le cadre de ce test. N’essayez pas de tester la validation de modèle ou la liaison de modèle dans des tests unitaires, testez simplement le comportement de votre méthode d’action quand elle est confrontée à un ModelState non valide :
[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);
}
Le deuxième test de Create dépend du retour de null par le dépôt, le dépôt fictif est donc configuré pour retourner null. Il est inutile de créer une base de données de test (dans la mémoire ou ailleurs) et de construire une requête qui retourne ce résultat. Le test peut être effectué en une seule instruction, comme le montre l’exemple de code :
[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);
}
Le troisième test Create, Create_ReturnsNewlyCreatedIdeaForSession, vérifie que la méthode UpdateAsync du dépôt est appelée. Le mock est appelé avec Verifiable, puis la méthode Verify du repository mocké est appelée pour confirmer que la méthode vérifiable est exécutée. Ce n’est pas au test unitaire de vérifier que la méthode UpdateAsync a enregistré les données : cela peut être vérifié avec un test d’intégration.
[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);
}
Test ActionResult<T>
Dans ASP.NET Core 2.1 ou version ultérieure, ActionResult<T> (ActionResult<TValue>) vous permet de retourner un type dérivant de ActionResult, ou de retourner un type spécifique.
L’exemple d’application comprend une méthode qui retourne un List<IdeaDTO> pour un id de session donné. Si la session id n’existe pas, le contrôleur retourne 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;
}
Deux tests du contrôleur ForSessionActionResult sont inclus dans ApiIdeasControllerTests.
Le premier test confirme que le contrôleur retourne un ActionResult, et non une liste inexistante d’idées pour une session inexistante id :
- Le type
ActionResultestActionResult<List<IdeaDTO>>. - Le Result est un 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);
}
Pour un id de session valide, le deuxième test confirme que la méthode retourne :
- Un
ActionResultavec un typeList<IdeaDTO>. -
ActionResult<T>.Value est un type
List<IdeaDTO>. - Le premier élément dans la liste est une idée valide correspondant à l’idée stockée dans la session fictive (obtenu en appelant
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);
}
L’exemple d’application comporte également une méthode pour créer un nouveau Idea pour une session donnée. Le contrôleur retourne :
- BadRequest pour un modèle invalide.
- NotFound si la session n’existe pas.
- CreatedAtAction lorsque la session est mise à jour avec la nouvelle idée.
[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);
}
Trois tests de CreateActionResult sont inclus dans ApiIdeasControllerTests.
Le premier texte confirme qu’un BadRequest est retourné pour un modèle non valide.
[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);
}
Le deuxième test vérifie qu’un NotFound est renvoyé si la session n’existe pas.
[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);
}
Pour un id de session valide, le dernier test confirme que :
- La méthode retourne un
ActionResultavec un typeBrainstormSession. -
ActionResult<T>.Result est un CreatedAtActionResult.
CreatedAtActionResults’apparente à une réponse 201 Created avec un en-têteLocation. -
ActionResult<T>.Value est un type
BrainstormSession. - L’appel fictif pour mettre à jour la session,
UpdateAsync(testSession), a été invoqué. L’appel de la méthodeVerifiableest contrôlé en exécutantmockRepo.Verify()dans les assertions. - Deux objets
Ideasont retournés pour la session. - Le dernier élément (
Ideaajouté par l’appel fictif àUpdateAsync) correspond ànewIdeaajouté à la session dans le test.
[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);
}
Contenu connexe
- Tests d’intégration avec ASP.NET Core
- Créer et exécuter des tests unitaires avec Visual Studio
- MyTested.AspNetCore.Mvc - Bibliothèque de test fluent pour ASP.NET Core MVC : bibliothèque de tests unitaires fortement typée, fournissant une interface fluide pour tester les applications MVC et API web. (Non géré ou pris en charge par Microsoft.)
- JustMockLite : framework de simulation pour les développeurs .NET. (Non maintenu ni pris en charge par Microsoft.)