.netcore持續整合測試篇之測試方法改造

周國通發表於2019-08-15

系列目錄

通過前面兩節講解,我們的測試類中已經有兩個測試方法了,總體上如下

 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物件傳給非同步請求方法.

相關文章