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:

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 der GetTestSessions-Methode.Mocks the IBrainstormSessionRepository service using the GetTestSessions 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 the Index method.
  • Nimmt Assertionen für das von der Methode zurückgegebene Ergebnis vor:Makes assertions on the result returned by the method:
[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.IsValidfalse ist, gibt die Aktionsmethode den Status 400 Bad Request zurück ( ViewResult mit den entsprechenden Daten).When ModelState.IsValid is false, the action method returns a 400 Bad Request ViewResult with the appropriate data.
  • Wenn ModelState.IsValid``true ist:When ModelState.IsValid is true:
    • Die Add-Methode für das Repository wird aufgerufen.The Add method on the repository is called.
    • Ein RedirectToActionResult wird mit den richtigen Argumenten zurückgegeben.A RedirectToActionResult is returned with the correct arguments.

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 new BrainstormSession is added (via the repository).
  • Die Methode gibt ein RedirectToActionResult mit den erwarteten Eigenschaften zurück.The method returns a RedirectToActionResult 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 der ForSession-Methode zurückgegeben.A list of ideas (IdeaDTO) associated with a brainstorming session is returned by the ForSession method.
  • Die Create-Methode fügt einer Sitzung neue Ideen hinzu.The Create 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:

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:

[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 einem List<IdeaDTO>-Typ.An ActionResult with a List<IdeaDTO> type.
  • Das Aktions Ergebnis <T> . Der Wert ist ein- List<IdeaDTO> Typ.The ActionResult<T>.Value is a List<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 calling 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);
}

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:

[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 einem BrainstormSession-Typ zurück.The method returns an ActionResult with a BrainstormSession 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 einem Location-Header.CreatedAtActionResult is analogous to a 201 Created response with a Location header.
  • Das Aktions Ergebnis <T> . Der Wert ist ein- BrainstormSession Typ.The ActionResult<T>.Value is a BrainstormSession type.
  • Der Pseudoaufruf zum Aktualisieren der Sitzung (UpdateAsync(testSession)) wurde aufgerufen.The mock call to update the session, UpdateAsync(testSession), was invoked. Der Verifiable-Methodenaufruf wird überprüft, indem mockRepo.Verify() in den Assertionen ausgeführt wird.The Verifiable method call is checked by executing mockRepo.Verify() in the assertions.
  • Zwei Idea-Objekte werden für die Sitzung zurückgegeben.Two Idea objects are returned for the session.
  • Das letzte Element (die Idea, die durch den Pseudoaufruf UpdateAsync hinzugefügt wurde) stimmt mit der newIdea überein, die der Sitzung im Test hinzugefügt wurde.The last item (the Idea added by the mock call to UpdateAsync) matches the newIdea 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:

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 der GetTestSessions-Methode.Mocks the IBrainstormSessionRepository service using the GetTestSessions 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 the Index method.
  • Nimmt Assertionen für das von der Methode zurückgegebene Ergebnis vor:Makes assertions on the result returned by the method:
[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.IsValidfalse ist, gibt die Aktionsmethode den Status 400 Bad Request zurück ( ViewResult mit den entsprechenden Daten).When ModelState.IsValid is false, the action method returns a 400 Bad Request ViewResult with the appropriate data.
  • Wenn ModelState.IsValid``true ist:When ModelState.IsValid is true:
    • Die Add-Methode für das Repository wird aufgerufen.The Add method on the repository is called.
    • Ein RedirectToActionResult wird mit den richtigen Argumenten zurückgegeben.A RedirectToActionResult is returned with the correct arguments.

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 new BrainstormSession is added (via the repository).
  • Die Methode gibt ein RedirectToActionResult mit den erwarteten Eigenschaften zurück.The method returns a RedirectToActionResult 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 der ForSession-Methode zurückgegeben.A list of ideas (IdeaDTO) associated with a brainstorming session is returned by the ForSession method.
  • Die Create-Methode fügt einer Sitzung neue Ideen hinzu.The Create 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:

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:

[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 einem List<IdeaDTO>-Typ.An ActionResult with a List<IdeaDTO> type.
  • Das Aktions Ergebnis <T> . Der Wert ist ein- List<IdeaDTO> Typ.The ActionResult<T>.Value is a List<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 calling 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);
}

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:

[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 einem BrainstormSession-Typ zurück.The method returns an ActionResult with a BrainstormSession 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 einem Location-Header.CreatedAtActionResult is analogous to a 201 Created response with a Location header.
  • Das Aktions Ergebnis <T> . Der Wert ist ein- BrainstormSession Typ.The ActionResult<T>.Value is a BrainstormSession type.
  • Der Pseudoaufruf zum Aktualisieren der Sitzung (UpdateAsync(testSession)) wurde aufgerufen.The mock call to update the session, UpdateAsync(testSession), was invoked. Der Verifiable-Methodenaufruf wird überprüft, indem mockRepo.Verify() in den Assertionen ausgeführt wird.The Verifiable method call is checked by executing mockRepo.Verify() in the assertions.
  • Zwei Idea-Objekte werden für die Sitzung zurückgegeben.Two Idea objects are returned for the session.
  • Das letzte Element (die Idea, die durch den Pseudoaufruf UpdateAsync hinzugefügt wurde) stimmt mit der newIdea überein, die der Sitzung im Test hinzugefügt wurde.The last item (the Idea added by the mock call to UpdateAsync) matches the newIdea 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