Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
Od Steve Smith
Testy jednotek zahrnují testování části aplikace izolovaně od infrastruktury a závislostí. Pokud je logika kontroleru testování jednotek, testují se pouze obsah jedné akce, nikoli chování jejích závislostí nebo samotné architektury.
Kontrolery testování jednotek
Nastavte jednotkové testy akcí kontroleru, abyste se mohli zaměřit na chování kontroleru. Test jednotek kontroleru se vyhýbá scénářům, jako jsou filtry, směrování a svazování modelu. Testy, které pokrývají interakce mezi komponentami, které souhrnně reagují na požadavek, se zpracovávají integračními testy. Další informace o integračních testech najdete v článku Integrační testy v ASP.NET Core.
Pokud píšete vlastní filtry a směrování, otestujte je izolovaně, ne jako součást testů na konkrétní akci kontroleru.
Pokud chcete předvést testy jednotek kontroleru, projděte si následující kontroler v ukázkové aplikaci.
Zobrazit nebo stáhnout ukázkový kód (postup stažení)
Kontroler Home zobrazí seznam brainstormingových relací a umožňuje vytváření nových brainstormingových relací pomocí požadavku 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));
}
}
Předchozí kontroler:
- Řídí se principem explicitních závislostí.
- Očekává poskytnutí instance
IBrainstormSessionRepositorypomocí vkládání závislostí (DI). - Lze testovat s mockovanou službou pomocí architektury mock objektů
IBrainstormSessionRepository, například Moq. Objekt mock je uměle vytvořený objekt s předem definovaným chováním vlastností a metod, který se používá k testování. Další informace naleznete v tématu Úvod do integračních testů.
Metoda HTTP GET Index nemá žádnou smyčku ani větvení a volá pouze jednu metodu. Jednotkový test pro tuto akci:
- Namockuje službu
IBrainstormSessionRepositorypomocí metodyGetTestSessions.GetTestSessionsvytvoří dvě ukázkové brainstormingové relace s daty a názvy relací. - Spustí metodu
Index. - Vytvoří kontrolní výrazy ve výsledku vrácené metodou:
- Vrátí se ViewResult.
-
ViewDataDictionary.Model je
StormSessionViewModel. - Existují dvě brainstormingová sezení uložená v
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;
}
Metoda Home kontroleru HTTP POST Index ověřuje, že:
- Pokud je ModelState.IsValid
false, metoda akce vrátí 400 Bad Request s příslušnými daty. - Když
ModelState.IsValidjetrue:- Volá se metoda
Addna repozitáři. - Vrátí se RedirectToActionResult se správnými argumenty.
- Volá se metoda
Neplatný stav modelu se testuje přidáním chyb, AddModelError jak je znázorněno v prvním testu níže:
[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();
}
Pokud modelState není platný, vrátí se totéž ViewResult jako u požadavku GET. Test se nepokouší předat neplatný model. Předání neplatného modelu není platný přístup, protože vazba modelu není spuštěná (i když integrační test používá vazbu modelu). V tomto případě se vazba modelu neotestuje. Tyto testy jednotek testují pouze kód v metodě akce.
Druhý test ověří, že pokud ModelState je platný:
- Přidá se nový
BrainstormSession(prostřednictvím úložiště). - Metoda vrátí
RedirectToActionResults očekávanými vlastnostmi.
Mockovaná volání, která nejsou volána, se obvykle ignorují, ale volání Verifiable na konci volání setup umožňuje ověření mocků v testu. To se provádí voláním mockRepo.Verify, které způsobí selhání testu, pokud očekávaná metoda nebyla zavolána.
Note
Knihovna Moq použitá v této ukázce umožňuje kombinovat ověřitelné neboli „striktní“ mock objekty s neověřitelnými mock objekty (označovanými také jako „volné“ mock objekty nebo stuby). Přečtěte si další informace o přizpůsobení chování napodobení pomocí Moq.
SessionController v ukázkové aplikaci zobrazuje informace související s konkrétní brainstormingovou relací. Kontroler obsahuje logiku pro řešení neplatných id hodnot (v následujícím příkladu existují dva return scénáře pro pokrytí těchto scénářů). Poslední return příkaz vrátí zobrazení nový StormSessionViewModel (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);
}
}
Testy jednotek zahrnují jeden test pro každý return scénář v akci Index kontroleru 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);
}
Přechod na kontroler Návrhy zpřístupňuje funkce jako webové rozhraní API na api/ideas trase:
- Seznam nápadů (
IdeaDTO) přidružených k brainstormingové relaci vrátí metodaForSession. - Metoda
Createpřidává do relace nové nápady.
[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);
}
Vyhněte se tomu, abyste přímo vraceli entity obchodní domény přes volání API. Entity domény:
- Často obsahují více dat, než vyžaduje klient.
- Zbytečně propojte interní doménový model aplikace s veřejně vystaveným rozhraním API.
Mapování mezi entitami domény a typy vrácené klientovi je možné provést:
- Ručně pomocí LINQ
Select, jak používá ukázková aplikace. Další informace naleznete v tématu LINQ (Language Integrated Query). - Automaticky s knihovnou, například automatickým mapováním.
Dále ukázková aplikace předvádí testy jednotek pro CreateForSession metody rozhraní API kontroleru Ideas.
Ukázková aplikace obsahuje dva ForSession testy. První test určuje, jestli ForSession vrátí NotFoundObjectResult hodnotu (HTTP Nenalezena) pro neplatnou relaci:
[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);
}
Druhý ForSession test určuje, jestli ForSession vrátí seznam nápadů relace (<List<IdeaDTO>>) pro platnou relaci. Kontroly také prověřují první myšlenku na ověření správnosti jeho Name vlastnosti:
[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);
}
Aby bylo možné otestovat chování metody Create, když je ModelState neplatný, ukázková aplikace přidá do kontroleru chybu modelu jako součást testu. Nepokoušejte se testovat ověření modelu nebo vazbu modelu v testech jednotek – pouze otestujte chování metody akce, když je konfrontováno s neplatným ModelState:
[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);
}
Druhý test Create závisí na vrácení nullúložiště , takže je nakonfigurované úložiště na vrácení null. Není potřeba vytvořit testovací databázi (v paměti nebo jinak) a vytvořit dotaz, který vrátí tento výsledek. Test lze provést v jednom příkazu, jak ukazuje ukázkový kód:
[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);
}
Třetí Create test, Create_ReturnsNewlyCreatedIdeaForSession, ověřuje, že je volána metoda UpdateAsync úložiště. Mock se volá pomocí Verifiable a metoda Verify napodobeného úložiště je volána, aby se potvrdilo, že je volána metoda označená jako verifiable. Není zodpovědností testu jednotek zajistit, aby metoda UpdateAsync uložila data – to lze provést integračním testem.
[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>
ActionResult<T> (ActionResult<TValue>) může vrátit typ odvozený z ActionResult nebo určitý typ.
Ukázková aplikace obsahuje metodu, která vrací List<IdeaDTO> pro danou relaci id. Pokud relace id neexistuje, vrátí kontroler 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;
}
Dva testy ForSessionActionResult řadiče jsou zahrnuty v sadě ApiIdeasControllerTests.
První test potvrzuje, že kontroler vrátí ActionResult, ale ne neexistující seznam nápadů pro neexistující relaci id:
- Typ
ActionResultjeActionResult<List<IdeaDTO>>. - Result je 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);
}
Pro platnou relaci iddruhý test potvrdí, že metoda vrátí:
-
ActionResults typemList<IdeaDTO>. -
ActionResult<T>.Value je typu
List<IdeaDTO>. - První položkou v seznamu je platný nápad odpovídající myšlence uložené v relaci napodobení (získané voláním
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);
}
Ukázková aplikace obsahuje také metodu vytvoření nové Idea pro danou relaci. Kontroler vrátí:
- BadRequest pro neplatný model.
- NotFound pokud relace neexistuje.
- CreatedAtAction při aktualizaci relace s novým nápadem.
[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);
}
Tři testy CreateActionResult jsou zahrnuty v ApiIdeasControllerTests.
První text potvrzuje, že BadRequest je vrácen pro neplatný model.
[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);
}
Druhý test zkontroluje, jestli NotFound se vrátí, pokud relace neexistuje.
[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);
}
Pro platnou relaci id závěrečný test potvrzuje, že:
- Metoda vrátí typ
ActionResultBrainstormSession. - The ActionResult<T>.Result je CreatedAtActionResult.
CreatedAtActionResultje analogická odpovědi 201 Created s hlavičkouLocation. -
ActionResult<T>.Value je typu
BrainstormSession. - Bylo vyvoláno simulované volání k aktualizaci relace,
UpdateAsync(testSession). VoláníVerifiablemetody je kontrolováno spuštěnímmockRepo.Verify()v kontrolních výrazech. - Pro relaci se vrátí dva
Ideaobjekty. - Poslední položka (
Ideapřidaná simulovaným volánímUpdateAsync) odpovídánewIdea, která byla přidána do relace v testu.
[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);
}
Kontrolery hrají ústřední roli v libovolné aplikaci ASP.NET Core MVC. Proto byste měli mít jistotu, že se řadiče chovají tak, jak mají. Automatizované testy můžou detekovat chyby před nasazením aplikace do produkčního prostředí.
Zobrazit nebo stáhnout ukázkový kód (postup stažení)
Jednotkové testy logiky řadiče
Testy jednotek zahrnují testování části aplikace izolovaně od infrastruktury a závislostí. Pokud je logika kontroleru testování jednotek, testují se pouze obsah jedné akce, nikoli chování jejích závislostí nebo samotné architektury.
Nastavte jednotkové testy akcí kontroleru, abyste se mohli zaměřit na chování kontroleru. Test jednotek kontroleru se vyhýbá scénářům, jako jsou filtry, směrování a svazování modelu. Testy, které pokrývají interakce mezi komponentami, které souhrnně reagují na požadavek, se zpracovávají integračními testy. Další informace o integračních testech najdete v článku Integrační testy v ASP.NET Core.
Pokud píšete vlastní filtry a směrování, otestujte je izolovaně, ne jako součást testů na konkrétní akci kontroleru.
Pokud chcete předvést testy jednotek kontroleru, projděte si následující kontroler v ukázkové aplikaci. Kontroler Home zobrazí seznam brainstormingových relací a umožňuje vytváření nových brainstormingových relací pomocí požadavku 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));
}
}
Předchozí kontroler:
- Řídí se principem explicitních závislostí.
- Očekává poskytnutí instance
IBrainstormSessionRepositorypomocí vkládání závislostí (DI). - Lze testovat s mockovanou službou pomocí architektury mock objektů
IBrainstormSessionRepository, například Moq. Objekt mock je uměle vytvořený objekt s předem definovaným chováním vlastností a metod, který se používá k testování. Další informace naleznete v tématu Úvod do integračních testů.
Metoda HTTP GET Index nemá žádnou smyčku ani větvení a volá pouze jednu metodu. Jednotkový test pro tuto akci:
- Namockuje službu
IBrainstormSessionRepositorypomocí metodyGetTestSessions.GetTestSessionsvytvoří dvě ukázkové brainstormingové relace s daty a názvy relací. - Spustí metodu
Index. - Vytvoří kontrolní výrazy ve výsledku vrácené metodou:
- Vrátí se ViewResult.
-
ViewDataDictionary.Model je
StormSessionViewModel. - Existují dvě brainstormingová sezení uložená v
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;
}
Metoda Home kontroleru HTTP POST Index ověřuje, že:
- Pokud je ModelState.IsValid
false, metoda akce vrátí 400 Bad Request s příslušnými daty. - Když
ModelState.IsValidjetrue:- Volá se metoda
Addna repozitáři. - Vrátí se RedirectToActionResult se správnými argumenty.
- Volá se metoda
Neplatný stav modelu se testuje přidáním chyb, AddModelError jak je znázorněno v prvním testu níže:
[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();
}
Pokud modelState není platný, vrátí se totéž ViewResult jako u požadavku GET. Test se nepokouší předat neplatný model. Předání neplatného modelu není platný přístup, protože vazba modelu není spuštěná (i když integrační test používá vazbu modelu). V tomto případě se vazba modelu neotestuje. Tyto testy jednotek testují pouze kód v metodě akce.
Druhý test ověří, že pokud ModelState je platný:
- Přidá se nový
BrainstormSession(prostřednictvím úložiště). - Metoda vrátí
RedirectToActionResults očekávanými vlastnostmi.
Mockovaná volání, která nejsou volána, se obvykle ignorují, ale volání Verifiable na konci volání setup umožňuje ověření mocků v testu. To se provádí voláním mockRepo.Verify, které způsobí selhání testu, pokud očekávaná metoda nebyla zavolána.
Note
Knihovna Moq použitá v této ukázce umožňuje kombinovat ověřitelné neboli „striktní“ mock objekty s neověřitelnými mock objekty (označovanými také jako „volné“ mock objekty nebo stuby). Přečtěte si další informace o přizpůsobení chování napodobení pomocí Moq.
SessionController v ukázkové aplikaci zobrazuje informace související s konkrétní brainstormingovou relací. Kontroler obsahuje logiku pro řešení neplatných id hodnot (v následujícím příkladu existují dva return scénáře pro pokrytí těchto scénářů). Poslední return příkaz vrátí zobrazení nový StormSessionViewModel (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);
}
}
Testy jednotek zahrnují jeden test pro každý return scénář v akci Index kontroleru 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);
}
Přechod na kontroler Návrhy zpřístupňuje funkce jako webové rozhraní API na api/ideas trase:
- Seznam nápadů (
IdeaDTO) přidružených k brainstormingové relaci vrátí metodaForSession. - Metoda
Createpřidává do relace nové nápady.
[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);
}
Vyhněte se tomu, abyste přímo vraceli entity obchodní domény přes volání API. Entity domény:
- Často obsahují více dat, než vyžaduje klient.
- Zbytečně propojte interní doménový model aplikace s veřejně vystaveným rozhraním API.
Mapování mezi entitami domény a typy vrácené klientovi je možné provést:
- Ručně pomocí LINQ
Select, jak používá ukázková aplikace. Další informace naleznete v tématu LINQ (Language Integrated Query). - Automaticky s knihovnou, například automatickým mapováním.
Dále ukázková aplikace předvádí testy jednotek pro CreateForSession metody rozhraní API kontroleru Ideas.
Ukázková aplikace obsahuje dva ForSession testy. První test určuje, jestli ForSession vrátí NotFoundObjectResult hodnotu (HTTP Nenalezena) pro neplatnou relaci:
[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);
}
Druhý ForSession test určuje, jestli ForSession vrátí seznam nápadů relace (<List<IdeaDTO>>) pro platnou relaci. Kontroly také prověřují první myšlenku na ověření správnosti jeho Name vlastnosti:
[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);
}
Aby bylo možné otestovat chování metody Create, když je ModelState neplatný, ukázková aplikace přidá do kontroleru chybu modelu jako součást testu. Nepokoušejte se testovat ověření modelu nebo vazbu modelu v testech jednotek – pouze otestujte chování metody akce, když je konfrontováno s neplatným ModelState:
[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);
}
Druhý test Create závisí na vrácení nullúložiště , takže je nakonfigurované úložiště na vrácení null. Není potřeba vytvořit testovací databázi (v paměti nebo jinak) a vytvořit dotaz, který vrátí tento výsledek. Test lze provést v jednom příkazu, jak ukazuje ukázkový kód:
[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);
}
Třetí Create test, Create_ReturnsNewlyCreatedIdeaForSession, ověřuje, že je volána metoda UpdateAsync úložiště. Mock se volá pomocí Verifiable a metoda Verify napodobeného úložiště je volána, aby se potvrdilo, že je volána metoda označená jako verifiable. Není zodpovědností testu jednotek zajistit, aby metoda UpdateAsync uložila data – to lze provést integračním testem.
[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>
V ASP.NET Core 2.1 nebo novějším umožňuje ActionResult<T> (ActionResult<TValue>) vrátit typ odvozený nebo ActionResult vrátit určitý typ.
Ukázková aplikace obsahuje metodu, která vrací List<IdeaDTO> pro danou relaci id. Pokud relace id neexistuje, vrátí kontroler 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;
}
Dva testy ForSessionActionResult řadiče jsou zahrnuty v sadě ApiIdeasControllerTests.
První test potvrzuje, že kontroler vrátí ActionResult, ale ne neexistující seznam nápadů pro neexistující relaci id:
- Typ
ActionResultjeActionResult<List<IdeaDTO>>. - Result je 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);
}
Pro platnou relaci iddruhý test potvrdí, že metoda vrátí:
-
ActionResults typemList<IdeaDTO>. -
ActionResult<T>.Value je typu
List<IdeaDTO>. - První položkou v seznamu je platný nápad odpovídající myšlence uložené v relaci napodobení (získané voláním
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);
}
Ukázková aplikace obsahuje také metodu vytvoření nové Idea pro danou relaci. Kontroler vrátí:
- BadRequest pro neplatný model.
- NotFound pokud relace neexistuje.
- CreatedAtAction při aktualizaci relace s novým nápadem.
[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);
}
Tři testy CreateActionResult jsou zahrnuty v ApiIdeasControllerTests.
První text potvrzuje, že BadRequest je vrácen pro neplatný model.
[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);
}
Druhý test zkontroluje, jestli NotFound se vrátí, pokud relace neexistuje.
[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);
}
Pro platnou relaci id závěrečný test potvrzuje, že:
- Metoda vrátí typ
ActionResultBrainstormSession. - The ActionResult<T>.Result je CreatedAtActionResult.
CreatedAtActionResultje analogická odpovědi 201 Created s hlavičkouLocation. -
ActionResult<T>.Value je typu
BrainstormSession. - Bylo vyvoláno simulované volání k aktualizaci relace,
UpdateAsync(testSession). VoláníVerifiablemetody je kontrolováno spuštěnímmockRepo.Verify()v kontrolních výrazech. - Pro relaci se vrátí dva
Ideaobjekty. - Poslední položka (
Ideapřidaná simulovaným volánímUpdateAsync) odpovídánewIdea, která byla přidána do relace v testu.
[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);
}
Související obsah
- Integrační testy v ASP.NET Core
- Vytváření a spouštění testů jednotek pomocí sady Visual Studio
- MyTested.AspNetCore.Mvc – Fluent Testing Library for ASP.NET Core MVC: Knihovna pro testování jednotek se silnými typy, která poskytuje fluentní rozhraní pro testování aplikací MVC a webových rozhraní API. (Není udržováno ani podporováno společností Microsoft.)
- JustMockLite: framework pro mockování pro vývojáře .NET. (Není udržováno ani podporováno společností Microsoft.)