Logika pengontrol pengujian unit di ASP.NET Core

Oleh Steve Smith

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

Pengontrol pengujian unit

Siapkan pengujian unit tindakan pengontrol untuk fokus pada perilaku pengontrol. 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 dalam isolasi, bukan sebagai bagian dari pengujian pada tindakan pengontrol tertentu.

Untuk menunjukkan pengujian unit pengontrol, tinjau pengontrol berikut di aplikasi sampel.

Melihat atau mengunduh kode sampel (cara mengunduh)

Pengontrol Home menampilkan daftar sesi curah otak dan memungkinkan pembuatan sesi curah otak baru dengan 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));
    }
}

Pengontrol sebelumnya:

  • Mengikuti Prinsip Dependensi Eksplisit.
  • Mengharapkan injeksi dependensi (DI) untuk memberikan instans .IBrainstormSessionRepository
  • Dapat diuji dengan layanan tiruan IBrainstormSessionRepository menggunakan kerangka kerja objek tiruan, seperti Moq. Objek yang ditiru 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. Pengujian unit untuk tindakan ini:

  • Mengejek IBrainstormSessionRepository layanan menggunakan metode .GetTestSessions GetTestSessions membuat dua sesi curah gagasan tiruan dengan tanggal dan nama sesi.
  • Index Menjalankan metode .
  • Membuat pernyataan pada 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 Home pengontrol HTTP POST Index menguji bahwa:

  • Ketika ModelState.IsValid adalah false, metode tindakan mengembalikan PermintaanViewResult Buruk 400 dengan data yang sesuai.
  • Kapan 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, hal yang sama ViewResult dikembalikan seperti untuk permintaan GET. Pengujian tidak mencoba untuk lulus dalam model yang tidak valid. Melewati 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 tindakan.

Pengujian kedua memverifikasi bahwa ketika ModelState valid:

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

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

Catatan

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 akhir return mengembalikan yang baru StormSessionViewModel 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 tindakan Pengontrol Index sesi:

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

Pindah ke pengontrol Ideas, aplikasi mengekspos fungsionalitas sebagai API web pada api/ideas rute:

  • Daftar ide (IdeaDTO) yang terkait dengan sesi curah gagasan dikembalikan oleh ForSession metode .
  • Metode ini Create 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.
  • Tidak perlu menggabungkan model domain internal aplikasi dengan API yang diekspos secara publik.

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

Selanjutnya, aplikasi sampel menunjukkan pengujian unit untuk Create metode API dan ForSession 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);
}

Pengujian ketiga Create , Create_ReturnsNewlyCreatedIdeaForSession, memverifikasi bahwa metode repositori UpdateAsync dipanggil. Tiruan dipanggil dengan Verifiable, dan metode repositori Verify yang ditiru dipanggil untuk mengonfirmasi metode yang dapat diverifikasi dijalankan. Bukan tanggung jawab pengujian unit untuk memastikan bahwa UpdateAsync metode 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 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 pengontrol 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:

  • Dengan ActionResultList<IdeaDTO> jenis.
  • ActionResult <T>. Nilai adalah List<IdeaDTO> jenis.
  • 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 baru Idea 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 BrainstormSession jenis.
  • ActionResult <T>. Hasilnya adalah CreatedAtActionResult. CreatedAtActionResult dianalogikan dengan respons 201 Dibuat dengan Location header.
  • ActionResult <T>. Nilai adalah BrainstormSession jenis.
  • Panggilan tiruan untuk memperbarui sesi, UpdateAsync(testSession), dipanggil. Panggilan Verifiable metode diperiksa dengan menjalankan mockRepo.Verify() dalam pernyataan.
  • Dua Idea objek dikembalikan untuk sesi tersebut.
  • Item terakhir (yang Idea ditambahkan oleh panggilan tiruan ke UpdateAsync) cocok dengan yang newIdea ditambahkan ke sesi dalam pengujian.
[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);
}

Pengontrol memainkan peran terpusat dalam aplikasi MVC Core ASP.NET apa pun. Dengan demikian, Anda harus memiliki keyakinan bahwa pengontrol bereaksi seperti yang diinginkan. Pengujian otomatis dapat mendeteksi kesalahan sebelum aplikasi disebarkan ke lingkungan produksi.

Melihat atau mengunduh kode sampel (cara mengunduh)

Pengujian unit logika pengontrol

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

Siapkan pengujian unit tindakan pengontrol untuk fokus pada perilaku pengontrol. 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 dalam isolasi, bukan sebagai bagian dari pengujian pada tindakan pengontrol tertentu.

Untuk menunjukkan pengujian unit pengontrol, tinjau pengontrol berikut di aplikasi sampel. Pengontrol Home menampilkan daftar sesi curah otak dan memungkinkan pembuatan sesi curah otak baru dengan 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));
    }
}

Pengontrol sebelumnya:

  • Mengikuti Prinsip Dependensi Eksplisit.
  • Mengharapkan injeksi dependensi (DI) untuk memberikan instans .IBrainstormSessionRepository
  • Dapat diuji dengan layanan tiruan IBrainstormSessionRepository menggunakan kerangka kerja objek tiruan, seperti Moq. Objek yang ditiru 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. Pengujian unit untuk tindakan ini:

  • Mengejek IBrainstormSessionRepository layanan menggunakan metode .GetTestSessions GetTestSessions membuat dua sesi curah gagasan tiruan dengan tanggal dan nama sesi.
  • Index Menjalankan metode .
  • Membuat pernyataan pada 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 Home pengontrol HTTP POST Index menguji bahwa:

  • Ketika ModelState.IsValid adalah false, metode tindakan mengembalikan PermintaanViewResult Buruk 400 dengan data yang sesuai.
  • Kapan 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, hal yang sama ViewResult dikembalikan seperti untuk permintaan GET. Pengujian tidak mencoba untuk lulus dalam model yang tidak valid. Melewati 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 tindakan.

Pengujian kedua memverifikasi bahwa ketika ModelState valid:

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

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

Catatan

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 akhir return mengembalikan yang baru StormSessionViewModel 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 tindakan Pengontrol Index sesi:

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

Pindah ke pengontrol Ideas, aplikasi mengekspos fungsionalitas sebagai API web pada api/ideas rute:

  • Daftar ide (IdeaDTO) yang terkait dengan sesi curah gagasan dikembalikan oleh ForSession metode .
  • Metode ini Create 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.
  • Tidak perlu menggabungkan model domain internal aplikasi dengan API yang diekspos secara publik.

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

Selanjutnya, aplikasi sampel menunjukkan pengujian unit untuk Create metode API dan ForSession 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);
}

Pengujian ketiga Create , Create_ReturnsNewlyCreatedIdeaForSession, memverifikasi bahwa metode repositori UpdateAsync dipanggil. Tiruan dipanggil dengan Verifiable, dan metode repositori Verify yang ditiru dipanggil untuk mengonfirmasi metode yang dapat diverifikasi dijalankan. Bukan tanggung jawab pengujian unit untuk memastikan bahwa UpdateAsync metode 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 pengontrol 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:

  • Dengan ActionResultList<IdeaDTO> jenis.
  • ActionResult <T>. Nilai adalah List<IdeaDTO> jenis.
  • 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 baru Idea 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 BrainstormSession jenis.
  • ActionResult <T>. Hasilnya adalah CreatedAtActionResult. CreatedAtActionResult dianalogikan dengan respons 201 Dibuat dengan Location header.
  • ActionResult <T>. Nilai adalah BrainstormSession jenis.
  • Panggilan tiruan untuk memperbarui sesi, UpdateAsync(testSession), dipanggil. Panggilan Verifiable metode diperiksa dengan menjalankan mockRepo.Verify() dalam pernyataan.
  • Dua Idea objek dikembalikan untuk sesi tersebut.
  • Item terakhir (yang Idea ditambahkan oleh panggilan tiruan ke UpdateAsync) cocok dengan yang newIdea ditambahkan ke sesi dalam pengujian.
[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);
}

Sumber Daya Tambahan: