學習ASP.NET Core(10)-全域性日誌與xUnit系統測試
阿新 • • 發佈:2020-06-12
上一篇我們介紹了資料塑形,HATEOAS和內容協商,並在制器方法中完成了對應功能的新增;本章我們將介紹日誌和測試相關的概念,並新增對應的功能
## 一、全域性日誌
在第一章介紹專案結構時,有提到.NET Core啟動時預設載入了日誌服務,且在appsetting.json檔案配置了一些日誌的設定,根據設定的日誌等級的不同可以進行不同級別的資訊的顯示,但它無法做到輸出固定格式的log資訊至本地磁碟或是資料庫,所以需要我們自己手動實現,而我們可以藉助日誌框架實現。
ps:在第7章節中我們記錄的是資料處理層方法呼叫的日誌資訊,這裡記錄的則是ASP.NET Core WebAPI層級的日誌資訊,兩者有所差異
### 1、引入日誌框架
.NET程式中常用的日誌框架有log4net,serilog 和Nlog,這裡我們使用Serilog來實現相關功能,在BlogSystem.Core層使用NuGet安裝**Serilog.AspNetCore**,同時還需要搜尋**Serilog.Skins**安裝希望支援的功能,這裡我們希望新增對檔案和控制檯的輸出,所以選擇安裝的是**Serilog.Skins.File和Serilog.Skins.Console**
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212247677-857161630.png)
需要注意的是Serilog是不受appsetting.json的日誌設定影響的,且它可以根據名稱空間重寫記錄級別。還有一點需要注意的是**需要手動對Serilog物件進行資源的釋放**,否則在系統執行期間,無法開啟日誌檔案。
### 2、系統新增
在BlogSystem.Core專案中新增一個Logs資料夾,並在Program類中進行Serilog物件的新增和使用,如下:
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212257028-1594324462.png)
### 3、全域性新增
1、這個時候其實系統已經使用Serilog替換了系統自帶的log物件,如下圖,Serilog會根據相關資訊進行高亮顯示:
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212307700-547604430.png)
2、這個時候問題就來了,我們怎麼才能進行全域性的新增呢,總不能一個方法一個方法的新增吧?還記得之前我們介紹AOP時提到的過濾器Filter嗎?ASP.NET Core中一共有五類過濾器,分別是:
- 授權過濾器**Authorization Filter**:優先順序最高,用於確定使用者是否獲得授權。如果請求未被授權,則授權過濾器會使管道短路;
- 資源過濾器**Resource Filter**:授權後執行,會在Authorization之後,Model Binding之前執行,可以實現類似快取的功能;
- 方法過濾器**Action Filter**:在控制器的Action方法執行之前和之後被呼叫,可以更改傳遞給操作的引數或更改從操作返回的結果;
- 異常過濾器**Exception Filter**:當Action方法執行過程中出現了未處理的異常,將會進入這個過濾器進行統一處理;
- 結果過濾器**Result Filter**:執行操作結果之前和之後執行,僅在action方法成功執行後才執行;
過濾器的具體執行順序如下:
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212328882-714249212.png)
3、這裡我們可以藉助異常過濾器實現全域性日誌功能的新增;在在BlogSystem.Core專案新增一個Filters資料夾,新增一個名為ExceptionFilter的類,繼承IExceptionFilter介面,這裡是參考[老張的哲學](https://www.cnblogs.com/laozhang-is-phi/p/9547574.html "點選檢視")的簡化版本,實現如下:
```c#
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Serilog;
using System;
namespace BlogSystem.Core.Filters
{
public class ExceptionsFilter : IExceptionFilter
{
private readonly ILogger _logger;
public ExceptionsFilter(ILogger logger)
{
_logger = logger;
}
public void OnException(ExceptionContext context)
{
try
{
//錯誤資訊
var msg = context.Exception.Message;
//錯誤堆疊資訊
var stackTraceMsg = context.Exception.StackTrace;
//返回資訊
context.Result = new InternalServerErrorObjectResult(new { msg, stackTraceMsg });
//記錄錯誤日誌
_logger.LogError(WriteLog(context.Exception));
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
finally
{
//記得釋放,否則執行時無法開啟日誌檔案
Log.CloseAndFlush();
}
}
//返回500錯誤
public class InternalServerErrorObjectResult : ObjectResult
{
public InternalServerErrorObjectResult(object value) : base(value)
{
StatusCode = StatusCodes.Status500InternalServerError;
}
}
//自定義格式內容
public string WriteLog(Exception ex)
{
return $"【異常資訊】:{ex.Message} \r\n 【異常型別】:{ex.GetType().Name} \r\n【堆疊呼叫】:{ex.StackTrace}";
}
}
}
```
4、在Startup類的ConfigureServices方法中進行異常處理過濾器的註冊,如下:
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212343952-989056936.png)
5、我們在控制器方法中丟擲一個異常,分別檢視效果如下,如果覺得資訊太多,可調整日誌記錄級別:
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212354856-1733069518.png)
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212400448-1805842045.png)
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212405653-322950388.png)
## 二、系統測試
這裡我們從測試的類別出發,瞭解下測試相關的內容,並新增相關的測試(介紹內容大部分來自微軟官方文件,為了更易理解,從個人習慣的角度進行了修改,如有形容不當之處,可在評論區指出)
### 1、測試說明及分類
1、自動測試是確保軟體應用程式按照作者期望執行操作的一種絕佳方式。軟體應用有多種型別的測試,包括單元測試、整合測試、Web測試、負載測試和其他測試。單元測試用於測試個人軟體的元件或方法,並不包括如資料庫、檔案系統和網路資源類的基礎結構測試。
當然我們可以使用編寫測試的最佳方法,如測試驅動開發(TDD)所指的先編寫單元測試,再編寫該單元測試要檢查的程式碼,就好比先編寫書籍的大綱,再編寫書籍。其主要目的是為了幫助開發人員編寫更簡單,更具可讀性的高效程式碼。兩者區別如下(來自[Edison Zhou](https://www.cnblogs.com/edisonchou/p/5437205.html "點選檢視"))
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212751646-2080065075.png)
2、以深度(測試的細緻程度)和廣度(測試的覆蓋程度)區分, 測試分類如下(此處內容來自[solenovex](https://www.cnblogs.com/cgzl/p/8283610.html "點選檢視")):
![](https://img2020.cnblogs.com/blog/2019059/202006/2019059-20200611212801954-1667682109.png)
**Unit Test 單元測試**:它可以測試一個類或者一個類的某個功能,但其覆蓋程度較低;
**Integration Test 整合測試**:它的細緻程度沒有單元測試高,但是有較好的覆蓋程度,它可以測試功能的組合,以及像資料庫或檔案系統這樣的外部資源;
**Subcutaneous Test 皮下測試** :其作用區域為UI層的下一層,有較好的覆蓋程度,但是深度欠佳;
**UI測試**:直接從UI層進行測試,覆蓋程度很高,但是深度欠佳
3、在編寫單元測試時,儘量不要引入基礎結構依賴項,這些依賴項會降低測試速度,使測試更加脆弱,我們應當將其保留供整合測試使用。可以通過遵循顯示依賴項原則和使用依賴項注入避免應用程式中的這些依賴項,還可以將單元測試保留在單獨的專案中與整合測試相分離,以確保單元測試專案沒有引用或依賴於基礎結構包。
總結下常用的單元測試和整合測試,單元測試會與外部資源隔離,以保證結果的一致性;而整合測試會依賴外部資源,且覆蓋面更廣。
### 2、測試的目的及特徵
1、為什麼需要測試?我們從以單元測試為例從4個方面進行說明:
- **時間人力成本**:進行功能測試時,通常涉及開啟應用程式,執行一系列需要遵循的步驟來驗證預期的行為,這意味著測試人員需要了解這些步驟或聯絡熟悉該步驟的人來獲取結果。對於細微的更改或者是較大的更改,都需要重複上述過程,而單元測試只需要按一下按鈕即可執行,無需測試人員瞭解整個系統,測試結果也取決於測試執行程式而非測試人員。
- **防止錯誤迴歸**:程式更改後有時會出現舊功能異常的問題,所以測試時不僅要測試新功能還要確保舊功能的正常執行。而單元測試可以確保在更改一行程式碼後重新執行整套測試,確保新程式碼不會破壞現有的功能。
- **可執行性**:在給定某個輸入的情況下,特定方法的作用或行為可能不會很明顯。比如,輸入或傳遞空白字串、null後,該方法會有怎樣的行為?而當我們使用一套命名正確的單元測試,並清楚的解釋給定的輸入和預期輸出,那麼它將可以驗證其有效性。
- **減少程式碼耦合**:當代碼緊密耦合時,會難以進行單元測試,所以以建立單元測試為目的時,會在一定程度上要求我們注意程式碼的解耦
2、優質的測試需要符合哪些特徵,同樣以單元測試為例:
- **快速:**成熟的專案會進行數千次的單元測試,所以應當花費非常少的時間來執行單元測試,一般來說在幾毫秒
- **獨立:**單元測試應當是獨立的,可以單獨執行,不依賴檔案系統或資料庫等外部因素
- **可重複:**單元測試的結果應當保持一致,即執行期間不進行更改,返回的結果應該相同
- **自檢查:**測試應當在沒有人工互動的情況下,自動檢測是否通過
- **及時:**編寫單元測試不應該花費過多的時間,如果花費時間較長,應當考慮另外一種更易測試的設計
在具體的執行時,我們應當遵循一些最佳實踐規則,具體請參考微軟官方文件[單元測試最佳做法](https://docs.microsoft.com/zh-cn/dotnet/core/testing/unit-testing-best-practices "點選檢視")
### 3、xUnit框架介紹
常用的單元測試框架有**MSTest**、**xUnit**和**NUnit**,這裡我們以**xUnit**為例進行相關的說明
#### 3.1、測試操作
首先我們要明確如何編寫測試程式碼,一般來說,測試分為三個主要操作:
- **Arrange**:意為安排或準備,這裡可以根據需求進行物件的建立或相關的設定;
- **Act**:意為操作,這裡可以執行獲取生產程式碼返回的結果或者是設定屬性;
- **Assert**:意為斷言,這裡可以用來判斷某些項是否按預期進行,即測試通過還是失敗
#### 3.2、Assert型別
Assert時通常會對不同型別的返回值進行判斷,而在xUnit中是支援多種返回值型別的,常用的型別如下:
**boolean:針對方法返回值為bool的結果**,可以判斷結果是true或false
**string:針對方法返回值為string的結果**,可以判斷結果是否相等,是否以某字串開頭或結尾,是否包含某些字元,並支援正則表示式
**數值型:針對方法返回值為數值的結果**,可以判斷數值是否相等,數值是否在某個區間內,數值是否為null或非null
**Collection:針對方法返回值為集合的結果**,可以針對集合內所有元素或至少一個元素判斷其是否包含某某字元,兩個集合是否相等
**ObjectType:針對方法返回值為某種型別的情況**,可以判斷是否為預期的型別,一個類是否繼承於另一個類,兩個類是否為同一例項
**Raised event:針對事件是否執行的情況**,可以判斷方法內部是否執行了預期的事件
#### 3.3、常用特性
在xUnit中還有一些常用的特性,可作用於方法或類,如下:
**[Fact]:**用來標註該方法為測試方法
**[Trait("Name","Value")]**:用來對測試方法進行分組,支援標註多個不同的組名
**[Fact(Skip="忽略說明...")]**:用來修飾需要忽略測試的方法
#### 3.4 、效能相關
在測試時我們應當注意效能上的問題,針對一個物件供多個方法使用的情況,我們可以使用共享上下文
- 針對一個物件供同一類中的多個方法使用時,可以將該物件提取出來,使用**IClassFixture**物件將其注入到建構函式中
- 針對一個物件供多個測試類使用的情況,可以使用**ICollectionFixture**物件和[CollectionDefinition("...")]定義該物件
需要注意在使用**IClassFixture**和**ICollectionFixture**物件時應當避免多個測試方法之間相互影響的情況
#### 3.5、資料驅動測試
在進行測試方法時,通常我們會指定輸入值和輸出值,如希望多測試幾種情況,我們可以定義多個測試方法,但這顯然不是一個最佳的實現;在合理的情況下,我們可以將引數和資料分離,如何實現?
- 方法一:使用**[Theory]**替換[Fact],將輸入輸出引數提取為方法引數,並使用多個[InlineData("輸入引數","輸出引數)]來標註方法
- 方法二:使用**[Theory]**替換[Fact],針對測試方法新增一個測試資料類,該類包含一個靜態屬性IEumerable