Komponententests für die Controllerlogik in ASP.NET CoreUnit test controller logic in ASP.NET Core
Von Steve SmithBy Steve Smith
Komponententests beinhalten das Testen einer App-Komponente isoliert von ihrer Infrastruktur und ihren Abhängigkeiten.Unit tests involve testing a part of an app in isolation from its infrastructure and dependencies. Bei einem Komponententest der Controllerlogik werden nur die Inhalte einer einzelnen Aktion getestet, nicht das Verhalten ihrer Abhängigkeiten oder des Frameworks selbst.When unit testing controller logic, only the contents of a single action are tested, not the behavior of its dependencies or of the framework itself.
Komponententests für ControllerUnit testing controllers
Richten Sie Komponententests für Controlleraktionen ein, um sich auf das Verhalten des Controllers zu konzentrieren.Set up unit tests of controller actions to focus on the controller's behavior. Bei einem Komponententest des Controllers werden Szenarien wie Filter, Routing und Modellbindung vermieden.A controller unit test avoids scenarios such as filters, routing, and model binding. Tests, die die Interaktionen zwischen Komponenten betreffen, die gemeinsam auf eine Anforderung reagieren, werden mithilfe von Integrationstests behandelt.Tests that cover the interactions among components that collectively respond to a request are handled by integration tests. Weitere Informationen zu Integrationstests finden Sie unter Integrationstests in ASP.NET Core.For more information on integration tests, see Integrationstests in ASP.NET Core.
Wenn Sie benutzerdefinierte Filter und Routen schreiben, sollten Sie für diese isoliert einen Komponententest durchführen, der nicht Bestandteil eines Tests für eine bestimmte Controlleraktion ist.If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.
Um mehr über Komponententests für Controller zu erfahren, sehen Sie sich den folgenden Controller in der Beispiel-App an.To demonstrate controller unit tests, review the following controller in the sample app.
Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)View or download sample code (how to download)
Der Homecontroller zeigt eine Liste von Brainstormingsitzungen an und ermöglicht das Erstellen neuer Brainstormingsitzungen mit einer POST-Anforderung:The Home controller displays a list of brainstorming sessions and allows the creation of new brainstorming sessions with a POST request:
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));
}
}
Für den oben aufgeführten Controller gilt Folgendes:The preceding controller:
- Er folgt dem Prinzip der expliziten Abhängigkeiten.Follows the Explicit Dependencies Principle.
- Er erwartet, dass Abhängigkeitsinjektion (Dependency Injection, DI) eine Instanz von
IBrainstormSessionRepository
bereitstellt.Expects dependency injection (DI) to provide an instance ofIBrainstormSessionRepository
. - Er kann mit einem simulierten
IBrainstormSessionRepository
-Dienst mithilfe eines Pseudoobjektframeworks wie Moq getestet werden.Can be tested with a mockedIBrainstormSessionRepository
service using a mock object framework, such as Moq. Ein Pseudoobjekt ist ein künstliches Objekt mit einem vordefiniertem Satz von Eigenschaften und Methodenverhalten, das zum Testen verwendet wird.A mocked object is a fabricated object with a predetermined set of property and method behaviors used for testing. Weitere Informationen finden Sie unter Einführung in Integrationstests.For more information, see Introduction to integration tests.
Die HTTP GET Index
-Methode verfügt weder über Schleifen noch über Verzweigungen und ruft nur eine Methode auf.The HTTP GET Index
method has no looping or branching and only calls one method. Der Komponententest für diese Aktion:The unit test for this action:
- Simuliert den
IBrainstormSessionRepository
-Dienst mit derGetTestSessions
-Methode.Mocks theIBrainstormSessionRepository
service using theGetTestSessions
method.GetTestSessions
erstellt zwei simulierte Brainstormingsitzungen mit Datumsangaben und Sitzungsnamen.GetTestSessions
creates two mock brainstorm sessions with dates and session names. - Führt die
Index
-Methode aus.Executes theIndex
method. - Nimmt Assertionen für das von der Methode zurückgegebene Ergebnis vor:Makes assertions on the result returned by the method:
- Ein ViewResult wird zurückgegeben.A ViewResult is returned.
- Das ViewDataDictionary.Model ist ein
StormSessionViewModel
.The ViewDataDictionary.Model is aStormSessionViewModel
. - Es werden zwei Brainstormingsitzungen in
ViewDataDictionary.Model
gespeichert.There are two brainstorming sessions stored in theViewDataDictionary.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;
}
Die HTTP POST Index
-Methodentests des Homecontrollers bestätigen dies:The Home controller's HTTP POST Index
method tests verifies that:
- Wenn ModelState.IsValid
false
ist, gibt die Aktionsmethode den Status 400 Bad Request zurück ( ViewResult mit den entsprechenden Daten).When ModelState.IsValid isfalse
, the action method returns a 400 Bad Request ViewResult with the appropriate data. - Wenn
ModelState.IsValid``true
ist:WhenModelState.IsValid
istrue
:- Die
Add
-Methode für das Repository wird aufgerufen.TheAdd
method on the repository is called. - Ein RedirectToActionResult wird mit den richtigen Argumenten zurückgegeben.A RedirectToActionResult is returned with the correct arguments.
- Die
Ein ungültiger Modellstatus wird getestet, indem mithilfe von AddModelError (wie im ersten Test unten gezeigt) Fehler hinzugefügt werden:An invalid model state is tested by adding errors using AddModelError as shown in the first test below:
[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();
}
Wenn ModelState nicht gültig ist, wird das gleiche ViewResult
wie für eine GET-Anforderung zurückgegeben.When ModelState isn't valid, the same ViewResult
is returned as for a GET request. Der Test versucht nicht, ein ungültiges Modell zu übergeben.The test doesn't attempt to pass in an invalid model. Das Übergeben eines ungültigen Modells ist kein gültiger Ansatz, da die Modellbindung nicht ausgeführt wird (obwohl ein Integrationstest Modellbindung verwendet).Passing an invalid model isn't a valid approach, since model binding isn't running (although an integration test does use model binding). In diesem Fall wird die Modellbindung nicht getestet.In this case, model binding isn't tested. Bei diesen Komponententests wird nur der Code in der Aktionsmethode getestet.These unit tests are only testing the code in the action method.
Der zweite Test bestätigt dies, wenn der ModelState
gültig ist:The second test verifies that when the ModelState
is valid:
- Eine neue
BrainstormSession
wird hinzugefügt (über das Repository).A newBrainstormSession
is added (via the repository). - Die Methode gibt ein
RedirectToActionResult
mit den erwarteten Eigenschaften zurück.The method returns aRedirectToActionResult
with the expected properties.
Simulierte Aufrufe, die nicht aufgerufen werden, werden normalerweise ignoriert. Durch Aufrufen von Verifiable
am Ende des Einrichtungsaufrufs wird jedoch eine Pseudoüberprüfung im Test ermöglicht.Mocked calls that aren't called are normally ignored, but calling Verifiable
at the end of the setup call allows mock validation in the test. Dies erfolgt mit dem Aufruf von mockRepo.Verify
. Damit schlägt der Test fehl, wenn die erwartete Methode nicht aufgerufen wurde.This is performed with the call to mockRepo.Verify
, which fails the test if the expected method wasn't called.
Hinweis
Mit der in diesem Beispiel verwendeten Moq-Bibliothek können überprüfbare (oder „strikte“) Pseudoobjekte mit nicht überprüfbaren Pseudoobjekten (auch „nicht-strikte“ Pseudoobjekte oder „Stubs“ genannt) kombiniert werden.The Moq library used in this sample makes it possible to mix verifiable, or "strict", mocks with non-verifiable mocks (also called "loose" mocks or stubs). Weitere Informationen finden Sie unter Customizing Mock behavior with Moq (Anpassen des Verhaltens von Pseudoobjekten mit Moq).Learn more about customizing Mock behavior with Moq.
In der Beispiel-App werden Informationen zu einer bestimmten Brainstormingsitzung mit SessionController angezeigt.SessionController in the sample app displays information related to a particular brainstorming session. Der Controller enthält Logik, um ungültige id
Werte zu behandeln (es gibt im folgenden Beispiel zwei return
-Szenarien, die diese Fälle abdecken).The controller includes logic to deal with invalid id
values (there are two return
scenarios in the following example to cover these scenarios). Die endgültige return
-Anweisung gibt ein neues StormSessionViewModel
-Objekt an die Ansicht zurück (Controllers/SessionController.cs):The final return
statement returns a new StormSessionViewModel
to the view (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);
}
}
Die Komponententests enthalten einen Test für jedes return
-Szenario in der Index
-Aktion des Sitzungscontrollers:The unit tests include one test for each return
scenario in the Session controller Index
action:
[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);
}
Mit dem Wechsel zum Ideas-Controller stellt die App Funktionalität als Web-API auf der api/ideas
-Route zur Verfügung:Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas
route:
- Eine Liste von Ideen (
IdeaDTO
), die einer Brainstormingsitzung zugeordnet sind, wird von derForSession
-Methode zurückgegeben.A list of ideas (IdeaDTO
) associated with a brainstorming session is returned by theForSession
method. - Die
Create
-Methode fügt einer Sitzung neue Ideen hinzu.TheCreate
method adds new ideas to a session.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return Ok(result);
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return Ok(session);
}
Vermeiden Sie die Rückgabe von Geschäftsdomänenentitäten direkt über API-Aufrufe.Avoid returning business domain entities directly via API calls. Für Domänenentitäten gilt Folgendes:Domain entities:
- Sie enthalten häufig mehr Daten als für den Client erforderlich.Often include more data than the client requires.
- Sie koppeln unnötigerweise das interne Domänenmodell der App mit der öffentlich bereitgestellten API.Unnecessarily couple the app's internal domain model with the publicly exposed API.
Die Zuordnung zwischen Domänenentitäten und den Typen, die an den Client zurückgegeben werden, kann ausgeführt werden:Mapping between domain entities and the types returned to the client can be performed:
- Manuell mit einer LINQ-
Select
-Anweisung, wie sie von der Beispiel-App verwendet wird.Manually with a LINQSelect
, as the sample app uses. Weitere Informationen finden Sie unter LINQ (Language Integrated Query).For more information, see LINQ (Language Integrated Query). - Automatisch mit einer Bibliothek, z.B. mit AutoMapper.Automatically with a library, such as AutoMapper.
Anschließend demonstriert die Beispiel-App Komponententests für die API-Methoden Create
und ForSession
des Ideas-Controllers.Next, the sample app demonstrates unit tests for the Create
and ForSession
API methods of the Ideas controller.
Die Beispiel-App enthält zwei ForSession
-Tests.The sample app contains two ForSession
tests. Der erste Test ermittelt, ob ForSession
ein NotFoundObjectResult (HTTP Not Found) für eine ungültige Sitzung zurückgibt:The first test determines if ForSession
returns a NotFoundObjectResult (HTTP Not Found) for an invalid session:
[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);
}
Die zweite ForSession
-Test ermittelt, ob ForSession
eine Liste der Sitzungsideen (<List<IdeaDTO>>
) für eine gültige Sitzung zurückgibt.The second ForSession
test determines if ForSession
returns a list of session ideas (<List<IdeaDTO>>
) for a valid session. Die Tests untersuchen auch die erste Idee, um zu bestätigen, dass ihre Name
-Eigenschaft richtig ist:The checks also examine the first idea to confirm its Name
property is correct:
[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);
}
Um das Verhalten der Create
-Methode zu testen, wenn der ModelState
ungültig ist, fügt die Beispiel-App dem Controller einen Modellfehler als Teil des Tests hinzu.To test the behavior of the Create
method when the ModelState
is invalid, the sample app adds a model error to the controller as part of the test. Versuchen Sie nicht, die Modellüberprüfung oder Modellbindung in Komponententests zu testen. Testen Sie einfach das Verhalten der Aktionsmethode, wenn Sie mit einem ungültigen ModelState
konfrontiert wird:Don't try to test model validation or model binding in unit tests—just test the action method's behavior when confronted with an invalid 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);
}
Der zweite Test von Create
hängt davon ab, ob das Repository null
zurückgibt. Daher wird das Pseudorepository so konfiguriert, dass es null
zurückgibt.The second test of Create
depends on the repository returning null
, so the mock repository is configured to return null
. Es ist nicht erforderlich, eine Testdatenbank (im Arbeitsspeicher oder anderweitig) zu erstellen und eine Abfrage zu generieren, die dieses Ergebnis zurückgibt.There's no need to create a test database (in memory or otherwise) and construct a query that returns this result. Der Test kann mit einer einzigen Anweisung ausgeführt werden, wie der Beispielcode veranschaulicht:The test can be accomplished in a single statement, as the sample code illustrates:
[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);
}
Im dritten Create
-Test (Create_ReturnsNewlyCreatedIdeaForSession
) wird überprüft, ob die UpdateAsync
-Methode des Repositorys aufgerufen wird.The third Create
test, Create_ReturnsNewlyCreatedIdeaForSession
, verifies that the repository's UpdateAsync
method is called. Das Pseudoobjekt wird mit Verifiable
aufgerufen. Anschließend wird die Verify
-Methode des Pseudorepositorys aufgerufen, um zu bestätigen, dass die überprüfbare Methode ausgeführt wurde.The mock is called with Verifiable
, and the mocked repository's Verify
method is called to confirm the verifiable method is executed. Sicherzustellen, dass die Daten von der UpdateAsync
-Methode gespeichert wurden, gehört nicht zu den Aufgaben des Komponententests. Dies kann mit einem Integrationstest bestätigt werden.It's not the unit test's responsibility to ensure that the UpdateAsync
method saved the data—that can be performed with an integration test.
[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-Aktions Ergebnis<T>Test ActionResult<T>
In ASP.net Core 2,1 oder höher können Sie mit " aktionresult <T> ( ActionResult<TValue> )" einen Typ zurückgeben, der von abgeleitet wird, ActionResult
oder einen bestimmten Typ zurückgeben.In ASP.NET Core 2.1 or later, ActionResult<T> (ActionResult<TValue>) enables you to return a type deriving from ActionResult
or return a specific type.
Die Beispielanwendung enthält eine Methode, die ein List<IdeaDTO>
für eine bestimmte Sitzung id
zurückgibt.The sample app includes a method that returns a List<IdeaDTO>
for a given session id
. Wenn die Sitzung id
nicht vorhanden ist, gibt der Controller NotFound zurück:If the session id
doesn't exist, the controller returns 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;
}
Zwei Tests des ForSessionActionResult
-Controllers sind in ApiIdeasControllerTests
vorhanden.Two tests of the ForSessionActionResult
controller are included in the ApiIdeasControllerTests
.
Der erste Test bestätigt, dass der Controller ein ActionResult
, aber keine nicht vorhandene Liste von Ideen für eine nicht vorhandene Sitzungs-id
zurückgibt:The first test confirms that the controller returns an ActionResult
but not a nonexistent list of ideas for a nonexistent session id
:
- Der Typ von
ActionResult
istActionResult<List<IdeaDTO>>
.TheActionResult
type isActionResult<List<IdeaDTO>>
. - Result ist ein NotFoundObjectResult.The Result is a 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);
}
Für eine gültige Sitzungs-id
bestätigt der zweite Test, dass die Methode Folgendes zurückgibt:For a valid session id
, the second test confirms that the method returns:
- Ein
ActionResult
mit einemList<IdeaDTO>
-Typ.AnActionResult
with aList<IdeaDTO>
type. - Das Aktions Ergebnis <T> . Der Wert ist ein-
List<IdeaDTO>
Typ.The ActionResult<T>.Value is aList<IdeaDTO>
type. - Das erste Element in der Liste ist eine gültige Idee, die der in der Pseudositzung gespeicherten Idee entspricht (abgerufen durch den Aufruf von
GetTestSession
).The first item in the list is a valid idea matching the idea stored in the mock session (obtained by callingGetTestSession
).
[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);
}
Die Beispiel-App enthält auch eine Methode zum Erstellen einer neuen Idea
für eine bestimmte Sitzung.The sample app also includes a method to create a new Idea
for a given session. Der Controller gibt Folgendes zurück:The controller returns:
- BadRequest für ein ungültiges Modell.BadRequest for an invalid model.
- NotFound, wenn die Sitzung nicht vorhanden ist.NotFound if the session doesn't exist.
- CreatedAtAction, wenn die Sitzung mit der neuen Idee aktualisiert wird.CreatedAtAction when the session is updated with the new idea.
[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);
}
Drei Tests von CreateActionResult
sind in ApiIdeasControllerTests
enthalten.Three tests of CreateActionResult
are included in the ApiIdeasControllerTests
.
Der erste Test bestätigt, dass eine BadRequest für ein ungültiges Modell zurückgegeben wird.The first text confirms that a BadRequest is returned for an invalid 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);
}
Der zweite Test überprüft, ob ein NotFound-Element zurückgegeben wird, wenn die Sitzung nicht vorhanden ist.The second test checks that a NotFound is returned if the session doesn't exist.
[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);
}
Für eine gültige Sitzungs-id
bestätigt der letzte Test Folgendes:For a valid session id
, the final test confirms that:
- Die Methode gibt ein
ActionResult
mit einemBrainstormSession
-Typ zurück.The method returns anActionResult
with aBrainstormSession
type. - Das Aktions Ergebnis <T> . Das Ergebnis ist ein CreatedAtActionResult .The ActionResult<T>.Result is a CreatedAtActionResult.
CreatedAtActionResult
ist analog zu einer 201 Created-Antwort mit einemLocation
-Header.CreatedAtActionResult
is analogous to a 201 Created response with aLocation
header. - Das Aktions Ergebnis <T> . Der Wert ist ein-
BrainstormSession
Typ.The ActionResult<T>.Value is aBrainstormSession
type. - Der Pseudoaufruf zum Aktualisieren der Sitzung (
UpdateAsync(testSession)
) wurde aufgerufen.The mock call to update the session,UpdateAsync(testSession)
, was invoked. DerVerifiable
-Methodenaufruf wird überprüft, indemmockRepo.Verify()
in den Assertionen ausgeführt wird.TheVerifiable
method call is checked by executingmockRepo.Verify()
in the assertions. - Zwei
Idea
-Objekte werden für die Sitzung zurückgegeben.TwoIdea
objects are returned for the session. - Das letzte Element (die
Idea
, die durch den PseudoaufrufUpdateAsync
hinzugefügt wurde) stimmt mit dernewIdea
überein, die der Sitzung im Test hinzugefügt wurde.The last item (theIdea
added by the mock call toUpdateAsync
) matches thenewIdea
added to the session in the test.
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
Controller spielen in jeder ASP.NET Core MVC-App eine zentrale Rolle.Controllers play a central role in any ASP.NET Core MVC app. Daher sollten Sie sich auch darauf verlassen können, dass Controller in Ihrer App wie beabsichtigt funktionieren.As such, you should have confidence that controllers behave as intended. Automatisierte Tests können Fehler erkennen, bevor die App in einer Produktionsumgebung bereitgestellt wird.Automated tests can detect errors before the app is deployed to a production environment.
Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)View or download sample code (how to download)
Komponententests der ControllerlogikUnit tests of controller logic
Komponententests beinhalten das Testen einer App-Komponente isoliert von ihrer Infrastruktur und ihren Abhängigkeiten.Unit tests involve testing a part of an app in isolation from its infrastructure and dependencies. Bei einem Komponententest der Controllerlogik werden nur die Inhalte einer einzelnen Aktion getestet, nicht das Verhalten ihrer Abhängigkeiten oder des Frameworks selbst.When unit testing controller logic, only the contents of a single action are tested, not the behavior of its dependencies or of the framework itself.
Richten Sie Komponententests für Controlleraktionen ein, um sich auf das Verhalten des Controllers zu konzentrieren.Set up unit tests of controller actions to focus on the controller's behavior. Bei einem Komponententest des Controllers werden Szenarien wie Filter, Routing und Modellbindung vermieden.A controller unit test avoids scenarios such as filters, routing, and model binding. Tests, die die Interaktionen zwischen Komponenten betreffen, die gemeinsam auf eine Anforderung reagieren, werden mithilfe von Integrationstests behandelt.Tests that cover the interactions among components that collectively respond to a request are handled by integration tests. Weitere Informationen zu Integrationstests finden Sie unter Integrationstests in ASP.NET Core.For more information on integration tests, see Integrationstests in ASP.NET Core.
Wenn Sie benutzerdefinierte Filter und Routen schreiben, sollten Sie für diese isoliert einen Komponententest durchführen, der nicht Bestandteil eines Tests für eine bestimmte Controlleraktion ist.If you're writing custom filters and routes, unit test them in isolation, not as part of tests on a particular controller action.
Um mehr über Komponententests für Controller zu erfahren, sehen Sie sich den folgenden Controller in der Beispiel-App an.To demonstrate controller unit tests, review the following controller in the sample app. Der Homecontroller zeigt eine Liste von Brainstormingsitzungen an und ermöglicht das Erstellen neuer Brainstormingsitzungen mit einer POST-Anforderung:The Home controller displays a list of brainstorming sessions and allows the creation of new brainstorming sessions with a POST request:
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));
}
}
Für den oben aufgeführten Controller gilt Folgendes:The preceding controller:
- Er folgt dem Prinzip der expliziten Abhängigkeiten.Follows the Explicit Dependencies Principle.
- Er erwartet, dass Abhängigkeitsinjektion (Dependency Injection, DI) eine Instanz von
IBrainstormSessionRepository
bereitstellt.Expects dependency injection (DI) to provide an instance ofIBrainstormSessionRepository
. - Er kann mit einem simulierten
IBrainstormSessionRepository
-Dienst mithilfe eines Pseudoobjektframeworks wie Moq getestet werden.Can be tested with a mockedIBrainstormSessionRepository
service using a mock object framework, such as Moq. Ein Pseudoobjekt ist ein künstliches Objekt mit einem vordefiniertem Satz von Eigenschaften und Methodenverhalten, das zum Testen verwendet wird.A mocked object is a fabricated object with a predetermined set of property and method behaviors used for testing. Weitere Informationen finden Sie unter Einführung in Integrationstests.For more information, see Introduction to integration tests.
Die HTTP GET Index
-Methode verfügt weder über Schleifen noch über Verzweigungen und ruft nur eine Methode auf.The HTTP GET Index
method has no looping or branching and only calls one method. Der Komponententest für diese Aktion:The unit test for this action:
- Simuliert den
IBrainstormSessionRepository
-Dienst mit derGetTestSessions
-Methode.Mocks theIBrainstormSessionRepository
service using theGetTestSessions
method.GetTestSessions
erstellt zwei simulierte Brainstormingsitzungen mit Datumsangaben und Sitzungsnamen.GetTestSessions
creates two mock brainstorm sessions with dates and session names. - Führt die
Index
-Methode aus.Executes theIndex
method. - Nimmt Assertionen für das von der Methode zurückgegebene Ergebnis vor:Makes assertions on the result returned by the method:
- Ein ViewResult wird zurückgegeben.A ViewResult is returned.
- Das ViewDataDictionary.Model ist ein
StormSessionViewModel
.The ViewDataDictionary.Model is aStormSessionViewModel
. - Es werden zwei Brainstormingsitzungen in
ViewDataDictionary.Model
gespeichert.There are two brainstorming sessions stored in theViewDataDictionary.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;
}
Die HTTP POST Index
-Methodentests des Homecontrollers bestätigen dies:The Home controller's HTTP POST Index
method tests verifies that:
- Wenn ModelState.IsValid
false
ist, gibt die Aktionsmethode den Status 400 Bad Request zurück ( ViewResult mit den entsprechenden Daten).When ModelState.IsValid isfalse
, the action method returns a 400 Bad Request ViewResult with the appropriate data. - Wenn
ModelState.IsValid``true
ist:WhenModelState.IsValid
istrue
:- Die
Add
-Methode für das Repository wird aufgerufen.TheAdd
method on the repository is called. - Ein RedirectToActionResult wird mit den richtigen Argumenten zurückgegeben.A RedirectToActionResult is returned with the correct arguments.
- Die
Ein ungültiger Modellstatus wird getestet, indem mithilfe von AddModelError (wie im ersten Test unten gezeigt) Fehler hinzugefügt werden:An invalid model state is tested by adding errors using AddModelError as shown in the first test below:
[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();
}
Wenn ModelState nicht gültig ist, wird das gleiche ViewResult
wie für eine GET-Anforderung zurückgegeben.When ModelState isn't valid, the same ViewResult
is returned as for a GET request. Der Test versucht nicht, ein ungültiges Modell zu übergeben.The test doesn't attempt to pass in an invalid model. Das Übergeben eines ungültigen Modells ist kein gültiger Ansatz, da die Modellbindung nicht ausgeführt wird (obwohl ein Integrationstest Modellbindung verwendet).Passing an invalid model isn't a valid approach, since model binding isn't running (although an integration test does use model binding). In diesem Fall wird die Modellbindung nicht getestet.In this case, model binding isn't tested. Bei diesen Komponententests wird nur der Code in der Aktionsmethode getestet.These unit tests are only testing the code in the action method.
Der zweite Test bestätigt dies, wenn der ModelState
gültig ist:The second test verifies that when the ModelState
is valid:
- Eine neue
BrainstormSession
wird hinzugefügt (über das Repository).A newBrainstormSession
is added (via the repository). - Die Methode gibt ein
RedirectToActionResult
mit den erwarteten Eigenschaften zurück.The method returns aRedirectToActionResult
with the expected properties.
Simulierte Aufrufe, die nicht aufgerufen werden, werden normalerweise ignoriert. Durch Aufrufen von Verifiable
am Ende des Einrichtungsaufrufs wird jedoch eine Pseudoüberprüfung im Test ermöglicht.Mocked calls that aren't called are normally ignored, but calling Verifiable
at the end of the setup call allows mock validation in the test. Dies erfolgt mit dem Aufruf von mockRepo.Verify
. Damit schlägt der Test fehl, wenn die erwartete Methode nicht aufgerufen wurde.This is performed with the call to mockRepo.Verify
, which fails the test if the expected method wasn't called.
Hinweis
Mit der in diesem Beispiel verwendeten Moq-Bibliothek können überprüfbare (oder „strikte“) Pseudoobjekte mit nicht überprüfbaren Pseudoobjekten (auch „nicht-strikte“ Pseudoobjekte oder „Stubs“ genannt) kombiniert werden.The Moq library used in this sample makes it possible to mix verifiable, or "strict", mocks with non-verifiable mocks (also called "loose" mocks or stubs). Weitere Informationen finden Sie unter Customizing Mock behavior with Moq (Anpassen des Verhaltens von Pseudoobjekten mit Moq).Learn more about customizing Mock behavior with Moq.
In der Beispiel-App werden Informationen zu einer bestimmten Brainstormingsitzung mit SessionController angezeigt.SessionController in the sample app displays information related to a particular brainstorming session. Der Controller enthält Logik, um ungültige id
Werte zu behandeln (es gibt im folgenden Beispiel zwei return
-Szenarien, die diese Fälle abdecken).The controller includes logic to deal with invalid id
values (there are two return
scenarios in the following example to cover these scenarios). Die endgültige return
-Anweisung gibt ein neues StormSessionViewModel
-Objekt an die Ansicht zurück (Controllers/SessionController.cs):The final return
statement returns a new StormSessionViewModel
to the view (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);
}
}
Die Komponententests enthalten einen Test für jedes return
-Szenario in der Index
-Aktion des Sitzungscontrollers:The unit tests include one test for each return
scenario in the Session controller Index
action:
[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);
}
Mit dem Wechsel zum Ideas-Controller stellt die App Funktionalität als Web-API auf der api/ideas
-Route zur Verfügung:Moving to the Ideas controller, the app exposes functionality as a web API on the api/ideas
route:
- Eine Liste von Ideen (
IdeaDTO
), die einer Brainstormingsitzung zugeordnet sind, wird von derForSession
-Methode zurückgegeben.A list of ideas (IdeaDTO
) associated with a brainstorming session is returned by theForSession
method. - Die
Create
-Methode fügt einer Sitzung neue Ideen hinzu.TheCreate
method adds new ideas to a session.
[HttpGet("forsession/{sessionId}")]
public async Task<IActionResult> ForSession(int sessionId)
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId);
}
var result = session.Ideas.Select(idea => new IdeaDTO()
{
Id = idea.Id,
Name = idea.Name,
Description = idea.Description,
DateCreated = idea.DateCreated
}).ToList();
return Ok(result);
}
[HttpPost("create")]
public async Task<IActionResult> Create([FromBody]NewIdeaModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId);
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return Ok(session);
}
Vermeiden Sie die Rückgabe von Geschäftsdomänenentitäten direkt über API-Aufrufe.Avoid returning business domain entities directly via API calls. Für Domänenentitäten gilt Folgendes:Domain entities:
- Sie enthalten häufig mehr Daten als für den Client erforderlich.Often include more data than the client requires.
- Sie koppeln unnötigerweise das interne Domänenmodell der App mit der öffentlich bereitgestellten API.Unnecessarily couple the app's internal domain model with the publicly exposed API.
Die Zuordnung zwischen Domänenentitäten und den Typen, die an den Client zurückgegeben werden, kann ausgeführt werden:Mapping between domain entities and the types returned to the client can be performed:
- Manuell mit einer LINQ-
Select
-Anweisung, wie sie von der Beispiel-App verwendet wird.Manually with a LINQSelect
, as the sample app uses. Weitere Informationen finden Sie unter LINQ (Language Integrated Query).For more information, see LINQ (Language Integrated Query). - Automatisch mit einer Bibliothek, z.B. mit AutoMapper.Automatically with a library, such as AutoMapper.
Anschließend demonstriert die Beispiel-App Komponententests für die API-Methoden Create
und ForSession
des Ideas-Controllers.Next, the sample app demonstrates unit tests for the Create
and ForSession
API methods of the Ideas controller.
Die Beispiel-App enthält zwei ForSession
-Tests.The sample app contains two ForSession
tests. Der erste Test ermittelt, ob ForSession
ein NotFoundObjectResult (HTTP Not Found) für eine ungültige Sitzung zurückgibt:The first test determines if ForSession
returns a NotFoundObjectResult (HTTP Not Found) for an invalid session:
[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);
}
Die zweite ForSession
-Test ermittelt, ob ForSession
eine Liste der Sitzungsideen (<List<IdeaDTO>>
) für eine gültige Sitzung zurückgibt.The second ForSession
test determines if ForSession
returns a list of session ideas (<List<IdeaDTO>>
) for a valid session. Die Tests untersuchen auch die erste Idee, um zu bestätigen, dass ihre Name
-Eigenschaft richtig ist:The checks also examine the first idea to confirm its Name
property is correct:
[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);
}
Um das Verhalten der Create
-Methode zu testen, wenn der ModelState
ungültig ist, fügt die Beispiel-App dem Controller einen Modellfehler als Teil des Tests hinzu.To test the behavior of the Create
method when the ModelState
is invalid, the sample app adds a model error to the controller as part of the test. Versuchen Sie nicht, die Modellüberprüfung oder Modellbindung in Komponententests zu testen. Testen Sie einfach das Verhalten der Aktionsmethode, wenn Sie mit einem ungültigen ModelState
konfrontiert wird:Don't try to test model validation or model binding in unit tests—just test the action method's behavior when confronted with an invalid 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);
}
Der zweite Test von Create
hängt davon ab, ob das Repository null
zurückgibt. Daher wird das Pseudorepository so konfiguriert, dass es null
zurückgibt.The second test of Create
depends on the repository returning null
, so the mock repository is configured to return null
. Es ist nicht erforderlich, eine Testdatenbank (im Arbeitsspeicher oder anderweitig) zu erstellen und eine Abfrage zu generieren, die dieses Ergebnis zurückgibt.There's no need to create a test database (in memory or otherwise) and construct a query that returns this result. Der Test kann mit einer einzigen Anweisung ausgeführt werden, wie der Beispielcode veranschaulicht:The test can be accomplished in a single statement, as the sample code illustrates:
[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);
}
Im dritten Create
-Test (Create_ReturnsNewlyCreatedIdeaForSession
) wird überprüft, ob die UpdateAsync
-Methode des Repositorys aufgerufen wird.The third Create
test, Create_ReturnsNewlyCreatedIdeaForSession
, verifies that the repository's UpdateAsync
method is called. Das Pseudoobjekt wird mit Verifiable
aufgerufen. Anschließend wird die Verify
-Methode des Pseudorepositorys aufgerufen, um zu bestätigen, dass die überprüfbare Methode ausgeführt wurde.The mock is called with Verifiable
, and the mocked repository's Verify
method is called to confirm the verifiable method is executed. Sicherzustellen, dass die Daten von der UpdateAsync
-Methode gespeichert wurden, gehört nicht zu den Aufgaben des Komponententests. Dies kann mit einem Integrationstest bestätigt werden.It's not the unit test's responsibility to ensure that the UpdateAsync
method saved the data—that can be performed with an integration test.
[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-Aktions Ergebnis<T>Test ActionResult<T>
In ASP.net Core 2,1 oder höher können Sie mit " aktionresult <T> ( ActionResult<TValue> )" einen Typ zurückgeben, der von abgeleitet wird, ActionResult
oder einen bestimmten Typ zurückgeben.In ASP.NET Core 2.1 or later, ActionResult<T> (ActionResult<TValue>) enables you to return a type deriving from ActionResult
or return a specific type.
Die Beispielanwendung enthält eine Methode, die ein List<IdeaDTO>
für eine bestimmte Sitzung id
zurückgibt.The sample app includes a method that returns a List<IdeaDTO>
for a given session id
. Wenn die Sitzung id
nicht vorhanden ist, gibt der Controller NotFound zurück:If the session id
doesn't exist, the controller returns 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;
}
Zwei Tests des ForSessionActionResult
-Controllers sind in ApiIdeasControllerTests
vorhanden.Two tests of the ForSessionActionResult
controller are included in the ApiIdeasControllerTests
.
Der erste Test bestätigt, dass der Controller ein ActionResult
, aber keine nicht vorhandene Liste von Ideen für eine nicht vorhandene Sitzungs-id
zurückgibt:The first test confirms that the controller returns an ActionResult
but not a nonexistent list of ideas for a nonexistent session id
:
- Der Typ von
ActionResult
istActionResult<List<IdeaDTO>>
.TheActionResult
type isActionResult<List<IdeaDTO>>
. - Result ist ein NotFoundObjectResult.The Result is a 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);
}
Für eine gültige Sitzungs-id
bestätigt der zweite Test, dass die Methode Folgendes zurückgibt:For a valid session id
, the second test confirms that the method returns:
- Ein
ActionResult
mit einemList<IdeaDTO>
-Typ.AnActionResult
with aList<IdeaDTO>
type. - Das Aktions Ergebnis <T> . Der Wert ist ein-
List<IdeaDTO>
Typ.The ActionResult<T>.Value is aList<IdeaDTO>
type. - Das erste Element in der Liste ist eine gültige Idee, die der in der Pseudositzung gespeicherten Idee entspricht (abgerufen durch den Aufruf von
GetTestSession
).The first item in the list is a valid idea matching the idea stored in the mock session (obtained by callingGetTestSession
).
[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);
}
Die Beispiel-App enthält auch eine Methode zum Erstellen einer neuen Idea
für eine bestimmte Sitzung.The sample app also includes a method to create a new Idea
for a given session. Der Controller gibt Folgendes zurück:The controller returns:
- BadRequest für ein ungültiges Modell.BadRequest for an invalid model.
- NotFound, wenn die Sitzung nicht vorhanden ist.NotFound if the session doesn't exist.
- CreatedAtAction, wenn die Sitzung mit der neuen Idee aktualisiert wird.CreatedAtAction when the session is updated with the new idea.
[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);
}
Drei Tests von CreateActionResult
sind in ApiIdeasControllerTests
enthalten.Three tests of CreateActionResult
are included in the ApiIdeasControllerTests
.
Der erste Test bestätigt, dass eine BadRequest für ein ungültiges Modell zurückgegeben wird.The first text confirms that a BadRequest is returned for an invalid 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);
}
Der zweite Test überprüft, ob ein NotFound-Element zurückgegeben wird, wenn die Sitzung nicht vorhanden ist.The second test checks that a NotFound is returned if the session doesn't exist.
[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);
}
Für eine gültige Sitzungs-id
bestätigt der letzte Test Folgendes:For a valid session id
, the final test confirms that:
- Die Methode gibt ein
ActionResult
mit einemBrainstormSession
-Typ zurück.The method returns anActionResult
with aBrainstormSession
type. - Das Aktions Ergebnis <T> . Das Ergebnis ist ein CreatedAtActionResult .The ActionResult<T>.Result is a CreatedAtActionResult.
CreatedAtActionResult
ist analog zu einer 201 Created-Antwort mit einemLocation
-Header.CreatedAtActionResult
is analogous to a 201 Created response with aLocation
header. - Das Aktions Ergebnis <T> . Der Wert ist ein-
BrainstormSession
Typ.The ActionResult<T>.Value is aBrainstormSession
type. - Der Pseudoaufruf zum Aktualisieren der Sitzung (
UpdateAsync(testSession)
) wurde aufgerufen.The mock call to update the session,UpdateAsync(testSession)
, was invoked. DerVerifiable
-Methodenaufruf wird überprüft, indemmockRepo.Verify()
in den Assertionen ausgeführt wird.TheVerifiable
method call is checked by executingmockRepo.Verify()
in the assertions. - Zwei
Idea
-Objekte werden für die Sitzung zurückgegeben.TwoIdea
objects are returned for the session. - Das letzte Element (die
Idea
, die durch den PseudoaufrufUpdateAsync
hinzugefügt wurde) stimmt mit dernewIdea
überein, die der Sitzung im Test hinzugefügt wurde.The last item (theIdea
added by the mock call toUpdateAsync
) matches thenewIdea
added to the session in the test.
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
// Arrange
int testSessionId = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.ReturnsAsync(testSession);
var controller = new IdeasController(mockRepo.Object);
var newIdea = new NewIdeaModel()
{
Description = testDescription,
Name = testName,
SessionId = testSessionId
};
mockRepo.Setup(repo => repo.UpdateAsync(testSession))
.Returns(Task.CompletedTask)
.Verifiable();
// Act
var result = await controller.CreateActionResult(newIdea);
// Assert
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
mockRepo.Verify();
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}
Zusätzliche RessourcenAdditional resources
- Integrationstests in ASP.NET Core
- Erstellen und Ausführen von Komponententests mit Visual StudioCreate and run unit tests with Visual Studio
- Mytesting. aspnetcore. MVC-fließende Test Bibliothek für ASP.net Core MVC: stark typisierte unittestbibliothek, die eine fließende Oberfläche zum Testen von MVC-und Web-API-apps bereitstellt.MyTested.AspNetCore.Mvc - Fluent Testing Library for ASP.NET Core MVC: Strongly-typed unit testing library, providing a fluent interface for testing MVC and web API apps. (Wird von Microsoft nicht verwaltet oder unterstützt. )(Not maintained or supported by Microsoft.)
- JustMockLite: Dies ist ein Beispielframework für .NET-Entwickler.JustMockLite: A mocking framework for .NET developers. (Wird von Microsoft nicht verwaltet oder unterstützt. )(Not maintained or supported by Microsoft.)