通過前面兩節講解,我們的測試類中已經有兩個測試方法了,總體上如下
public class mvc20
{
private readonly HttpClient _client;
public mvc20()
{
var builder = new WebHostBuilder()
.UseContentRoot(@"E:\personal project\newTest2018\ConsoleApp1\CoreMvc")
.UseEnvironment("Development")
.UseStartup<CoreMvc.Startup>();
var server = new TestServer(builder);
_client = server.CreateClient();
}
[Fact]
public async Task SimpleGet()
{
var response = await _client.GetAsync("/HelloWorld/Hello");
response.EnsureSuccessStatusCode();
var responseStr = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello,World", responseStr);
}
[Theory]
[AutoData]
public async Task SimplePost(Student stud)
{
var content = new StringContent(JsonConvert.SerializeObject(stud), Encoding.UTF8, "application/json");
var response = await _client.PostAsync("/HelloWorld/StudentInfo", content);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync();
Assert.True(!string.IsNullOrEmpty(result));
}
}
改進一:將對象初始化移到外部類中
以上方法看似沒有問題,實際上卻有一個性能陷阱,我們通過前面章節的知識已經知道,xunit里測試類的構造函數會在每一個測試方法運行的時候都執行一遍,通常情況下我們的測試代碼遠不止三幾個,有時候幾十個甚至上百個.這樣每次都創建一個是非常影響性能的.並且這里的TestServer和_client都沒有釋放.此外就是web項目里可能每一個測試類都需要創建這樣一個TestServer,這樣重復的代碼會復制很多次,帶來維護困難.
我們前面講到過,我們如果想要讓一個對象在一個測試類中只初始化一次,就要讓這個類實現IClassFixture泛型接口,類在初始化的時候會自動注入這個泛型對象的實體,並且只初始化一次,如果這個泛型對象實現了IDisposable接口,則會在測試類所有方法都執行完成的時候執行這個對象里的Dispose方法.
首先我們創建一個名為MyTestServerFixtrue
的類,TestServer和HttpClient對象的初始化在這里執行.代碼如下
public class MyTestServerFixtrue:IDisposable
{
public readonly HttpClient _client;
private readonly TestServer _server;
public MyTestServerFixtrue()
{
var builder = new WebHostBuilder()
.UseContentRoot(@"E:\personal project\newTest2018\ConsoleApp1\CoreMvc")
.UseEnvironment("Development")
.UseStartup<CoreMvc.Startup>();
_server = new TestServer(builder);
_client = _server.CreateClient();
}
public void Dispose()
{
_client.Dispose();
_server.Dispose();
}
這里的方法和參數大部分都和前面在測試類中添加的一樣,只是有以下幾點需要注意:
1.把server變量放在構造函數外邊,這樣我們才能在Dispose里把它釋放掉,不然無法定位到它.
2.把client變成public類型,因為我們需要在測試類中訪問它.
下面我們再看測試類改造后的代碼
public class mvc20:IClassFixture<MyTestServerFixtrue>
{
private readonly HttpClient _client;
public mvc20(MyTestServerFixtrue fixtrue)
{
this._client = fixtrue._client;
}
}
這里是主要代碼,首先這個實現了IClassFixture,然后我們把無參構造函數改變成有參的,並且傳入MyTestServerFixtrue類型對象,Xunit會自動注入這個對象,然后我們把這個對象里的httpclient賦值給本類的_client對象,這樣我們就可以在本類中使用它了.
這樣其它的測試類也可以實現IClassFixture<MyTestServerFixtrue>
,如果想要改TestServer的配置只需要在MyTestServerFixtrue類中改就行了.
改進二:固定路由參數
我們看到前面講到的兩個測試方法提交的路徑中都包含"/HelloWorld",它其實匹配控制器名,一般情況下同一個Controller下的方法的測試方法都寫在同一個測試類中.這樣Controller名稱是固定的,我們可以把它單獨抽離出來,只需要Action后面的路由.
我們把測試類改成如下:
public class mvc20:IClassFixture<MyTestServerFixtrue>
{
private readonly HttpClient _client;
public mvc20(MyTestServerFixtrue fixtrue)
{
var baseAddr = fixtrue._client.BaseAddress.AbsoluteUri;
string controllerName ="HelloWorld";
this._client = fixtrue._client;
if (!fixtrue._client.BaseAddress.AbsoluteUri.Contains(controllerName))
{
fixtrue._client.BaseAddress = new Uri(baseAddr + controllerName+"/");
}
}
[Fact]
public async Task SimpleGet()
{
var response = await _client.GetAsync($"{nameof(HelloWorldController.Hello)}");
response.EnsureSuccessStatusCode();
var responseStr = await response.Content.ReadAsStringAsync();
Assert.Equal("Hello,World", responseStr);
}
[Theory]
[AutoData]
public async Task SimplePost(Student stud)
{
var content = new StringContent(JsonConvert.SerializeObject(stud), Encoding.UTF8, "application/json");
var response = await _client.PostAsync($"{nameof(HelloWorldController.StudentInfo)}", content);
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadAsStringAsync();
Assert.True(!string.IsNullOrEmpty(result));
}
}
這里我們把controller的名稱加到HttpClient的BaseUrl里面,然后發送get,post等請求的時候只要Action的名字,這里我們使用nameof關鍵字來獲取action的名字,使用nameof關鍵字來獲取的好處是:第一,我們點擊方法名就可以快速定位到指定的方法.更為重要的是如果方法的名稱改了,編譯的時候就會出現編譯錯誤,我們可以快速定位到錯誤然后修改.
改進三:資源路徑改為相對路徑
上面MyTestServerFixtrue類中的代碼有一處有明顯問題:那就是UseContentRoot里的路徑是寫死的,項目在本機上地址與在服務器上的或者與其它同事的絕大多數情況下是不一樣的(因為大家項目所在的目錄名不相同)這時候如果其它人調用這些代碼就可能會出現錯誤.
我們可以使用相對路徑來獲取絕對路來解決這個問題,由於這兩個項目的主文件夾在同一文件夾下面,因此測試項目向外退若干層就能夠得到mvc項目的主目錄了.
我們將MyTestServerFixtrue
類的構造方法改為如下:
public MyTestServerFixtrue()
{
var rootPath = GetContentRootDir();
var builder = new WebHostBuilder()
.UseContentRoot(rootPath)
.UseEnvironment("Development")
.UseStartup<CoreMvc.Startup>();
_server = new TestServer(builder);
_client = _server.CreateClient();
}
這次我們不是再寫死rootPath而是通過方法GetContentRootDir
來獲取.
下面我們來看這個GetContentRootDir
方法
private string GetContentRootDir()
{
var currentPath = AppDomain.CurrentDomain.BaseDirectory;
var relativePath = @"..\..\..\..\CoreMvc";
var combinedPath = Path.Combine(currentPath, relativePath);
var absPath = Path.GetFullPath(combinedPath);
return absPath;
}
首先我們先獲取當前程序域的目錄,也就是程序的運行目錄,獲取到它之后我們看看向上移動多少層能夠到達包含mvc項目和這個test項目的文件夾,經查是四層,下面的相對路徑我們就寫為如變量relativePath
定義的那樣.
我們把它們組合在一起,然后通過Path.GetFullPath來獲取到相對路徑的絕路徑.
改進四 設置超時
有時候服務器故障會導致請求非常慢,服務器很長時間無法返回請求,這就會導致集成測試代碼一直'卡'着無法完成,這時候可以設置一個超時.設置非常簡單,HttpClient有一個Timeout屬性,設置相應的超時時間即可.HttpClient的默認請求超時時間是100s,這個值應該大部分時候不需要修改的,但是關於具體的業務,可能有一些方法本身執行時間特別長(業務邏輯非常復雜,sql語句非常復雜等)這時候可以單元給本次請求設置一個超時時間.比如說是150s,設置如下
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(150));
var response = await client.GetAsync("/Home/index", cts.Token);
這里定義一個CancellationTokenSource對象,並指定超時時間,然后把此對象的Token對象傳給異步請求方法.