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 typ
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í. Při jednotkovém testování logiky kontroleru se testuje pouze obsah jedné akce, nikoli chování jeho závislostí ani samotného frameworku.
Nastavte testy jednotek akcí kontroleru, abyste se mohli zaměřit na chování kontroleru. Test jednotek kontroleru zabraňuje scénářům, jako jsou filtry, směrování a vazby modelu. Testy, které pokrývají interakce mezi komponentami, které souhrnně reagují na požadavek, jsou pokryty integračními testy. Další informace o integračníchtestch ASP.NET ch
Pokud píšete vlastní filtry a routy, otestujte je izolovaně, ne jako součást testů na konkrétní akci kontroleru.
Pro ukázku testů jednotek kontroleru si projděte 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í s požadavkem 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í řadič:
- Řídí se principem explicitních závislostí.
-
Očekává, že injektáž závislostí (DI) poskytne instanci
IBrainstormSessionRepository. - Lze testovat pomocí mockované služby s využitím frameworku pro mock objekty
IBrainstormSessionRepository, jako je například Moq. Mock objekt je objekt s předem určenou sadou vlastností a chování metod používaných 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:
- Vytvoří mock pro službu
IBrainstormSessionRepositorypomocí metodyGetTestSessions.GetTestSessionsvytvoří dvě ukázkové brainstormingové relace s daty a názvy relací. - Spustí metodu
Index. - Provádí aserce nad výsledkem vráceným metodou:
- Vrátí se ViewResult.
-
ViewDataDictionary.Model je
StormSessionViewModel. - Existují dvě debaty 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 , metoda akce vrátí
falses příslušnými daty. - Kdy
ModelState.IsValidjetrue:- Volá se metoda
Addna repozitáři. - Vrátí se A RedirectToActionResult se správnými argumenty.
- Volá se metoda
Neplatný stav modelu se testuje přidáním chyb pomocí 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 netestuje. 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 vyvolána, se obvykle ignorují, ale volání Verifiable na konci volání setupu umožňuje v testu ověření mocku. To se provádí s voláním mockRepo.Verify, což selže test, pokud očekávaná metoda nebyla volána.
Note
Knihovna Moq použitá v této ukázce umožňuje kombinovat ověřitelné nebo "striktní", napodobení s neověřitelnými napodobením (označovanými také jako "volné" napodobení nebo zástupné procedury). Přečtěte si další informace o přizpůsobení chování mocku pomocí Moq.
SessionController v ukázkové aplikaci zobrazuje informace související s konkrétní brainstormingovou relací. Kontroler obsahuje logiku pro řešení neplatných hodnot id (v následujícím příkladu jsou dva scénáře return, které tyto případy pokrývají). Konečný return příkaz vrátí nové StormSessionViewModel zobrazení (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);
}
}
Jednotkové testy obsahují jeden test pro každý scénář return 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);
}
V kontroleru Návrhy aplikace zpřístupňuje funkce jako webové rozhraní API na trase api/ideas:
- Seznam nápadů (
IdeaDTO) spojených s brainstormingovou relací vrací 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 vracení entit obchodní domény přímo prostřednictvím volání rozhraní API. Entity domén:
- Často obsahují více dat, než vyžaduje klient.
- Zbytečně nepropojujte interní doménový model aplikace s veřejně dostupným API.
Mapování mezi entitami domény a typy vracenými klientovi lze 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 AutoMapper.
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 zjišťuje, zda ForSession vrátí NotFoundObjectResult (HTTP Not Found) 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ávrhů relací (<List<IdeaDTO>>) pro platnou relaci. Kontroly také prověřují první položku, aby potvrdily správnost její vlastnosti Name:
[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);
}
Pokud chcete otestovat chování Create metody, když ModelState je neplatná, ukázková aplikace přidá do kontroleru chybu modelu jako součást testu. Nepokoušejte se v jednotkových testech testovat validaci modelu ani mapování modelu – otestujte pouze chování akční metody, když narazí na neplatný 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 tom, že úložiště vrátí null, takže je mock úložiště nakonfigurováno tak, aby vrátilo 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 jediném 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 , ověřuje, Create_ReturnsNewlyCreatedIdeaForSessionže je volána metoda úložiště UpdateAsync . Napodobení se volá pomocí Verifiablea metoda napodobeného úložiště Verify je volána k potvrzení, že je ověřená metoda provedena. Není zodpovědností testu jednotek zajistit, aby UpdateAsync metoda uložila data – která se dá provést s 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>. Hodnota je
List<IdeaDTO>typ. - První položka v seznamu je platná idea odpovídající ideji uložené ve falešné testovací relaci (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. Řadič vrátí:
- BadRequest pro neplatný model.
- NotFound pokud relace neexistuje.
- CreatedAtAction když je relace aktualizována o nový nápad.
[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);
}
Do souboru CreateActionResultjsou zahrnuty tři testy 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 konečný test potvrzuje, že:
- Metoda vrátí
ActionResulttypuBrainstormSession. - The ActionResult<T>.Result je CreatedAtActionResult.
CreatedAtActionResultje analogická odpovědi 201 Created s hlavičkouLocation. -
ActionResult<T>.Value je typu
BrainstormSession. - Fiktivní volání pro aktualizaci relace,
UpdateAsync(testSession), bylo vyvoláno. VoláníVerifiablemetody je kontrolováno spuštěnímmockRepo.Verify()v kontrolních výrazech. - Pro sezení se vrátí dva
Ideaobjekty. - Poslední položka (
Idea, přidaná voláním napodobení funkceUpdateAsync) odpovídánewIdea, přidané 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.)