Logika kontroleru testů jednotek v ASP.NET Core
Autor: 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 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, se zpracovávají integračními testy. Další informace o integračníchtestch ASP.NET ch
Pokud píšete vlastní filtry a trasy, 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.
Zobrazení nebo stažení ukázkového kódu (postup stažení)
Kontroler Home zobrazí seznam debat a umožňuje vytváření nových debat 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í kontroler:
- Řídí se principem explicitních závislostí.
- Očekává, že injektáž závislostí (DI) poskytne instanci
IBrainstormSessionRepository
. - Můžete testovat pomocí napodobené služby pomocí napodobené
IBrainstormSessionRepository
architektury objektů, jako je Například Moq. Napodobený 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. Test jednotek pro tuto akci:
- Napodobí
IBrainstormSessionRepository
službu pomocíGetTestSessions
metody.GetTestSessions
vytvoří dvě napodobené debaty s daty a názvy relací. - Spustí metodu
Index
. - Vytvoří kontrolní výrazy ve výsledku vrácené metodou:
- Vrátí se hodnota A 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 je ModelState.IsValid
false
, metoda akce vrátí 400 Chybný požadavek ViewResult s příslušnými daty. - Kdy
ModelState.IsValid
jetrue
:- Volá se
Add
metoda v úložišti. - Vrátí se funkce A RedirectToActionResult se správnými argumenty.
- Volá se
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í
RedirectToActionResult
hodnotu s očekávanými vlastnostmi.
Volání napodobená volání, která nejsou volána, se obvykle ignorují, ale volání Verifiable
na konci volání nastavení umožňuje ověření napodobení v testu. To se provádí s voláním mockRepo.Verify
, což selže test, pokud očekávaná metoda nebyla volána.
Poznámka:
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í napodobení pomocí Moq.
SessionController v ukázkové aplikaci zobrazuje informace související s konkrétní relací debaty. 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ářů). 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);
}
}
Testy jednotek zahrnují jeden test pro každý return
scénář v akci kontroleru Index
relace:
[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:
- Metoda vrátí
ForSession
seznam nápadů (IdeaDTO
) přidružených k relaci debaty. - Metoda
Create
přidá 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é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 Create
ForSession
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);
}
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 testovat ověření modelu nebo vazbu modelu v testech jednotek – pouze otestujte chování metody akce, když je konfrontováno s neplatným ModelState
kódem:
[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 , ověřuje, Create_ReturnsNewlyCreatedIdeaForSession
že je volána metoda úložiště UpdateAsync
. Napodobení se volá pomocí Verifiable
a 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>
ActionResult<T> (ActionResult<TValue>) může vrátit typ odvozený nebo ActionResult
vrátit určitý typ.
Ukázková aplikace obsahuje metodu, která vrací pro danou List<IdeaDTO>
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
kontroleru jsou zahrnuty v sadě ApiIdeasControllerTests
.
První test potvrdí, že kontroler vrátí ActionResult
neexistující seznam nápadů pro neexistující relaci id
:
- Typ
ActionResult
jeActionResult<List<IdeaDTO>>
. - Je to Result .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 id
druhý test potvrdí, že metoda vrátí:
List<IdeaDTO>
TypActionResult
.- ActionResult <T>. Hodnota je
List<IdeaDTO>
typ. - 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 novou myšlenkou.
[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 ApiIdeasControllerTests
jsou zahrnuty tři testy CreateActionResult
.
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í typ
ActionResult
BrainstormSession
. - ActionResult <T>. Výsledek je .CreatedAtActionResult
CreatedAtActionResult
je podobná odpovědi vytvořené v roce 201 s hlavičkouLocation
. - ActionResult <T>. Hodnota je
BrainstormSession
typ. - Volání napodobení aktualizace relace ,
UpdateAsync(testSession)
bylo vyvoláno. VoláníVerifiable
metody je kontrolováno spuštěnímmockRepo.Verify()
v kontrolních výrazech. - Pro relaci se vrátí dva
Idea
objekty. - Poslední položka (
Idea
přidaná volánímUpdateAsync
napodobení) odpovídánewIdea
přidané relaci 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 kontrolery chovají tak, jak mají. Automatizované testy můžou detekovat chyby před nasazením aplikace do produkčního prostředí.
Zobrazení nebo stažení ukázkového kódu (postup stažení)
Testy jednotek logiky kontroleru
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 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, se zpracovávají integračními testy. Další informace o integračníchtestch ASP.NET ch
Pokud píšete vlastní filtry a trasy, 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 debat a umožňuje vytváření nových debat 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í kontroler:
- Řídí se principem explicitních závislostí.
- Očekává, že injektáž závislostí (DI) poskytne instanci
IBrainstormSessionRepository
. - Můžete testovat pomocí napodobené služby pomocí napodobené
IBrainstormSessionRepository
architektury objektů, jako je Například Moq. Napodobený 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. Test jednotek pro tuto akci:
- Napodobí
IBrainstormSessionRepository
službu pomocíGetTestSessions
metody.GetTestSessions
vytvoří dvě napodobené debaty s daty a názvy relací. - Spustí metodu
Index
. - Vytvoří kontrolní výrazy ve výsledku vrácené metodou:
- Vrátí se hodnota A 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 je ModelState.IsValid
false
, metoda akce vrátí 400 Chybný požadavek ViewResult s příslušnými daty. - Kdy
ModelState.IsValid
jetrue
:- Volá se
Add
metoda v úložišti. - Vrátí se funkce A RedirectToActionResult se správnými argumenty.
- Volá se
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í
RedirectToActionResult
hodnotu s očekávanými vlastnostmi.
Volání napodobená volání, která nejsou volána, se obvykle ignorují, ale volání Verifiable
na konci volání nastavení umožňuje ověření napodobení v testu. To se provádí s voláním mockRepo.Verify
, což selže test, pokud očekávaná metoda nebyla volána.
Poznámka:
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í napodobení pomocí Moq.
SessionController v ukázkové aplikaci zobrazuje informace související s konkrétní relací debaty. 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ářů). 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);
}
}
Testy jednotek zahrnují jeden test pro každý return
scénář v akci kontroleru Index
relace:
[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:
- Metoda vrátí
ForSession
seznam nápadů (IdeaDTO
) přidružených k relaci debaty. - Metoda
Create
přidá 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é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 Create
ForSession
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);
}
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 testovat ověření modelu nebo vazbu modelu v testech jednotek – pouze otestujte chování metody akce, když je konfrontováno s neplatným ModelState
kódem:
[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 , ověřuje, Create_ReturnsNewlyCreatedIdeaForSession
že je volána metoda úložiště UpdateAsync
. Napodobení se volá pomocí Verifiable
a 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í pro danou List<IdeaDTO>
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
kontroleru jsou zahrnuty v sadě ApiIdeasControllerTests
.
První test potvrdí, že kontroler vrátí ActionResult
neexistující seznam nápadů pro neexistující relaci id
:
- Typ
ActionResult
jeActionResult<List<IdeaDTO>>
. - Je to Result .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 id
druhý test potvrdí, že metoda vrátí:
List<IdeaDTO>
TypActionResult
.- ActionResult <T>. Hodnota je
List<IdeaDTO>
typ. - 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 novou myšlenkou.
[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 ApiIdeasControllerTests
jsou zahrnuty tři testy CreateActionResult
.
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í typ
ActionResult
BrainstormSession
. - ActionResult <T>. Výsledek je .CreatedAtActionResult
CreatedAtActionResult
je podobná odpovědi vytvořené v roce 201 s hlavičkouLocation
. - ActionResult <T>. Hodnota je
BrainstormSession
typ. - Volání napodobení aktualizace relace ,
UpdateAsync(testSession)
bylo vyvoláno. VoláníVerifiable
metody je kontrolováno spuštěnímmockRepo.Verify()
v kontrolních výrazech. - Pro relaci se vrátí dva
Idea
objekty. - Poslední položka (
Idea
přidaná volánímUpdateAsync
napodobení) odpovídánewIdea
přidané relaci 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);
}
Další materiály
- 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 a poskytuje fluentní rozhraní pro testování aplikací MVC a webových rozhraní API. (Microsoft neudržuje ani nepodporuje.)
- JustMockLite: napodobování rozhraní pro vývojáře .NET. (Microsoft neudržuje ani nepodporuje.)