Menguji unit logika controller di ASP.NET Core

Oleh Steve Smith

Pengujian unit melibatkan pengujian bagian dari aplikasi dalam isolasi dari infrastruktur dan dependensinya. Saat menguji unit logika pengontrol, hanya konten dari satu tindakan yang diuji, bukan perilaku dependensinya atau kerangka kerja itu sendiri.

Pengujian unit untuk controller

Siapkan pengujian unit untuk aksi controller agar berfokus pada perilaku controller. Pengujian unit pengontrol menghindari skenario seperti filter, perutean, dan pengikatan model. Pengujian yang mencakup interaksi di antara komponen yang secara kolektif menanggapi permintaan ditangani oleh pengujian integrasi. Untuk informasi selengkapnya tentang pengujian integrasi, lihat Pengujian integrasi di ASP.NET Core.

Jika Anda menulis filter dan rute kustom, uji unit filter dan rute tersebut secara terpisah, bukan sebagai bagian dari pengujian pada tindakan pengontrol tertentu.

Untuk mendemonstrasikan uji unit pengontrol, tinjau pengontrol berikut di aplikasi contoh.

Lihat atau unduh contoh kode (cara mengunduhnya)

Controller Home menampilkan daftar sesi brainstorming dan memungkinkan pembuatan sesi brainstorming baru melalui permintaan 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));
    }
}

Kontroler sebelumnya:

  • Mengikuti Prinsip Dependensi Eksplisit.
  • Mengharapkan agar injeksi dependensi (DI) menyediakan instans IBrainstormSessionRepository.
  • Dapat diuji dengan layanan yang dimock IBrainstormSessionRepository menggunakan kerangka kerja objek tiruan, seperti Moq. Sebuah objek tiruan adalah objek fabrikasi dengan serangkaian perilaku properti dan metode yang telah ditentukan yang digunakan untuk pengujian. Untuk informasi selengkapnya, lihat Pengenalan pengujian integrasi.

Metode HTTP GET Index ini tidak memiliki perulangan atau percabangan dan hanya memanggil satu metode. Unit test untuk aksi ini:

  • Mock layanan IBrainstormSessionRepository menggunakan metode GetTestSessions. GetTestSessions membuat dua sesi brainstorming simulasi dengan tanggal dan nama sesi.
  • Menjalankan metode Index.
  • Membuat asersi terhadap hasil yang dikembalikan oleh metode:
[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;
}

Metode HTTP POST Index pengontrol Home menguji dan memverifikasi bahwa:

  • Ketika ModelState.IsValid adalah false, metode tindakan mengembalikan 400 Bad RequestViewResult dengan data yang sesuai.
  • Saat ModelState.IsValid adalah true:

Status model yang tidak valid diuji dengan menambahkan kesalahan menggunakan AddModelError seperti yang ditunjukkan pada pengujian pertama di bawah ini:

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

Saat ModelState tidak valid, ViewResult yang sama dikembalikan seperti untuk permintaan GET. Pengujian tidak mencoba untuk meneruskan model yang tidak valid. Meneruskan model yang tidak valid bukan pendekatan yang valid, karena pengikatan model tidak berjalan (meskipun pengujian integrasi memang menggunakan pengikatan model). Dalam hal ini, pengikatan model tidak diuji. Pengujian unit ini hanya menguji kode dalam metode action.

Pengujian kedua memverifikasi bahwa ketika ModelState valid:

  • Sebuah BrainstormSession baru ditambahkan (melalui repositori).
  • Metode mengembalikan RedirectToActionResult dengan properti yang diharapkan.

Pemanggilan tiruan yang tidak dipanggil biasanya diabaikan, tetapi memanggil Verifiable di akhir panggilan penyiapan memungkinkan validasi pemanggilan tiruan dalam pengujian. Ini dilakukan dengan panggilan ke mockRepo.Verify, yang menyebabkan pengujian gagal jika metode yang diharapkan tidak dipanggil.

Note

Pustaka Moq yang digunakan dalam sampel ini memungkinkan untuk mencampur tiruan yang dapat diverifikasi, atau "ketat", tiruan dengan tiruan yang tidak dapat diverifikasi (juga disebut tiruan atau stub "longgar"). Pelajari selengkapnya tentang menyesuaikan perilaku Mock dengan Moq.

SessionController dalam aplikasi sampel menampilkan informasi yang terkait dengan sesi curah gagasan tertentu. Pengontrol mencakup logika untuk menangani nilai yang tidak valid id (ada dua return skenario dalam contoh berikut untuk mencakup skenario ini). Pernyataan return terakhir mengembalikan sebuah StormSessionViewModel baru ke tampilan (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);
    }
}

Pengujian unit mencakup satu pengujian untuk setiap return skenario dalam pengontrol Session Index aksi:

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

Beralih ke controller Ideas, aplikasi mengekspos fungsi sebagai API web pada rute api/ideas:

  • Daftar ide (IdeaDTO) yang terkait dengan sesi curah gagasan dikembalikan oleh ForSession metode .
  • Metode Create ini menambahkan ide baru ke sesi.
[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);
}

Hindari mengembalikan entitas domain bisnis secara langsung melalui panggilan API. Entitas domain:

  • Sering kali menyertakan lebih banyak data daripada yang dibutuhkan klien.
  • Jangan menggandengkan model domain internal aplikasi dengan API yang diekspos ke publik secara tidak perlu.

Pemetaan antara entitas domain dan jenis yang dikembalikan ke klien dapat dilakukan:

Selanjutnya, aplikasi sampel menunjukkan pengujian unit untuk metode API Create dan ForSession dari pengontrol Ideas.

Aplikasi sampel berisi dua ForSession pengujian. Pengujian pertama menentukan apakah ForSession mengembalikan NotFoundObjectResult (HTTP Not Found) untuk sesi yang tidak valid:

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

Pengujian kedua ForSession menentukan apakah ForSession mengembalikan daftar ide sesi (<List<IdeaDTO>>) untuk sesi yang valid. Pemeriksaan juga memeriksa ide pertama untuk mengonfirmasi propertinya Name benar:

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

Untuk menguji perilaku Create metode ketika ModelState tidak valid, aplikasi sampel menambahkan kesalahan model ke pengontrol sebagai bagian dari pengujian. Jangan mencoba menguji validasi model atau pengikatan model dalam pengujian unit—cukup uji perilaku metode tindakan saat dihadapkan dengan yang tidak valid 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);
}

Pengujian Create kedua tergantung pada repositori yang mengembalikan null, sehingga repositori tiruan dikonfigurasi untuk mengembalikan null. Tidak perlu membuat database pengujian (dalam memori atau sebaliknya) dan membuat kueri yang mengembalikan hasil ini. Pengujian dapat dilakukan dalam satu pernyataan, seperti yang diilustrasikan kode sampel:

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

Tes ketiga Create, Create_ReturnsNewlyCreatedIdeaForSession, memverifikasi bahwa metode repositori UpdateAsync dipanggil. Mock dipanggil dengan Verifiable, dan metode Verify pada repositori mock dipanggil untuk mengonfirmasi metode yang dapat diverifikasi dijalankan. Bukan tanggung jawab pengujian unit untuk memastikan bahwa metode UpdateAsync menyimpan data—yang dapat dilakukan dengan pengujian integrasi.

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

Uji ActionResult<T>

ActionResult<T> (ActionResult<TValue>) dapat mengembalikan tipe yang diturunkan dari ActionResult atau tipe tertentu.

Aplikasi sampel menyertakan metode yang mengembalikan List<IdeaDTO> untuk sesi idtertentu . Jika sesi id tidak ada, pengontrol mengembalikan 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;
}

Dua pengujian ForSessionActionResult controller disertakan dalam ApiIdeasControllerTests.

Pengujian pertama mengonfirmasi bahwa pengontrol mengembalikan ActionResult tetapi bukan daftar ide yang tidak ada untuk sesi idyang tidak ada :

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

Untuk sesi idyang valid , pengujian kedua mengonfirmasi bahwa metode mengembalikan:

  • Sebuah ActionResult dengan jenis List<IdeaDTO>.
  • ActionResult<T>.Value adalah tipe List<IdeaDTO>.
  • Item pertama dalam daftar adalah ide yang valid yang cocok dengan ide yang disimpan dalam sesi tiruan (diperoleh dengan memanggil 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);
}

Aplikasi sampel juga menyertakan metode untuk membuat Idea baru untuk sesi tertentu. Pengontrol mengembalikan:

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

Tiga pengujian CreateActionResult disertakan dalam ApiIdeasControllerTests.

Teks pertama mengonfirmasi bahwa dikembalikan BadRequest untuk model yang tidak valid.

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

Pengujian kedua memeriksa bahwa sebuah NotFound dikembalikan jika sesi tidak ada.

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

Untuk sesi idyang valid , pengujian akhir mengonfirmasi bahwa:

  • Metode mengembalikan ActionResult dengan tipe BrainstormSession.
  • ActionResult<T>.Result adalah CreatedAtActionResult. CreatedAtActionResult dianalogikan dengan respons 201 Created dengan header Location.
  • ActionResult<T>.Value adalah tipe BrainstormSession.
  • Pemanggilan tiruan untuk memperbarui sesi, UpdateAsync(testSession), telah dipanggil. Panggilan metode Verifiable diperiksa dengan menjalankan mockRepo.Verify() di dalam asersi.
  • Dua Idea objek dikembalikan untuk sesi tersebut.
  • Item terakhir (yang ditambahkan oleh panggilan tiruan ke UpdateAsync pada Idea) cocok dengan yang ditambahkan ke sesi dalam pengujian oleh newIdea.
[Fact]
public async Task CreateActionResult_ReturnsNewlyCreatedIdeaForSession()
{
    // Arrange
    int testSessionId = 123;
    string testName = "test name";
    string testDescription = "test description";
    var testSession = GetTestSession();
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync(testSession);
    var controller = new IdeasController(mockRepo.Object);

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

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

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

Controller memainkan peran sentral dalam aplikasi ASP.NET Core MVC apa pun. Dengan demikian, Anda harus memiliki keyakinan bahwa pengontrol berfungsi sebagaimana mestinya. Pengujian otomatis dapat mendeteksi kesalahan sebelum aplikasi disebarkan ke lingkungan produksi.

Lihat atau unduh contoh kode (cara mengunduhnya)

Pengujian unit logika pengontrol

Pengujian unit melibatkan pengujian bagian dari aplikasi dalam isolasi dari infrastruktur dan dependensinya. Saat menguji unit logika pengontrol, hanya konten dari satu tindakan yang diuji, bukan perilaku dependensinya atau kerangka kerja itu sendiri.

Siapkan pengujian unit untuk aksi controller agar berfokus pada perilaku controller. Pengujian unit pengontrol menghindari skenario seperti filter, perutean, dan pengikatan model. Pengujian yang mencakup interaksi di antara komponen yang secara kolektif menanggapi permintaan ditangani oleh pengujian integrasi. Untuk informasi selengkapnya tentang pengujian integrasi, lihat Pengujian integrasi di ASP.NET Core.

Jika Anda menulis filter dan rute kustom, uji unit filter dan rute tersebut secara terpisah, bukan sebagai bagian dari pengujian pada tindakan pengontrol tertentu.

Untuk mendemonstrasikan uji unit pengontrol, tinjau pengontrol berikut di aplikasi contoh. Controller Home menampilkan daftar sesi brainstorming dan memungkinkan pembuatan sesi brainstorming baru melalui permintaan 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));
    }
}

Kontroler sebelumnya:

  • Mengikuti Prinsip Dependensi Eksplisit.
  • Mengharapkan agar injeksi dependensi (DI) menyediakan instans IBrainstormSessionRepository.
  • Dapat diuji dengan layanan yang dimock IBrainstormSessionRepository menggunakan kerangka kerja objek tiruan, seperti Moq. Sebuah objek tiruan adalah objek fabrikasi dengan serangkaian perilaku properti dan metode yang telah ditentukan yang digunakan untuk pengujian. Untuk informasi selengkapnya, lihat Pengenalan pengujian integrasi.

Metode HTTP GET Index ini tidak memiliki perulangan atau percabangan dan hanya memanggil satu metode. Unit test untuk aksi ini:

  • Mock layanan IBrainstormSessionRepository menggunakan metode GetTestSessions. GetTestSessions membuat dua sesi brainstorming simulasi dengan tanggal dan nama sesi.
  • Menjalankan metode Index.
  • Membuat asersi terhadap hasil yang dikembalikan oleh metode:
[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;
}

Metode HTTP POST Index pengontrol Home menguji dan memverifikasi bahwa:

  • Ketika ModelState.IsValid adalah false, metode tindakan mengembalikan 400 Bad RequestViewResult dengan data yang sesuai.
  • Saat ModelState.IsValid adalah true:

Status model yang tidak valid diuji dengan menambahkan kesalahan menggunakan AddModelError seperti yang ditunjukkan pada pengujian pertama di bawah ini:

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

Saat ModelState tidak valid, ViewResult yang sama dikembalikan seperti untuk permintaan GET. Pengujian tidak mencoba untuk meneruskan model yang tidak valid. Meneruskan model yang tidak valid bukan pendekatan yang valid, karena pengikatan model tidak berjalan (meskipun pengujian integrasi memang menggunakan pengikatan model). Dalam hal ini, pengikatan model tidak diuji. Pengujian unit ini hanya menguji kode dalam metode action.

Pengujian kedua memverifikasi bahwa ketika ModelState valid:

  • Sebuah BrainstormSession baru ditambahkan (melalui repositori).
  • Metode mengembalikan RedirectToActionResult dengan properti yang diharapkan.

Pemanggilan tiruan yang tidak dipanggil biasanya diabaikan, tetapi memanggil Verifiable di akhir panggilan penyiapan memungkinkan validasi pemanggilan tiruan dalam pengujian. Ini dilakukan dengan panggilan ke mockRepo.Verify, yang menyebabkan pengujian gagal jika metode yang diharapkan tidak dipanggil.

Note

Pustaka Moq yang digunakan dalam sampel ini memungkinkan untuk mencampur tiruan yang dapat diverifikasi, atau "ketat", tiruan dengan tiruan yang tidak dapat diverifikasi (juga disebut tiruan atau stub "longgar"). Pelajari selengkapnya tentang menyesuaikan perilaku Mock dengan Moq.

SessionController dalam aplikasi sampel menampilkan informasi yang terkait dengan sesi curah gagasan tertentu. Pengontrol mencakup logika untuk menangani nilai yang tidak valid id (ada dua return skenario dalam contoh berikut untuk mencakup skenario ini). Pernyataan return terakhir mengembalikan sebuah StormSessionViewModel baru ke tampilan (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);
    }
}

Pengujian unit mencakup satu pengujian untuk setiap return skenario dalam pengontrol Session Index aksi:

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

Beralih ke controller Ideas, aplikasi mengekspos fungsi sebagai API web pada rute api/ideas:

  • Daftar ide (IdeaDTO) yang terkait dengan sesi curah gagasan dikembalikan oleh ForSession metode .
  • Metode Create ini menambahkan ide baru ke sesi.
[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);
}

Hindari mengembalikan entitas domain bisnis secara langsung melalui panggilan API. Entitas domain:

  • Sering kali menyertakan lebih banyak data daripada yang dibutuhkan klien.
  • Jangan menggandengkan model domain internal aplikasi dengan API yang diekspos ke publik secara tidak perlu.

Pemetaan antara entitas domain dan jenis yang dikembalikan ke klien dapat dilakukan:

Selanjutnya, aplikasi sampel menunjukkan pengujian unit untuk metode API Create dan ForSession dari pengontrol Ideas.

Aplikasi sampel berisi dua ForSession pengujian. Pengujian pertama menentukan apakah ForSession mengembalikan NotFoundObjectResult (HTTP Not Found) untuk sesi yang tidak valid:

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

Pengujian kedua ForSession menentukan apakah ForSession mengembalikan daftar ide sesi (<List<IdeaDTO>>) untuk sesi yang valid. Pemeriksaan juga memeriksa ide pertama untuk mengonfirmasi propertinya Name benar:

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

Untuk menguji perilaku Create metode ketika ModelState tidak valid, aplikasi sampel menambahkan kesalahan model ke pengontrol sebagai bagian dari pengujian. Jangan mencoba menguji validasi model atau pengikatan model dalam pengujian unit—cukup uji perilaku metode tindakan saat dihadapkan dengan yang tidak valid 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);
}

Pengujian Create kedua tergantung pada repositori yang mengembalikan null, sehingga repositori tiruan dikonfigurasi untuk mengembalikan null. Tidak perlu membuat database pengujian (dalam memori atau sebaliknya) dan membuat kueri yang mengembalikan hasil ini. Pengujian dapat dilakukan dalam satu pernyataan, seperti yang diilustrasikan kode sampel:

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

Tes ketiga Create, Create_ReturnsNewlyCreatedIdeaForSession, memverifikasi bahwa metode repositori UpdateAsync dipanggil. Mock dipanggil dengan Verifiable, dan metode Verify pada repositori mock dipanggil untuk mengonfirmasi metode yang dapat diverifikasi dijalankan. Bukan tanggung jawab pengujian unit untuk memastikan bahwa metode UpdateAsync menyimpan data—yang dapat dilakukan dengan pengujian integrasi.

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

Uji ActionResult<T>

Di ASP.NET Core 2.1 atau yang lebih baru, ActionResult<T> (ActionResult<TValue>) memungkinkan Anda mengembalikan jenis yang berasal dari ActionResult atau mengembalikan jenis tertentu.

Aplikasi sampel menyertakan metode yang mengembalikan List<IdeaDTO> untuk sesi idtertentu . Jika sesi id tidak ada, pengontrol mengembalikan 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;
}

Dua pengujian ForSessionActionResult controller disertakan dalam ApiIdeasControllerTests.

Pengujian pertama mengonfirmasi bahwa pengontrol mengembalikan ActionResult tetapi bukan daftar ide yang tidak ada untuk sesi idyang tidak ada :

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

Untuk sesi idyang valid , pengujian kedua mengonfirmasi bahwa metode mengembalikan:

  • Sebuah ActionResult dengan jenis List<IdeaDTO>.
  • ActionResult<T>.Value adalah tipe List<IdeaDTO>.
  • Item pertama dalam daftar adalah ide yang valid yang cocok dengan ide yang disimpan dalam sesi tiruan (diperoleh dengan memanggil 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);
}

Aplikasi sampel juga menyertakan metode untuk membuat Idea baru untuk sesi tertentu. Pengontrol mengembalikan:

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

Tiga pengujian CreateActionResult disertakan dalam ApiIdeasControllerTests.

Teks pertama mengonfirmasi bahwa dikembalikan BadRequest untuk model yang tidak valid.

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

Pengujian kedua memeriksa bahwa sebuah NotFound dikembalikan jika sesi tidak ada.

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

Untuk sesi idyang valid , pengujian akhir mengonfirmasi bahwa:

  • Metode mengembalikan ActionResult dengan tipe BrainstormSession.
  • ActionResult<T>.Result adalah CreatedAtActionResult. CreatedAtActionResult dianalogikan dengan respons 201 Created dengan header Location.
  • ActionResult<T>.Value adalah tipe BrainstormSession.
  • Pemanggilan tiruan untuk memperbarui sesi, UpdateAsync(testSession), telah dipanggil. Panggilan metode Verifiable diperiksa dengan menjalankan mockRepo.Verify() di dalam asersi.
  • Dua Idea objek dikembalikan untuk sesi tersebut.
  • Item terakhir (yang ditambahkan oleh panggilan tiruan ke UpdateAsync pada Idea) cocok dengan yang ditambahkan ke sesi dalam pengujian oleh newIdea.
[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);
}