首次在WebAPI中寫單元測試

ggtc發表於2024-08-16

xUnit

這次我使用的是xUnit測試框架,而不是VS自帶的MSTest框架。在新增新建專案時選擇xUnit測試專案就行了。

目前只體驗到了一個差別,即xUnit可以使用特性向測試方法傳參,而不用在測試方法中一個賦值語句一個個去定義引數,這是比較方便的。

單元測試有一個好處,就是一次性可以獲得所測試的很多介面的失敗資訊。如果使用swagger去測試介面,只能去啟動專案,輸入密碼鑑權,然後一個個發請求。遇到一個錯誤處理一個介面,比較麻煩。單元測試可以把所有介面的報錯資訊展示在測試視窗,而且是快取的,不用啟動專案,只需要點選一下測試按鈕,就把所有介面測試了。我在遷移介面時,有一個控制器一次性遷移了14個介面,單元測試透過了6個,失敗了8個,失敗的都列出了錯誤訊息,這就很舒服了。經過了幾天的使用,我發現這比到swagger或postman中手動測試介面方便太多了。

image

這裡的失敗基本都是遷移資料庫結構引起的,我申請修改結構後,就又有幾個透過了測試。修改進度如何,看起來很直觀。到目前為止幾天了,測試仍然沒有全部透過,單元測試起到了很好的監控作用。

image

點選失敗的測試,可以看到呼叫堆疊,跳轉到執行失敗的那一行程式碼。這使得修改起來很方便。

單元測試環境準備

我寫的是控制器方法中action的單元測試。但是一般來說,控制器和Service層會注入許多服務,而action依賴於這些服務。在使用依賴注入時,單元測試要如何處理這種情況?

可以和ASP.NET core的做法一樣。它準備了一個依賴注入容器,那我也準備一個依賴注入容器。WebAPI還構造了一個web主機。但是單元測試是獨立執行的,就不需要建立一個web主機了。在單元測試專案種新增了一個TestBase基類,用於建立容器,註冊服務,以供測試方法使用。

//測試環境
public class TestBase
{
	//依賴注入容器
	public IServiceCollection Services;
	//從容器獲取服務
	public IServiceProvider Provider;

	public TestBase()
	{
		//建立容器
		Services = new ServiceCollection();
		//....註冊服務
		Provider = Services.BuildServiceProvider();
	}
}

然後向容器註冊我們需要的服務,比如常見的MemoryCache IWebHostEnvironment ISqlSugarClient XXXService。我們就不需要在測試方法中使用new運算子建立服務類例項,而是可以直接從容器中獲得了。

//註冊服務層
Services.AddScoped<ISingleWellService, SingleWellService>();
//註冊快取
Services.AddScoped<IMemoryCache, MemoryCache>(service =>
{
	return new MemoryCache(new MemoryCacheOptions());
});
//註冊SqlSuger
Services.AddScoped<ISqlSugarClient>(service =>
{
	return new SqlSugarClient(new ConnectionConfig()
	{
		ConnectionString = "Data Source=XXX",
		DbType = DbType.Oracle,
		IsAutoCloseConnection = true,
		InitKeyType = InitKeyType.Attribute,
	});
});
//註冊環境變數
Services.AddScoped<IWebHostEnvironment, WebHostEnvironment>();

IWebHostEnvironment

為了使用這個介面需要引入包Microsoft.Extensions.DependencyInjection.Abstractions。這個介面一般是WebApplicationBuilder建立的,在涉及檔案讀寫時經常用到。但是單元測試專案中沒有builder,我就自己新增了一個IWebHostEnvironment實現類,並註冊到容器中。缺點是還要把可能需要的WebAPI專案中的檔案,比如模板檔案、資料檔案也複製到單元測試專案中。

public class WebHostEnvironment : IWebHostEnvironment
    {
        public string WebRootPath { get => Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "wwwroot");  }
        public IFileProvider WebRootFileProvider { get; set; }
        public string EnvironmentName { get; set; }
        public string ApplicationName { get; set; }
        public string ContentRootPath { get => AppDomain.CurrentDomain.BaseDirectory; }
        public IFileProvider ContentRootFileProvider { get; set; }
    }

ICurentUser

這個自定義介面一般是在請求處理管道中儲存身份驗證後的相關資訊。單元測試中不同的介面可能需要不同的user,比如一個流程中,不同角色呼叫同一個介面。ICurentUser同樣也是註冊到容器中的,然後在service層注入。具體的業務方法中根據這個角色的不同執行不同邏輯。

我比較疑惑的是,單元測試又該怎麼注入呢?要注意的是,不同測試方法的ICurentUser是不同的。要知道從容器中獲取service,容器自動幫我們挑選了合適夠構造方法。但是這裡由於角色的不同,不能直接從容器獲取準備好的角色存根。難道我們要手動構造service傳入controller中嗎?我是有聽說mokq,但不知道怎麼用來模擬多個ICurentUser。

為控制器新增單元測試

新增一個HomeControllerUnitTest類,並繼承於前面定義的基類TestBase。我們應該在建構函式中從容器取出相應的服務以供使用。

public class HomeControllerUnitTest:TestBase
    {
        HomeController homeController;
        public HomeControllerUnitTest()
        {
			//從容器注入服務
            homeController = new HomeController(
				Provider.GetRequiredService<IHomeService>(), 
				Provider.GetRequiredService<IWebHostEnvironment>());
        }
}

接著,向測試類新增單元Action的測試方法。一般都是三步

  • 準備資料 Arrange
  • 呼叫測試方法 Act
  • 斷言結果 Assert

也就是AAA模式。

[Theory(DisplayName = "測試XXX")]
[InlineData("xxx", "xxx", 1,30)]
public void Test_GetData(string wId, string tId, int page, int rows)
{
	var data = homeController.GetData(wId, tId, page, rows).Result;
	Assert.True(data.Tag);
}

在實際使用時,我會每新增一個測試,就執行未執行的測試。頭一天看一下哪些測試沒透過,這裡一般是資料庫結構不對,然後申請修改資料庫。第二天再執行失敗的測試驗證。不會每次執行全部測試。

相關文章