Delen via


Unittestcontrollerlogica in ASP.NET Core

Door Steve Smith

Eenheidstests omvatten het testen van een deel van een app in isolatie van de infrastructuur en afhankelijkheden. Wanneer eenheidstestcontrollerlogica wordt getest, wordt alleen de inhoud van één actie getest, niet het gedrag van de afhankelijkheden of van het framework zelf.

Eenheidstestcontrollers

Stel eenheidstests van controlleracties in om te focussen op het gedrag van de controller. Een controllereenheidtest vermijdt scenario's zoals filters, routering en modelbinding. Tests die betrekking hebben op de interacties tussen onderdelen die gezamenlijk reageren op een aanvraag, worden verwerkt door integratietests. Zie Integratietests in ASP.NET Core voor meer informatie over integratietests.

Als u aangepaste filters en routes schrijft, test u deze afzonderlijk, niet als onderdeel van tests voor een bepaalde controlleractie.

Bekijk de volgende controller in de voorbeeld-app om tests van controllereenheden te demonstreren.

Voorbeeldcode bekijken of downloaden (hoe download je)

De Home controller geeft een lijst met brainstormsessies weer en maakt het mogelijk om nieuwe brainstormsessies te maken met een POST-aanvraag:

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

De voorgaande controller:

De HTTP GET Index methode heeft geen lus of vertakking en roept slechts één methode aan. De eenheidstest voor deze handeling:

  • Hiermee wordt de IBrainstormSessionRepository service gesimuleerd met behulp van de GetTestSessions methode. GetTestSessions maakt twee mock brainstormsessies met datums en sessienamen.
  • Hiermee wordt de Index methode uitgevoerd.
  • Doet beweringen over het resultaat dat door de methode wordt geretourneerd:
    • Er wordt een ViewResult geretourneerd.
    • Het ViewDataDictionary.Model is een StormSessionViewModel.
    • Er zijn twee brainstormsessies opgeslagen in de ViewDataDictionary.Model.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

    // Act
    var result = await controller.Index();

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
        viewResult.ViewData.Model);
    Assert.Equal(2, model.Count());
}
private List<BrainstormSession> GetTestSessions()
{
    var sessions = new List<BrainstormSession>();
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 2),
        Id = 1,
        Name = "Test One"
    });
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 1),
        Id = 2,
        Name = "Test Two"
    });
    return sessions;
}

De Home methode van de HTTP POST Index controller test en verifieert dat:

  • Wanneer ModelState.IsValid is false, retourneert de actiemethode een 400 Ongeldige aanvraagViewResult met de juiste gegevens.
  • Wanneer ModelState.IsValid is true:
    • De Add methode in de opslagplaats wordt aangeroepen.
    • A RedirectToActionResult wordt geretourneerd met de juiste argumenten.

Er wordt een ongeldige modelstatus getest door fouten toe te voegen met behulp van AddModelError, zoals weergegeven in de eerste test hieronder:

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

Als ModelState niet geldig is, wordt hetzelfde ViewResult geretourneerd als voor een GET-aanvraag. De test probeert geen ongeldig model door te geven. Het doorgeven van een ongeldig model is geen geldige benadering, omdat modelbinding niet wordt uitgevoerd (hoewel een integratietest wel modelbinding gebruikt). In dit geval wordt modelbinding niet getest. Deze eenheidstests testen alleen de code in de actiemethode.

De tweede test controleert of wanneer het ModelState geldig is:

  • Er wordt een nieuwe BrainstormSession toegevoegd (via de opslagplaats).
  • De methode retourneert een RedirectToActionResult met de verwachte eigenschappen.

Gesimuleerde aanroepen die niet worden aangeroepen, worden normaal gesproken genegeerd, maar het aanroepen Verifiable aan het einde van de installatieoproep maakt mockvalidatie in de test mogelijk. Dit wordt uitgevoerd met de aanroep naar mockRepo.Verify, waardoor de test mislukt als de verwachte methode niet is aangeroepen.

Note

De Moq-bibliotheek die in dit voorbeeld wordt gebruikt, maakt het mogelijk om verifieerbare of 'strikte' mocks te combineren met niet-verifieerbare mocks (ook wel 'losse' mocks of stubs genoemd). Meer informatie over het aanpassen van mockgedrag met Moq.

SessionController in de voorbeeld-app geeft informatie weer met betrekking tot een bepaalde brainstormsessie. De controller bevat logica voor het verwerken van ongeldige id waarden (er zijn twee return scenario's in het volgende voorbeeld om deze scenario's te behandelen). De laatste return instructie retourneert een nieuw StormSessionViewModel object voor de weergave (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);
    }
}

De eenheidstests bevatten één test voor elk return scenario in de sessiecontrolleractie Index :

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

Als u overstapt op de Ideeën-controller, wordt de functionaliteit van de app beschikbaar gesteld als een web-API op de api/ideas route:

  • Een lijst met ideeën (IdeaDTO) die zijn gekoppeld aan een brainstormsessie, wordt geretourneerd door de ForSession methode.
  • Met de Create methode worden nieuwe ideeën toegevoegd aan een sessie.
[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);
}

Vermijd het rechtstreeks retourneren van zakelijke domeinentiteiten via API-aanroepen. Domeinentiteiten:

  • Neem vaak meer gegevens op dan de client vereist.
  • Koppel het interne domeinmodel van de app onnodig aan de openbaar weergegeven API.

Mapping tussen domeinentiteiten en de typen die aan de client worden geretourneerd, kan worden uitgevoerd:

Vervolgens demonstreert de voorbeeld-app eenheidstests voor de Create en ForSession API-methoden van de Ideeën-controller.

De voorbeeld-app bevat twee ForSession tests. De eerste test bepaalt of ForSession een NotFoundObjectResult (HTTP Niet gevonden) retourneert voor een ongeldige sessie:

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

De tweede ForSession test bepaalt of ForSession een lijst met sessieideeën (<List<IdeaDTO>>) voor een geldige sessie wordt geretourneerd. De controles onderzoeken ook het eerste idee om te bevestigen dat de Name eigenschap juist is:

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

Als u het gedrag van de Create methode wilt testen wanneer de ModelState methode ongeldig is, voegt de voorbeeld-app een modelfout toe aan de controller als onderdeel van de test. Probeer geen modelvalidatie of modelbinding te testen in eenheidstests. Test alleen het gedrag van de actiemethode wanneer u wordt geconfronteerd met een ongeldige ModelStateactiemethode:

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

De tweede test van Create hangt af van de opslagplaats die null retourneert, dus de mock-opslagplaats is geconfigureerd om null terug te geven. U hoeft geen testdatabase (in het geheugen of anderszins) te maken en een query te maken die dit resultaat retourneert. De test kan in één instructie worden uitgevoerd, zoals in de voorbeeldcode wordt geïllustreerd:

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

De derde Create test controleert of de Create_ReturnsNewlyCreatedIdeaForSession methode van de repository wordt aangeroepen. De mock wordt aangeroepen met Verifiable, en de methode van de gesimuleerde opslagplaats Verify wordt aangeroepen om te bevestigen dat de verifieerbare methode wordt uitgevoerd. Het is niet de verantwoordelijkheid van de eenheidstest om ervoor te zorgen dat de UpdateAsync methode de gegevens heeft opgeslagen, die kan worden uitgevoerd met een integratietest.

[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>) kan een type retourneren dat is afgeleid van ActionResult of een specifiek type retourneert.

De voorbeeld-app bevat een methode die een List<IdeaDTO> voor een bepaalde sessie idretourneert. Als de sessie id niet bestaat, retourneert NotFoundde controller:

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

Twee tests van de ForSessionActionResult controller zijn opgenomen in de ApiIdeasControllerTests.

De eerste test bevestigt dat de controller een ActionResult retourneert, maar geen niet-bestaande lijst met ideeën voor een niet-bestaande sessie 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);
}

Voor een geldige sessie idbevestigt de tweede test dat de methode retourneert:

  • Een ActionResult met een List<IdeaDTO> type.
  • De ActionResult<T>.Waarde is van het type List<IdeaDTO>.
  • Het eerste item in de lijst is een geldig idee dat overeenkomt met het idee dat is opgeslagen in de mocksessie (verkregen door aan te roepen 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);
}

De voorbeeld-app bevat ook een methode voor het maken van een nieuwe Idea voor een bepaalde sessie. De controller geeft:

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

Er zijn drie tests CreateActionResult opgenomen in de ApiIdeasControllerTests.

In de eerste tekst wordt bevestigd dat er een BadRequest wordt geretourneerd voor een ongeldig 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);
}

De tweede test controleert of er een NotFound wordt geretourneerd als de sessie niet bestaat.

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

Voor een geldige sessie idbevestigt de laatste test dat:

  • De methode retourneert een ActionResult met een BrainstormSession type.
  • De ActionResult<T>.Result is een CreatedAtActionResult. CreatedAtActionResult is vergelijkbaar met een 201 Gemaakt antwoord met een Location header.
  • De ActionResult<T>.Waarde is van het type BrainstormSession.
  • De mock-aanroep om de sessie bij te werken, UpdateAsync(testSession), werd aangeroepen. De Verifiable methode-aanroep wordt gecontroleerd door de asserties uit te mockRepo.Verify() voeren.
  • Er worden twee Idea objecten geretourneerd voor de sessie.
  • Het laatste item (de Idea toegevoegde door de mock-aanroep) UpdateAsynckomt overeen met het newIdea item dat is toegevoegd aan de sessie in de 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);
}

Controllers spelen een centrale rol in elke ASP.NET Core MVC-app. Daarom moet u erop vertrouwen dat controllers zich gedragen zoals bedoeld. Met geautomatiseerde tests kunnen fouten worden gedetecteerd voordat de app in een productieomgeving wordt geïmplementeerd.

Voorbeeldcode bekijken of downloaden (hoe download je)

Eenheidstests van controllerlogica

Eenheidstests omvatten het testen van een deel van een app in isolatie van de infrastructuur en afhankelijkheden. Wanneer eenheidstestcontrollerlogica wordt getest, wordt alleen de inhoud van één actie getest, niet het gedrag van de afhankelijkheden of van het framework zelf.

Stel eenheidstests van controlleracties in om te focussen op het gedrag van de controller. Een controllereenheidtest vermijdt scenario's zoals filters, routering en modelbinding. Tests die betrekking hebben op de interacties tussen onderdelen die gezamenlijk reageren op een aanvraag, worden verwerkt door integratietests. Zie Integratietests in ASP.NET Core voor meer informatie over integratietests.

Als u aangepaste filters en routes schrijft, test u deze afzonderlijk, niet als onderdeel van tests voor een bepaalde controlleractie.

Bekijk de volgende controller in de voorbeeld-app om tests van controllereenheden te demonstreren. De Home controller geeft een lijst met brainstormsessies weer en maakt het mogelijk om nieuwe brainstormsessies te maken met een POST-aanvraag:

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

De voorgaande controller:

De HTTP GET Index methode heeft geen lus of vertakking en roept slechts één methode aan. De eenheidstest voor deze handeling:

  • Hiermee wordt de IBrainstormSessionRepository service gesimuleerd met behulp van de GetTestSessions methode. GetTestSessions maakt twee mock brainstormsessies met datums en sessienamen.
  • Hiermee wordt de Index methode uitgevoerd.
  • Doet beweringen over het resultaat dat door de methode wordt geretourneerd:
    • Er wordt een ViewResult geretourneerd.
    • Het ViewDataDictionary.Model is een StormSessionViewModel.
    • Er zijn twee brainstormsessies opgeslagen in de ViewDataDictionary.Model.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

    // Act
    var result = await controller.Index();

    // Assert
    var viewResult = Assert.IsType<ViewResult>(result);
    var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(
        viewResult.ViewData.Model);
    Assert.Equal(2, model.Count());
}
private List<BrainstormSession> GetTestSessions()
{
    var sessions = new List<BrainstormSession>();
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 2),
        Id = 1,
        Name = "Test One"
    });
    sessions.Add(new BrainstormSession()
    {
        DateCreated = new DateTime(2016, 7, 1),
        Id = 2,
        Name = "Test Two"
    });
    return sessions;
}

De Home methode van de HTTP POST Index controller test en verifieert dat:

  • Wanneer ModelState.IsValid is false, retourneert de actiemethode een 400 Ongeldige aanvraagViewResult met de juiste gegevens.
  • Wanneer ModelState.IsValid is true:
    • De Add methode in de opslagplaats wordt aangeroepen.
    • A RedirectToActionResult wordt geretourneerd met de juiste argumenten.

Er wordt een ongeldige modelstatus getest door fouten toe te voegen met behulp van AddModelError, zoals weergegeven in de eerste test hieronder:

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

Als ModelState niet geldig is, wordt hetzelfde ViewResult geretourneerd als voor een GET-aanvraag. De test probeert geen ongeldig model door te geven. Het doorgeven van een ongeldig model is geen geldige benadering, omdat modelbinding niet wordt uitgevoerd (hoewel een integratietest wel modelbinding gebruikt). In dit geval wordt modelbinding niet getest. Deze eenheidstests testen alleen de code in de actiemethode.

De tweede test controleert of wanneer het ModelState geldig is:

  • Er wordt een nieuwe BrainstormSession toegevoegd (via de opslagplaats).
  • De methode retourneert een RedirectToActionResult met de verwachte eigenschappen.

Gesimuleerde aanroepen die niet worden aangeroepen, worden normaal gesproken genegeerd, maar het aanroepen Verifiable aan het einde van de installatieoproep maakt mockvalidatie in de test mogelijk. Dit wordt uitgevoerd met de aanroep naar mockRepo.Verify, waardoor de test mislukt als de verwachte methode niet is aangeroepen.

Note

De Moq-bibliotheek die in dit voorbeeld wordt gebruikt, maakt het mogelijk om verifieerbare of 'strikte' mocks te combineren met niet-verifieerbare mocks (ook wel 'losse' mocks of stubs genoemd). Meer informatie over het aanpassen van mockgedrag met Moq.

SessionController in de voorbeeld-app geeft informatie weer met betrekking tot een bepaalde brainstormsessie. De controller bevat logica voor het verwerken van ongeldige id waarden (er zijn twee return scenario's in het volgende voorbeeld om deze scenario's te behandelen). De laatste return instructie retourneert een nieuw StormSessionViewModel object voor de weergave (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);
    }
}

De eenheidstests bevatten één test voor elk return scenario in de sessiecontrolleractie Index :

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

Als u overstapt op de Ideeën-controller, wordt de functionaliteit van de app beschikbaar gesteld als een web-API op de api/ideas route:

  • Een lijst met ideeën (IdeaDTO) die zijn gekoppeld aan een brainstormsessie, wordt geretourneerd door de ForSession methode.
  • Met de Create methode worden nieuwe ideeën toegevoegd aan een sessie.
[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);
}

Vermijd het rechtstreeks retourneren van zakelijke domeinentiteiten via API-aanroepen. Domeinentiteiten:

  • Neem vaak meer gegevens op dan de client vereist.
  • Koppel het interne domeinmodel van de app onnodig aan de openbaar weergegeven API.

Mapping tussen domeinentiteiten en de typen die aan de client worden geretourneerd, kan worden uitgevoerd:

Vervolgens demonstreert de voorbeeld-app eenheidstests voor de Create en ForSession API-methoden van de Ideeën-controller.

De voorbeeld-app bevat twee ForSession tests. De eerste test bepaalt of ForSession een NotFoundObjectResult (HTTP Niet gevonden) retourneert voor een ongeldige sessie:

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

De tweede ForSession test bepaalt of ForSession een lijst met sessieideeën (<List<IdeaDTO>>) voor een geldige sessie wordt geretourneerd. De controles onderzoeken ook het eerste idee om te bevestigen dat de Name eigenschap juist is:

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

Als u het gedrag van de Create methode wilt testen wanneer de ModelState methode ongeldig is, voegt de voorbeeld-app een modelfout toe aan de controller als onderdeel van de test. Probeer geen modelvalidatie of modelbinding te testen in eenheidstests. Test alleen het gedrag van de actiemethode wanneer u wordt geconfronteerd met een ongeldige ModelStateactiemethode:

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

De tweede test van Create hangt af van de opslagplaats die null retourneert, dus de mock-opslagplaats is geconfigureerd om null terug te geven. U hoeft geen testdatabase (in het geheugen of anderszins) te maken en een query te maken die dit resultaat retourneert. De test kan in één instructie worden uitgevoerd, zoals in de voorbeeldcode wordt geïllustreerd:

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

De derde Create test controleert of de Create_ReturnsNewlyCreatedIdeaForSession methode van de repository wordt aangeroepen. De mock wordt aangeroepen met Verifiable, en de methode van de gesimuleerde opslagplaats Verify wordt aangeroepen om te bevestigen dat de verifieerbare methode wordt uitgevoerd. Het is niet de verantwoordelijkheid van de eenheidstest om ervoor te zorgen dat de UpdateAsync methode de gegevens heeft opgeslagen, die kan worden uitgevoerd met een integratietest.

[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 of hoger kunt u met ActionResult<T> (ActionResult<TValue>) een type retourneren dat is afgeleid van ActionResult of een specifiek type retourneert.

De voorbeeld-app bevat een methode die een List<IdeaDTO> voor een bepaalde sessie idretourneert. Als de sessie id niet bestaat, retourneert NotFoundde controller:

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

Twee tests van de ForSessionActionResult controller zijn opgenomen in de ApiIdeasControllerTests.

De eerste test bevestigt dat de controller een ActionResult retourneert, maar geen niet-bestaande lijst met ideeën voor een niet-bestaande sessie 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);
}

Voor een geldige sessie idbevestigt de tweede test dat de methode retourneert:

  • Een ActionResult met een List<IdeaDTO> type.
  • De ActionResult<T>.Waarde is van het type List<IdeaDTO>.
  • Het eerste item in de lijst is een geldig idee dat overeenkomt met het idee dat is opgeslagen in de mocksessie (verkregen door aan te roepen 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);
}

De voorbeeld-app bevat ook een methode voor het maken van een nieuwe Idea voor een bepaalde sessie. De controller geeft:

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

Er zijn drie tests CreateActionResult opgenomen in de ApiIdeasControllerTests.

In de eerste tekst wordt bevestigd dat er een BadRequest wordt geretourneerd voor een ongeldig 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);
}

De tweede test controleert of er een NotFound wordt geretourneerd als de sessie niet bestaat.

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

Voor een geldige sessie idbevestigt de laatste test dat:

  • De methode retourneert een ActionResult met een BrainstormSession type.
  • De ActionResult<T>.Result is een CreatedAtActionResult. CreatedAtActionResult is vergelijkbaar met een 201 Gemaakt antwoord met een Location header.
  • De ActionResult<T>.Waarde is van het type BrainstormSession.
  • De mock-aanroep om de sessie bij te werken, UpdateAsync(testSession), werd aangeroepen. De Verifiable methode-aanroep wordt gecontroleerd door de asserties uit te mockRepo.Verify() voeren.
  • Er worden twee Idea objecten geretourneerd voor de sessie.
  • Het laatste item (de Idea toegevoegde door de mock-aanroep) UpdateAsynckomt overeen met het newIdea item dat is toegevoegd aan de sessie in de 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);
}

Aanvullende bronnen