Unit-Tests für die Controllerlogik in ASP.NET Core

Von Steve Smith

Unit-Tests beinhalten das Testen einer App-Komponente isoliert von ihrer Infrastruktur und ihren Abhängigkeiten. Beim Unit-Testen der Controllerlogik werden nur die Inhalte einer einzelnen Aktion getestet, nicht das Verhalten ihrer Abhängigkeiten oder des Frameworks selbst.

Unit-Tests für Controller

Richten Sie Komponententests für Controlleraktionen ein, um sich auf das Verhalten des Controllers zu konzentrieren. Bei einem Unit-Test eines Controllers werden Szenarien wie Filter, Routing und Modellbindung vermieden. Tests, die die Interaktionen zwischen Komponenten betreffen, die gemeinsam auf eine Anforderung reagieren, werden mithilfe von Integrationstests behandelt. Weitere Informationen zu Integrationstests finden Sie unter Integrationstests in ASP.NET Core.

Wenn Sie benutzerdefinierte Filter und Routen schreiben, sollten Sie diese isoliert mit einem Unit-Test testen, nicht als Teil von Tests für eine bestimmte Controlleraktion.

Zur Veranschaulichung von Unit-Tests für Controller sehen Sie sich den folgenden Controller in der Beispiel-App an.

Beispielcode anzeigen oder herunterladen (Vorgehensweise zum Herunterladen)

Der Home-Controller zeigt eine Liste von Brainstormingsitzungen an und ermöglicht das Erstellen neuer Brainstormingsitzungen mit einer POST-Anforderung:

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));
    }
}

Der vorangehende Controller:

Die HTTP GET Index-Methode enthält weder Schleifen noch Verzweigungen und ruft nur eine Methode auf. Der Komponententest für diese Aktion:

  • Simuliert den IBrainstormSessionRepository-Dienst mit der GetTestSessions-Methode. GetTestSessions erstellt zwei simulierte Brainstormingsitzungen mit Datumsangaben und Sitzungsnamen.
  • Führt die Index-Methode aus.
  • Nimmt Assertionen für das von der Methode zurückgegebene Ergebnis vor:
    • Ein ViewResult wird zurückgegeben.
    • Das ViewDataDictionary.Model ist ein StormSessionViewModel.
    • Es sind zwei Brainstorming-Sitzungen in ViewDataDictionary.Model gespeichert.
[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-Methode des Home-Controllers überprüft Folgendes:

  • Wenn ModelState.IsValid den Wert false hat, gibt die Aktionsmethode ein 400 Bad RequestViewResult mit den entsprechenden Daten zurück.
  • Wenn ModelState.IsValidtrue ist:
    • Die Add-Methode des Repositorys wird aufgerufen.
    • Ein RedirectToActionResult wird mit den richtigen Argumenten zurückgegeben.

Ein ungültiger Modellzustand wird getestet, indem mit AddModelError Fehler hinzugefügt werden, wie im ersten Test unten gezeigt:

[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. Der Test versucht nicht, ein ungültiges Modell zu übergeben. Das Übergeben eines ungültigen Modells ist kein gültiger Ansatz, da die Modellbindung nicht ausgeführt wird (obwohl ein Integrationstest Modellbindung verwendet). In diesem Fall wird die Modellbindung nicht getestet. Diese Unit-Tests testen nur den Code in der Action-Methode.

Der zweite Test bestätigt dies, wenn der ModelState gültig ist:

  • Eine neue BrainstormSession wird hinzugefügt (über das Repository).
  • Die Methode gibt ein RedirectToActionResult mit den erwarteten Eigenschaften zurück.

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. Dies erfolgt mit dem Aufruf von mockRepo.Verify. Damit schlägt der Test fehl, wenn die erwartete Methode nicht aufgerufen wurde.

Note

Mit der in diesem Beispiel verwendeten Moq-Bibliothek können überprüfbare (oder „strikte“) Pseudoobjekte mit nicht überprüfbaren Pseudoobjekten (auch „Loose-Mocks“ oder „Stubs“ genannt) kombiniert werden. Erfahren Sie mehr über das Anpassen des Mock-Verhaltens mit Moq.

In der Beispiel-App werden Informationen zu einer bestimmten Brainstormingsitzung mit SessionController angezeigt. 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). Die endgültige return-Anweisung gibt ein neues StormSessionViewModel an die Ansicht zurück (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 Unit-Tests enthalten einen Test für jedes return-Szenario in der Aktion Index des Session-Controllers:

[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:

  • Eine Liste von Ideen (IdeaDTO), die einer Brainstormingsitzung zugeordnet sind, wird von der ForSession-Methode zurückgegeben.
  • Die Create-Methode fügt einer Sitzung neue Ideen hinzu.
[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. Domänenentitäten:

  • Sie enthalten häufig mehr Daten als der Client benötigt.
  • Sie koppeln unnötigerweise das interne Domänenmodell der App mit der öffentlich bereitgestellten API.

Die Zuordnung zwischen Domänenentitäten und den Typen, die an den Client zurückgegeben werden, kann ausgeführt werden:

Anschließend demonstriert die Beispiel-App Unit-Tests für die API-Methoden Create und ForSession des Ideas-Controllers.

Die Beispiel-App enthält zwei ForSession-Tests. Der erste Test ermittelt, ob ForSession ein NotFoundObjectResult (HTTP Not Found) für eine ungültige Sitzung zurückgibt:

[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);
}

Der zweite ForSession-Test ermittelt, ob ForSession eine Liste der Sitzungsideen (<List<IdeaDTO>>) für eine gültige Sitzung zurückgibt. Die Tests untersuchen auch die erste Idee, um zu bestätigen, dass ihre Name-Eigenschaft richtig ist:

[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. Versuchen Sie nicht, Modellvalidierung oder Modellbindung in Unit-Tests zu testen – testen Sie einfach das Verhalten der Aktionsmethode, wenn sie mit ungültigem ModelState konfrontiert wird:

[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. Es ist nicht nötig, eine Testdatenbank (im Arbeitsspeicher oder anderweitig) zu erstellen und eine Abfrage zu erstellen, um dieses Ergebnis zurückzugeben. Der Test kann mit einer einzigen Anweisung ausgeführt werden, wie der Beispielcode veranschaulicht:

[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. Der Mock wird mit Verifiable aufgerufen, und die Verify-Methode des gemockten Repositorys wird aufgerufen, um zu bestätigen, dass die als verifizierbar markierte Methode ausgeführt wird. Es ist nicht die Aufgabe des Unit-Tests, sicherzustellen, dass die UpdateAsync-Methode die Daten gespeichert hat – das kann mit einem Integrationstest überprüft werden.

[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.Create(newIdea);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnSession.Ideas.Count());
    Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}

Test ActionResult<T>

ActionResult<T> (ActionResult<TValue>) kann einen von ActionResult abgeleiteten Typ oder einen spezifischen Typ zurückgeben.

Die Beispielanwendung enthält eine Methode, die ein List<IdeaDTO> für eine bestimmte Sitzung id zurückgibt. Wenn die Sitzung id nicht vorhanden ist, gibt der Controller NotFound zurück:

[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 enthalten.

Der erste Test bestätigt, dass der Controller ein ActionResult, aber keine nicht vorhandene Liste von Ideen für eine nicht vorhandene Sitzung id zurückgibt:

[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:

  • Ein ActionResult mit einem List<IdeaDTO>-Typ.
  • ActionResult<T>.Value ist ein List<IdeaDTO>-Typ.
  • 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).
[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. Der Controller gibt Folgendes zurück:

[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.

Der erste Text bestätigt, dass eine BadRequest für ein ungültiges Modell zurückgegeben wird.

[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 zurückgegeben wird, wenn die Sitzung nicht vorhanden ist.

[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:

  • Die Methode gibt ein ActionResult mit einem BrainstormSession-Typ zurück.
  • ActionResult<T>.Result ist ein CreatedAtActionResult. CreatedAtActionResult ist analog zu einer 201 Created-Antwort mit einem Location-Header.
  • ActionResult<T>.Value ist ein BrainstormSession-Typ.
  • Der Pseudoaufruf zum Aktualisieren der Sitzung (UpdateAsync(testSession)) wurde aufgerufen. Der Verifiable-Methodenaufruf wird überprüft, indem mockRepo.Verify() in den Assertionen ausgeführt wird.
  • Zwei Idea-Objekte werden für die Sitzung zurückgegeben.
  • 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.
[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. Daher sollten Sie sich auch darauf verlassen können, dass Controller wie beabsichtigt funktionieren. Automatisierte Tests können Fehler erkennen, bevor die App in einer Produktionsumgebung bereitgestellt wird.

Beispielcode anzeigen oder herunterladen (Vorgehensweise zum Herunterladen)

Unit-Tests der Controllerlogik

Unit-Tests beinhalten das Testen einer App-Komponente isoliert von ihrer Infrastruktur und ihren Abhängigkeiten. Bei einem Unit-Test der Controllerlogik werden nur die Inhalte einer einzelnen Aktion getestet, nicht das Verhalten ihrer Abhängigkeiten oder des Frameworks selbst.

Richten Sie Unit-Tests für Controlleraktionen ein, um sich auf das Verhalten des Controllers zu konzentrieren. Bei einem Komponententest des Controllers werden Szenarien wie Filter, Routing und Modellbindung vermieden. Tests, die die Interaktionen zwischen Komponenten betreffen, die gemeinsam auf eine Anforderung reagieren, werden durch Integrationstests abgedeckt. Weitere Informationen zu Integrationstests finden Sie unter Integrationstests in ASP.NET Core.

Wenn Sie benutzerdefinierte Filter und Routen schreiben, sollten Sie für diese isoliert einen Unit-Test durchführen, der nicht Bestandteil eines Tests für eine bestimmte Controlleraktion ist.

Um Unit-Tests für Controller zu demonstrieren, sehen Sie sich den folgenden Controller in der Beispiel-App an. Der Home-Controller zeigt eine Liste von Brainstormingsitzungen an und ermöglicht das Erstellen neuer Brainstormingsitzungen mit einer POST-Anforderung:

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:

Die HTTP GET Index-Methode enthält weder Schleifen noch Verzweigungen und ruft nur eine Methode auf. Der Komponententest für diese Aktion:

  • Simuliert den IBrainstormSessionRepository-Dienst mit der GetTestSessions-Methode. GetTestSessions erstellt zwei simulierte Brainstormingsitzungen mit Datumsangaben und Sitzungsnamen.
  • Führt die Index-Methode aus.
  • Nimmt Assertionen für das von der Methode zurückgegebene Ergebnis vor:
    • Ein ViewResult wird zurückgegeben.
    • Das ViewDataDictionary.Model ist ein StormSessionViewModel.
    • Es sind zwei Brainstormingsitzungen in ViewDataDictionary.Model gespeichert.
[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 Tests der HTTP POST Index-Methode des Home-Controllers überprüfen, dass:

  • Wenn ModelState.IsValidfalse ist, gibt die Aktionsmethode den Status 400 Bad Request zurück (ViewResult mit den entsprechenden Daten).
  • Wenn ModelState.IsValidtrue ist:
    • Die Add-Methode für das Repository wird aufgerufen.
    • Ein RedirectToActionResult wird mit den richtigen Argumenten zurückgegeben.

Ein ungültiger Modellstatus wird getestet, indem mithilfe von AddModelError (wie im ersten Test unten gezeigt) Fehler hinzugefügt werden:

[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. Der Test versucht nicht, ein ungültiges Modell zu übergeben. Das Übergeben eines ungültigen Modells ist kein gültiger Ansatz, da die Modellbindung nicht ausgeführt wird (obwohl ein Integrationstest Modellbindung verwendet). In diesem Fall wird die Modellbindung nicht getestet. Diese Unit-Tests testen nur den Code in der Action-Methode.

Der zweite Test überprüft, dass der ModelState gültig ist:

  • BrainstormSession wird neu hinzugefügt (über das Repository).
  • Die Methode gibt ein RedirectToActionResult mit den erwarteten Eigenschaften zurück.

Simulierte Aufrufe, die nicht aufgerufen werden, werden normalerweise ignoriert. Durch Aufrufen von Verifiable am Ende des Einrichtungsaufrufs wird jedoch eine Mock-Validierung im Test ermöglicht. Dies erfolgt mit dem Aufruf von mockRepo.Verify. Damit schlägt der Test fehl, wenn die erwartete Methode nicht aufgerufen wurde.

Note

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. Erfahren Sie mehr über das Anpassen des Verhaltens von Mock-Objekten mit Moq.

SessionController in der Beispiel-App zeigt Informationen zu einer bestimmten Brainstorming-Sitzung an. Der Controller enthält Logik zum Umgang mit ungültigen id-Werten (im folgenden Beispiel gibt es zwei return-Szenarien, um diese Fälle abzudecken). Die endgültige return-Befehl gibt ein neues StormSessionViewModel an die Ansicht zurück (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 Unit-Tests enthalten einen Test für jedes return-Szenario in der Index-Aktion des Sitzungscontrollers:

[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:

  • Eine Liste von Ideen (IdeaDTO), die einer Brainstormingsitzung zugeordnet sind, wird durch die ForSession-Methode zurückgegeben.
  • Die Create-Methode fügt einer Sitzung neue Ideen hinzu.
[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. Domänenentitäten:

  • Enthalten häufig mehr Daten, als der Client benötigt.
  • Das interne Domänenmodell der App unnötigerweise mit der öffentlich bereitgestellten API koppeln.

Die Zuordnung zwischen Domänenentitäten und den an den Client zurückgegebenen Typen kann durchgeführt werden:

Anschließend demonstriert die Beispiel-App Unit-Tests für die API-Methoden Create und ForSession des Ideas-Controllers.

Die Beispiel-App enthält zwei ForSession-Tests. Der erste Test prüft, ob ForSession bei einer ungültigen Session ein NotFoundObjectResult (HTTP Not Found) zurückgibt:

[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);
}

Der zweite ForSession-Test ermittelt, ob ForSession eine Liste der Sitzungsideen (<List<IdeaDTO>>) für eine gültige Sitzung zurückgibt. Die Prüfungen untersuchen auch die erste Idee, um zu bestätigen, dass ihre Name-Eigenschaft richtig ist:

[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 ModelState ungültig ist, fügt die Beispiel-App dem Controller einen Modellfehler als Teil des Tests hinzu. Versuchen Sie nicht, die Modellvalidierung oder Modellbindung in Unit-Tests zu testen – testen Sie einfach das Verhalten der Aktionsmethode, wenn ihr ein ungültiges ModelState übergeben wird:

[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 in Create setzt voraus, dass das Repository null zurückgibt, daher wird das Mock-Repository so konfiguriert, dass es null zurückgibt. Es ist nicht erforderlich, eine Testdatenbank (im Arbeitsspeicher oder anderweitig) zu erstellen und eine Abfrage zu generieren, die dieses Ergebnis zurückgibt. Der Test kann mit einer einzigen Anweisung ausgeführt werden, wie der Beispielcode veranschaulicht:

[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. Der Mock wird mit Verifiable aufgerufen. Anschließend wird die Verify-Methode des gemockten Repositorys aufgerufen, um zu bestätigen, dass die überprüfbare Methode ausgeführt wird. Sicherzustellen, dass die Daten von der UpdateAsync-Methode gespeichert wurden, gehört nicht zu den Aufgaben des Unit-Tests. Dies kann mit einem Integrationstest bestätigt werden.

[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

    var newIdea = new NewIdeaModel()
    {
        Description = testDescription,
        Name = testName,
        SessionId = testSessionId
    };
    mockRepo.Setup(repo => repo.UpdateAsync(testSession))
        .Returns(Task.CompletedTask)
        .Verifiable();

    // Act
    var result = await controller.Create(newIdea);

    // Assert
    var okResult = Assert.IsType<OkObjectResult>(result);
    var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnSession.Ideas.Count());
    Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}

Test ActionResult<T>

In ASP.NET Core 2.1 oder höher ermöglicht es ActionResult<T> (ActionResult<TValue>) Ihnen, einen Typ, der von ActionResult abgeleitet ist, oder einen bestimmten Typ zurückzugeben.

Die Beispielanwendung enthält eine Methode, die ein List<IdeaDTO> für eine bestimmte Sitzung id zurückgibt. Wenn die Sitzung id nicht vorhanden ist, gibt der Controller NotFound zurück:

[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.

Der erste Test bestätigt, dass der Controller ein ActionResult zurückgibt, aber keine nicht existierende Liste von Ideen für eine nicht existierende Sitzung 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 Sitzung id bestätigt der zweite Test, dass die Methode Folgendes zurückgibt:

  • Ein ActionResult mit einem List<IdeaDTO>-Typ.
  • ActionResult<T>.Value ist vom Typ List<IdeaDTO>.
  • 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).
[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. Der Controller gibt Folgendes zurück:

[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 zu CreateActionResult sind in ApiIdeasControllerTests enthalten.

Der erste Test bestätigt, dass eine BadRequest für ein ungültiges Modell zurückgegeben wird.

[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 zurückgegeben wird, wenn die Sitzung nicht vorhanden ist.

[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 Sitzung id bestätigt der letzte Test Folgendes:

  • Die Methode gibt ein ActionResult mit einem BrainstormSession-Typ zurück.
  • ActionResult<T>.Result ist ein CreatedAtActionResult. CreatedAtActionResult entspricht einer 201 Created-Antwort mit einem Location-Header.
  • ActionResult<T>.Value ist ein BrainstormSession-Typ.
  • Der Mock-Aufruf zum Aktualisieren der Sitzung (UpdateAsync(testSession)) wurde aufgerufen. Der Verifiable-Methodenaufruf wird überprüft, indem mockRepo.Verify() in den Assertionen ausgeführt wird.
  • Zwei Idea-Objekte werden für die Sitzung zurückgegeben.
  • Das letzte Element (das Idea, das durch den Mock-Aufruf von UpdateAsync hinzugefügt wurde) entspricht dem newIdea, das im Test zur Sitzung hinzugefügt wurde.
[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);
}