簡體   English   中英

一個單元應該如何測試 .NET MVC 控制器?

[英]How should one unit test a .NET MVC controller?

我正在尋找有關 .NET mvc 控制器有效單元測試的建議。

在我工作的地方,許多這樣的測試使用 moq 來模擬數據層並斷言某些數據層方法被調用。 這對我來說似乎沒什么用,因為它本質上是驗證實現沒有改變,而不是測試 API。

我還閱讀了推薦諸如檢查返回的視圖模型類型是否正確的文章。 我可以看到它提供了一些價值,但僅憑它似乎並不值得編寫多行模擬代碼(我們的應用程序的數據模型非常龐大和復雜)。

誰能建議一些更好的控制器單元測試方法或解釋為什么上述方法有效/有用?

謝謝!

控制器單元測試應該測試您的操作方法中的代碼算法,而不是您的數據層。 這是模擬這些數據服務的原因之一。 控制器期望從存儲庫/服務/等接收某些值,並在從它們接收不同的信息時采取不同的行動。

您編寫單元測試來斷言控制器在非常特定的場景/情況下以非常特定的方式運行。 您的數據層是為控制器/操作方法提供這些情況的應用程序的一部分。 斷言服務方法被控制器調用是有價值的,因為您可以確定控制器從另一個地方獲取信息。

檢查返回的視圖模型的類型是有價值的,因為如果返回錯誤的視圖模型類型,MVC 將拋出運行時異常。 您可以通過運行單元測試來防止這種情況在生產中發生。 如果測試失敗,那么視圖可能會在生產中拋出異常。

單元測試很有價值,因為它們使重構更容易。 您可以更改實現,並通過確保所有單元測試通過來斷言行為仍然相同。

回復評論 #1

如果更改被測方法的實現需要更改/刪除下層模擬方法,那么單元測試也必須更改。 但是,這不應該像您想象的那樣經常發生。

典型的紅綠重構工作流要求在編寫單元測試之前編寫單元測試。 (這意味着在短時間內,您的測試代碼將無法編譯,這也是許多年輕/缺乏經驗的開發人員難以采用紅綠重構的原因。)

如果您首先編寫單元測試,您將知道控制器需要從較低層獲取信息。 您如何確定它會嘗試獲取該信息? 通過模擬提供信息的下層方法,並斷言控制器調用了下層方法。

當我使用“改變實施”這個詞時,我可能說錯了。 當必須更改控制器的操作方法和相應的單元測試以更改或刪除模擬方法時,您實際上是在更改控制器的行為。 根據定義,重構意味着在不改變整體行為和預期結果的情況下改變實現。

紅綠重構是一種質量保證方法,有助於防止代碼中的錯誤和缺陷出現。 通常,開發人員會在錯誤出現后更改實現以將其刪除。 所以重申一下,你擔心的情況不應該像你想象的那樣經常發生。

你應該首先讓你的控制器節食。 然后你就可以愉快地對它們進行單元測試了。 如果它們很胖並且你已經把所有的業務邏輯都塞進它們里面,我同意你會在你的單元測試中傳遞你的生活嘲笑的東西,並抱怨這是浪費時間。

當你談論復雜的邏輯時,這並不一定意味着這個邏輯不能在不同的層中分離並且每個方法都單獨進行單元測試。

是的,您應該一直測試到數據庫。 您投入到模擬中的時間更少,並且您從模擬中獲得的價值也非常少(系統中 80% 的可能錯誤無法通過模擬來選擇)。

當您從控制器一直測試到數據庫或 Web 服務時,它不稱為單元測試,而是集成測試。 我個人相信集成測試而不是單元測試(即使它們都用於不同的目的)。 而且我能夠通過集成測試(場景測試)成功地進行測試驅動開發。

這是我們團隊的工作方式。 開始時的每個測試類都會重新生成數據庫並使用最少的數據集(例如:用戶角色)填充/播種表。 基於控制器的需要,我們填充數據庫並驗證控制器是否完成它的任務。 這樣做的目的是使其他方法留下的 DB 損壞數據永遠不會使測試失敗。 除了運行所需的時間之外,幾乎所有單元測試的質量(即使它是一個理論)都是可以獲得的。 使用容器可以減少順序運行所需的時間。 同樣對於容器,我們不需要重新創建數據庫,因為每個測試都會在容器中獲得自己的新數據庫(將在測試后刪除)。

在我的職業生涯中,只有 2% 的情況(或很少)當我被迫使用模擬/存根時,因為無法創建更真實的數據源。 但在所有其他情況下,集成測試是可能的。

使用這種方法,我們需要時間才能達到成熟的水平。 我們有一個很好的框架來處理測試數據的填充和檢索(一等公民)。 它得到了很大的回報! 第一步是告別模擬和單元測試。 如果模擬沒有意義,那么它們不適合您! 集成測試讓您睡個好覺。

====================================

在下面的評論后編輯:演示

集成測試或功能測試必須直接處理 DB/源。 沒有嘲笑。 所以這些是步驟。 您想測試getEmployee( emp_id) 下面的所有這 5 個步驟都是在一個測試方法中完成的。

  1. 刪除數據庫

  2. 創建數據庫並填充角色和其他基礎設施數據

  3. 創建帶有 ID 的員工記錄

  4. 使用這個ID並調用getEmployee(emp_id) //這可以是一個api-url調用(這樣db連接字符串就不需要在測試項目中維護,我們只需更改域名就可以測試幾乎所有環境)

  5. 現在 Assert()/ 驗證返回的數據是否正確

    這證明getEmployee()有效。 第 3 步之前的步驟要求您擁有僅供測試項目使用的代碼。 第 4 步調用應用程序代碼。 我的意思是創建員工(第 2 步)應該通過測試項目代碼而不是應用程序代碼來完成。 如果有用於創建員工的應用程序代碼(例如: CreateEmployee() ),則不應使用此代碼。 同樣,當我們測試CreateEmployee() 時,不應使用GetEmployee()應用程序代碼。 我們應該有一個用於從表中獲取數據的測試項目代碼。

這樣就沒有嘲笑了! 刪除和創建 DB 的原因是為了防止 DB 有損壞的數據。 使用我們的方法,無論我們運行多少次,測試都會通過。

特別提示:在第 5 步 getEmployee() 返回一個員工對象。 如果稍后開發人員刪除或更改字段名稱,則測試中斷。 如果開發人員稍后添加新字段怎么辦? 他/她忘記為其添加測試(斷言)? 測試不會撿起來。 解決方案是添加字段計數檢查。 例如:Employee 對象有 4 個字段(First Name、Last Name、Designation、Sex)。 因此員工對象的Assert 字段數為4。所以當添加新字段時,我們的測試會因為計數失敗並提醒開發人員為新添加的字段添加一個assert 字段。

這是一篇很棒的文章,討論了集成測試相對於單元測試的好處,因為“單元測試會殺死!” (它說)

單元測試的重點是根據一組條件單獨測試方法的行為。 您使用模擬設置測試的條件,並通過檢查它如何與周圍的其他代碼交互來斷言方法的行為——通過檢查它嘗試調用哪些外部方法,但特別是通過檢查給定條件下它返回的值。

因此,對於返回 ActionResults 的 Controller 方法,檢查返回的 ActionResult 的值非常有用。

查看此處的“為控制器創建單元測試”部分,了解使用 Moq 的一些非常清晰的示例

這是來自該頁面的一個很好的示例,它測試當控制器嘗試創建聯系人記錄但失敗時返回適當的視圖。

[TestMethod]
public void CreateInvalidContact()
{
    // Arrange
    var contact = new Contact();
    _service.Expect(s => s.CreateContact(contact)).Returns(false);
    var controller = new ContactController(_service.Object);

    // Act
    var result = (ViewResult)controller.Create(contact);

    // Assert
    Assert.AreEqual("Create", result.ViewName);
}

我認為對控制器進行單元測試沒有多大意義,因為它通常只是連接其他部分的一段代碼。 單元測試通常包括大量模擬,只是驗證其他服務是否正確連接。 測試本身是實現代碼的反映。

我更喜歡集成測試——我不是從一個具體的控制器開始,而是從一個 Url 開始,並驗證返回的模型是否具有正確的值。 Ivonna的幫助下,測試可能如下所示:

var response = new TestSession().Get("/Users/List");
Assert.IsInstanceOf<UserListModel>(response.Model);

var model = (UserListModel) response.Model;
Assert.AreEqual(1, model.Users.Count);

我可以模擬數據庫訪問,但我更喜歡不同的方法:設置 SQLite 的內存實例,並在每個新測試中重新創建它,以及所需的數據。 它使我的測試足夠快,但不是復雜的模擬,而是讓它們清楚,例如,只需創建並保存一個 User 實例,而不是模擬UserService (這可能是一個實現細節)。

通常,當您談論單元測試時,您正在測試一個單獨的過程或方法,而不是整個系統,同時試圖消除所有外部依賴項。

換句話說,在測試控制器時,您正在逐個編寫測試方法,您甚至不需要加載視圖或模型,這些是您應該“模擬”的部分。 然后,您可以更改模擬以返回在其他測試中難以重現的值或錯誤。

我通常遵循ASP.NET Core本指南:

https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/testing?view=aspnetcore-5.0

代碼示例:

https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/mvc/controllers/testing/samples/

例子:

控制器:

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

單元測試:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM