Testuj jednostkowo logikę kontrolera w ASP.NET Core

Autorstwa Steve'a Smitha

Testy jednostkowe obejmują testowanie części aplikacji w izolacji od jej infrastruktury i zależności. Podczas testowania jednostkowego logiki kontrolera sprawdzana jest tylko zawartość pojedynczej akcji, a nie zachowanie jego zależności ani samego frameworka.

Kontrolery testów jednostkowych

Skonfiguruj testy jednostkowe akcji kontrolera, koncentrując się na zachowaniu kontrolera. Test jednostkowy kontrolera unika scenariuszy takich jak filtry, routing i wiązanie modelu. Testy obejmujące interakcje między składnikami, które wspólnie odpowiadają na żądanie, są obsługiwane przez testy integracji. Aby uzyskać więcej informacji na temat testów integracji, zobacz Testy integracji w programie ASP.NET Core.

Jeśli piszesz niestandardowe filtry i routing, przetestuj je w izolacji, a nie w ramach testów dla określonej akcji kontrolera.

Aby zademonstrować testy jednostkowe kontrolera, zapoznaj się z poniższym kontrolerem w przykładowej aplikacji.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Kontroler Home wyświetla listę sesji burzy mózgów i umożliwia tworzenie nowych sesji burzy mózgów z żądaniem POST:

public class HomeController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public HomeController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index()
    {
        var sessionList = await _sessionRepository.ListAsync();

        var model = sessionList.Select(session => new StormSessionViewModel()
        {
            Id = session.Id,
            DateCreated = session.DateCreated,
            Name = session.Name,
            IdeaCount = session.Ideas.Count
        });

        return View(model);
    }

    public class NewSessionModel
    {
        [Required]
        public string SessionName { get; set; }
    }

    [HttpPost]
    public async Task<IActionResult> Index(NewSessionModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        else
        {
            await _sessionRepository.AddAsync(new BrainstormSession()
            {
                DateCreated = DateTimeOffset.Now,
                Name = model.SessionName
            });
        }

        return RedirectToAction(actionName: nameof(Index));
    }
}

Poprzedni kontroler:

  • Przestrzega Zasady jawnych zależności.
  • Oczekuje, że iniekcja zależności (DI) dostarczy instancję klasy IBrainstormSessionRepository.
  • Można to przetestować przy użyciu pozorowanej usługi IBrainstormSessionRepository oraz frameworka obiektów pozorowanych, takiego jak Moq. Obiekt mockowany jest obiektem skonstruowanym ze wstępnie określonym zestawem zachowań właściwości i metod używanych do testowania. Aby uzyskać więcej informacji, zobacz Wprowadzenie do testów integracji.

Metoda HTTP GET Index nie ma pętli ani rozgałęziania i wywołuje tylko jedną metodę. Test jednostkowy dla tej akcji:

  • Imituje usługę IBrainstormSessionRepository przy użyciu metody GetTestSessions. GetTestSessions Tworzy dwie próbne sesje burzy mózgów z datami i nazwami sesji.
  • Wykonuje metodę Index.
  • Wykonuje asercje na wyniku zwróconym przez metodę:
[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;
}

Test metody kontrolera HomeHTTP POST Index sprawdza, czy:

Nieprawidłowy stan modelu testuje się, dodając błędy przy użyciu AddModelError, jak pokazano w pierwszym teście poniżej:

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

Jeśli element ModelState jest niepoprawny, zwracany jest ten sam ViewResult, co w przypadku żądania GET. Test nie próbuje przejść w nieprawidłowym modelu. Przekazywanie nieprawidłowego modelu nie jest prawidłowym podejściem, ponieważ powiązanie modelu nie jest uruchomione (chociaż test integracji używa powiązania modelu). W takim przypadku powiązanie modelu nie jest testowane. Te testy jednostkowe testują tylko kod w metodzie akcji.

Drugi test sprawdza, że jeśli element ModelState jest prawidłowy:

  • Nowy BrainstormSession jest dodawany (za pośrednictwem repozytorium).
  • Metoda zwraca RedirectToActionResult obiekt z oczekiwanymi właściwościami.

Wyśmiewane wywołania, które nie są wywoływane, są zwykle ignorowane, ale wywołanie Verifiable na końcu wywołania konfiguracji umożliwia pozorowanie weryfikacji w teście. Odbywa się to przez wywołanie mockRepo.Verify, co powoduje niepowodzenie testu, jeśli oczekiwana metoda nie została wywołana.

Note

Biblioteka Moq używana w tym przykładzie umożliwia mieszanie weryfikowalnych lub "surowych" makiety z niemożliwymi do zweryfikowania makiety (nazywane również "luźnymi" makietami lub wycinkami). Dowiedz się więcej o dostosowywaniu zachowania obiektu Mock w bibliotece Moq.

SessionController w przykładowej aplikacji wyświetla informacje związane z określoną sesją burzy mózgów. Kontroler zawiera logikę do obsługi nieprawidłowych id wartości (istnieją dwa return scenariusze w poniższym przykładzie, aby uwzględnić te scenariusze). Ostatnia instrukcja return zwraca nowy StormSessionViewModel do widoku (Controllers/SessionController.cs):

public class SessionController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public SessionController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index(int? id)
    {
        if (!id.HasValue)
        {
            return RedirectToAction(actionName: nameof(Index), 
                controllerName: "Home");
        }

        var session = await _sessionRepository.GetByIdAsync(id.Value);
        if (session == null)
        {
            return Content("Session not found.");
        }

        var viewModel = new StormSessionViewModel()
        {
            DateCreated = session.DateCreated,
            Name = session.Name,
            Id = session.Id
        };

        return View(viewModel);
    }
}

Testy jednostkowe obejmują jeden test dla każdego return scenariusza w akcji Index kontrolera Session:

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

Przechodząc do kontrolera Ideas, aplikacja udostępnia funkcje jako internetowy interfejs API pod ścieżką api/ideas:

  • Lista pomysłów (IdeaDTO) powiązanych z sesją burzy mózgów jest zwracana przez metodę ForSession.
  • Metoda Create dodaje nowe pomysły do sesji.
[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);
}

Unikaj zwracania encji domeny biznesowej bezpośrednio za pośrednictwem wywołań interfejsu API. Encje domenowe:

  • Często zawierają więcej danych niż wymaga klient.
  • Niepotrzebnie połącz wewnętrzny model domeny aplikacji z publicznie uwidocznionym interfejsem API.

Mapowanie między jednostkami domeny a typami zwracanym do klienta można wykonać:

Następnie przykładowa aplikacja demonstruje testy jednostkowe metod Create i ForSession API kontrolera Ideas.

Przykładowa aplikacja zawiera dwa ForSession testy. Pierwszy test określa, czy ForSession zwraca wartość NotFoundObjectResult (nie znaleziono protokołu HTTP) dla nieprawidłowej sesji:

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

Drugi ForSession test sprawdza, czy ForSession zwraca listę propozycji sesji (<List<IdeaDTO>>) dla poprawnej sesji. Kontrole sprawdzają również pierwszą koncepcję, aby potwierdzić, że jej właściwość Name jest poprawna:

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

Aby przetestować działanie metody Create, gdy parametr ModelState jest nieprawidłowy, przykładowa aplikacja dodaje do kontrolera błąd modelu w ramach testowania. Nie próbuj testować walidacji modelu ani powiązania modelu w testach jednostkowych — po prostu przetestuj zachowanie metody akcji w obliczu nieprawidłowego ModelState:

[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.Create(model: null);

    // Assert
    Assert.IsType<BadRequestObjectResult>(result);
}

Drugi test Create funkcji zależy od zwracanego nullrepozytorium , więc repozytorium makietowe jest skonfigurowane tak, aby zwracało wartość null. Nie ma potrzeby tworzenia testowej bazy danych (w pamięci lub w inny sposób) i tworzenia zapytania zwracającego ten wynik. Test można wykonać w jednej instrukcji, jak pokazano w przykładowym kodzie:

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

Trzeci Create test sprawdza, Create_ReturnsNewlyCreatedIdeaForSessionczy metoda repozytorium jest wywoływana UpdateAsync . Pozorowana metoda jest wywoływana za pomocą Verifiablemetody , a metoda wyśmiewanego repozytorium Verify jest wywoływana w celu potwierdzenia wykonania metody weryfikowalnej. Nie jest to odpowiedzialność za test jednostkowy w celu zapewnienia, że UpdateAsync metoda zapisała dane — które można wykonać z testem integracji.

[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>) może zwrócić typ pochodzący z ActionResult lub zwrócić określony typ.

Przykładowa aplikacja zawiera metodę, która dla danej sesji id zwraca List<IdeaDTO>. Jeśli sesja id nie istnieje, kontroler zwraca NotFound:

[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);

    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return result;
}

Dwa testy kontrolera ForSessionActionResult są zawarte w ApiIdeasControllerTests.

Pierwszy test potwierdza, że kontroler zwraca ActionResult, ale nie nieistniejącą listę pomysłów na nieistniejącą sesję 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);
}

W przypadku prawidłowej sesji iddrugi test potwierdza, że metoda zwraca następujące elementy:

  • Element ActionResult typu List<IdeaDTO>.
  • Element ActionResult<T>.Value jest typu List<IdeaDTO>.
  • Pierwszy element na liście jest prawidłową ideą zgodną z ideą przechowywaną w sesji mock (uzyskaną przez wywołanie metody 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);
}

Przykładowa aplikacja zawiera również metodę tworzenia nowej Idea dla danej sesji. Kontroler zwraca:

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

Trzy testy CreateActionResult są zawarte w ApiIdeasControllerTests.

Pierwszy tekst potwierdza, że zwracany jest element BadRequest dla nieprawidłowego modelu.

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

Drugi test sprawdza, czy zwracany jest NotFound, jeśli sesja nie istnieje.

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

Dla prawidłowej sesji id test końcowy potwierdza, że:

  • Metoda zwraca ActionResult typu BrainstormSession.
  • ActionResult<T>.Result to CreatedAtActionResult. CreatedAtActionResult jest analogiczny do odpowiedzi 201 Utworzono z nagłówkiem Location .
  • ActionResult<T>.Value to typ BrainstormSession.
  • Wywołanie pozorne w celu zaktualizowania sesji, UpdateAsync(testSession)zostało wywołane. Wywołanie Verifiable metody jest sprawdzane przez wykonanie mockRepo.Verify() w asercji.
  • Dla sesji są zwracane dwa Idea obiekty.
  • Ostatni element (Idea dodany przez pozorowane wywołanie do UpdateAsync) odpowiada elementowi newIdea dodanemu do sesji w teście.
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

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

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

    // Assert
    var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
    var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
    var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
    mockRepo.Verify();
    Assert.Equal(2, returnValue.Ideas.Count());
    Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
    Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);
}

Kontrolery odgrywają kluczową rolę w dowolnej aplikacji MVC platformy ASP.NET Core. W związku z tym należy mieć pewność, że kontrolery zachowują się zgodnie z oczekiwaniami. Testy automatyczne mogą wykrywać błędy przed wdrożeniem aplikacji w środowisku produkcyjnym.

Wyświetl lub pobierz przykładowy kod (jak pobrać)

Testy jednostkowe logiki kontrolera

Testy jednostkowe obejmują testowanie części aplikacji w izolacji od jej infrastruktury i zależności. Podczas testowania jednostkowego logiki kontrolera sprawdzana jest tylko zawartość pojedynczej akcji, a nie zachowanie jego zależności ani samego frameworka.

Skonfiguruj testy jednostkowe akcji kontrolera tak, aby skupiały się na zachowaniu kontrolera. Test jednostkowy kontrolera pomija scenariusze takie jak filtry, routing i wiązanie modelu. Testy obejmujące interakcje między składnikami, które wspólnie odpowiadają na żądanie, są obsługiwane przez testy integracyjne. Aby uzyskać więcej informacji na temat testów integracyjnych, zobacz Testy integracyjne w ASP.NET Core.

Jeśli piszesz niestandardowe filtry i routing, testuj je jednostkowo w izolacji, a nie jako część testów konkretnej akcji kontrolera.

Aby zademonstrować testy jednostkowe kontrolera, zapoznaj się z następującym kontrolerem w przykładowej aplikacji. Kontroler Home wyświetla listę sesji burzy mózgów i umożliwia tworzenie nowych sesji burzy mózgów za pomocą żądania POST:

public class HomeController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public HomeController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index()
    {
        var sessionList = await _sessionRepository.ListAsync();

        var model = sessionList.Select(session => new StormSessionViewModel()
        {
            Id = session.Id,
            DateCreated = session.DateCreated,
            Name = session.Name,
            IdeaCount = session.Ideas.Count
        });

        return View(model);
    }

    public class NewSessionModel
    {
        [Required]
        public string SessionName { get; set; }
    }

    [HttpPost]
    public async Task<IActionResult> Index(NewSessionModel model)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
        else
        {
            await _sessionRepository.AddAsync(new BrainstormSession()
            {
                DateCreated = DateTimeOffset.Now,
                Name = model.SessionName
            });
        }

        return RedirectToAction(actionName: nameof(Index));
    }
}

Poprzedni kontroler:

Metoda HTTP GET Index nie ma pętli ani rozgałęziania i wywołuje tylko jedną metodę. Test jednostkowy dla tej akcji:

  • Wyśmiewa usługę IBrainstormSessionRepositoryGetTestSessions przy użyciu metody . GetTestSessions Tworzy dwie makiety sesji burzy mózgów z datami i nazwami sesji.
  • Wykonuje metodę Index .
  • Tworzy asercje dotyczące wyniku zwróconego przez metodę:
    • Zwracane jest A ViewResult.
    • ViewDataDictionary.Model jest elementem StormSessionViewModel.
    • Istnieją dwie sesje burzy mózgów przechowywane w ViewDataDictionary.Model.
[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
    // Arrange
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.ListAsync())
        .ReturnsAsync(GetTestSessions());
    var controller = new HomeController(mockRepo.Object);

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

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

Metoda HTTP POST Index kontrolera Home sprawdza, że:

Nieprawidłowy stan modelu jest testowany przez dodanie błędów, jak AddModelError pokazano w pierwszym teście poniżej:

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

Jeśli parametr ModelState jest nieprawidłowy, jest zwracany taki sam ViewResult jak w przypadku żądania GET. Test nie próbuje przekazać nieprawidłowego modelu. Przekazywanie niepoprawnego modelu nie jest właściwym podejściem, ponieważ wiązanie modelu nie działa (chociaż test integracyjny korzysta z wiązania modelu). W takim przypadku powiązanie modelu nie jest testowane. Te testy jednostkowe testują tylko kod w metodzie obsługi akcji.

Drugi test sprawdza, czy gdy ModelState element jest prawidłowy:

  • Zostanie dodany nowy BrainstormSession (za pośrednictwem repozytorium).
  • Metoda zwraca RedirectToActionResult obiekt z oczekiwanymi właściwościami.

Zamockowane wywołania, które nie zostały wywołane, są zwykle ignorowane, ale wywołanie Verifiable na końcu wywołania konfiguracji umożliwia weryfikację obiektu mock w teście. Jest to wykonywane za pomocą wywołania mockRepo.Verify, które powoduje niepowodzenie testu, jeśli oczekiwana metoda nie została wywołana.

Note

Biblioteka Moq używana w tym przykładzie umożliwia łączenie weryfikowalnych, czyli „ścisłych”, obiektów pozorowanych z nieweryfikowalnymi obiektami pozorowanymi (nazywanymi również „luźnymi” obiektami pozorowanymi lub zaślepkami). Dowiedz się więcej na temat dostosowywania zachowania makiety za pomocą narzędzia Moq.

SessionController w przykładowej aplikacji wyświetla informacje związane z określoną sesją burzy mózgów. Kontroler zawiera logikę do obsługi nieprawidłowych id wartości (istnieją dwa return scenariusze w poniższym przykładzie, aby uwzględnić te scenariusze). return Końcowa instrukcja zwraca nowy StormSessionViewModel widok (Controllers/SessionController.cs):

public class SessionController : Controller
{
    private readonly IBrainstormSessionRepository _sessionRepository;

    public SessionController(IBrainstormSessionRepository sessionRepository)
    {
        _sessionRepository = sessionRepository;
    }

    public async Task<IActionResult> Index(int? id)
    {
        if (!id.HasValue)
        {
            return RedirectToAction(actionName: nameof(Index), 
                controllerName: "Home");
        }

        var session = await _sessionRepository.GetByIdAsync(id.Value);
        if (session == null)
        {
            return Content("Session not found.");
        }

        var viewModel = new StormSessionViewModel()
        {
            DateCreated = session.DateCreated,
            Name = session.Name,
            Id = session.Id
        };

        return View(viewModel);
    }
}

Testy jednostkowe obejmują jeden test dla każdego return scenariusza w akcji Kontroler Index sesji:

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

Przejście do kontrolera Ideas umożliwia aplikacji uwidacznianie funkcji jako internetowego interfejsu API na api/ideas trasie:

  • Lista pomysłów (IdeaDTO) skojarzonych z sesją burzy mózgów jest zwracana przez metodę ForSession .
  • Metoda Create dodaje nowe pomysły do sesji.
[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);
}

Unikaj zwracania jednostek domeny biznesowej bezpośrednio za pośrednictwem wywołań interfejsu API. Jednostki domeny:

  • Często zawierają więcej danych niż wymaga klient.
  • Niepotrzebnie połącz wewnętrzny model domeny aplikacji z publicznie udostępnionym interfejsem API.

Mapowanie między jednostkami domeny a typami zwracanym do klienta można wykonać:

Następnie przykładowa aplikacja demonstruje testy jednostkowe metod Create i ForSession interfejsu API kontrolera Ideas.

Przykładowa aplikacja zawiera dwa ForSession testy. Pierwszy test określa, czy ForSession zwraca wartość NotFoundObjectResult (HTTP Not Found) dla nieprawidłowej sesji:

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

Drugi ForSession test określa, czy ForSession zwraca listę pomysłów sesji (<List<IdeaDTO>>) dla prawidłowej sesji. Kontrole sprawdzają również pierwszy pomysł, aby potwierdzić, że jego Name właściwość jest poprawna:

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

Aby przetestować zachowanie Create metody, gdy ModelState parametr jest nieprawidłowy, przykładowa aplikacja dodaje błąd modelu do kontrolera w ramach testu. Nie próbuj testować walidacji modelu ani powiązania modelu w testach jednostkowych — po prostu przetestuj zachowanie metody akcji, gdy otrzyma nieprawidłowy ModelState:

[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.Create(model: null);

    // Assert
    Assert.IsType<BadRequestObjectResult>(result);
}

Drugi test elementu Create zależy od tego, czy repozytorium zwraca null, więc pozorowane repozytorium skonfigurowano tak, aby zwracało null. Nie ma potrzeby tworzenia testowej bazy danych (w pamięci lub w inny sposób) i konstruowania zapytania zwracającego ten wynik. Test można wykonać w jednej instrukcji, jak pokazano w przykładowym kodzie:

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

Trzeci Create test sprawdza, Create_ReturnsNewlyCreatedIdeaForSessionczy metoda repozytorium jest wywoływana UpdateAsync . Obiekt mock jest wywoływany z użyciem Verifiable, a metoda Verify mockowanego repozytorium jest wywoływana w celu potwierdzenia wykonania metody weryfikowalnej. Nie jest obowiązkiem testu jednostkowego, aby upewnić się, że metoda UpdateAsync zapisała dane — można to sprawdzić testem integracyjnym.

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

W systemie ASP.NET Core 2.1 lub nowszym ActionResult<T> (ActionResult<TValue>) umożliwia zwrócenie typu pochodzącego z ActionResultActionResult lub zwrócenie określonego typu.

Przykładowa aplikacja zawiera metodę zwracającą List<IdeaDTO> dla danej sesji id. Jeśli sesja id nie istnieje, kontroler zwraca NotFound:

[HttpGet("forsessionactionresult/{sessionId}")]
[ProducesResponseType(200)]
[ProducesResponseType(404)]
public async Task<ActionResult<List<IdeaDTO>>> ForSessionActionResult(int sessionId)
{
    var session = await _sessionRepository.GetByIdAsync(sessionId);

    if (session == null)
    {
        return NotFound(sessionId);
    }

    var result = session.Ideas.Select(idea => new IdeaDTO()
    {
        Id = idea.Id,
        Name = idea.Name,
        Description = idea.Description,
        DateCreated = idea.DateCreated
    }).ToList();

    return result;
}

Dwa testy kontrolera ForSessionActionResult są zawarte w ApiIdeasControllerTests.

Pierwszy test potwierdza, że kontroler zwraca element ActionResult, ale nie nieistniejącą listę pomysłów na nieistniejącą sesję 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);
}

W przypadku prawidłowej sesji id, drugi test potwierdza, że metoda zwraca:

  • Element typu List<IdeaDTO> z ActionResult.
  • AkcjaResult <T>. Wartość jest typemList<IdeaDTO>.
  • Pierwszy element na liście jest prawidłową ideą zgodną z ideą przechowywaną w sesji mock (uzyskaną przez wywołanie metody 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);
}

Przykładowa aplikacja zawiera również metodę służącą do tworzenia nowej Idea dla danej sesji. Kontroler zwraca następujące dane:

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

Trzy testy CreateActionResult są zawarte w ApiIdeasControllerTests.

Pierwszy tekst potwierdza, że BadRequest element jest zwracany dla nieprawidłowego modelu.

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

Drugi test sprawdza, czy NotFound jest zwracany, jeśli sesja nie istnieje.

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

W przypadku prawidłowej sesji idkońcowy test potwierdza, że:

  • Metoda zwraca ActionResult typu BrainstormSession.
  • ActionResult<T>.Result to CreatedAtActionResult. CreatedAtActionResult jest analogiczny do odpowiedzi 201 Utworzono z nagłówkiem Location .
  • AkcjaResult <T>. Wartość jest typemBrainstormSession.
  • Pozorowane wywołanie w celu zaktualizowania sesji, UpdateAsync(testSession), zostało wykonane. Wywołanie Verifiable metody jest sprawdzane przez wykonanie mockRepo.Verify() w asercji.
  • Dla sesji są zwracane dwa Idea obiekty.
  • Ostatni element (Idea dodany przez wywołanie makiety do UpdateAsync) jest zgodny z elementem newIdea dodanym do sesji w teście.
[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);
}