.netcore持續集成測試篇之MVC層單元測試

系列目錄html

前面咱們講的不少單元測試的的方法和技巧不管是在.net core和.net framework裏面都是通用的,可是mvc項目裏有一種比較特殊的類是Controller,首先Controller類的返回結果跟普通的類並不同,普通的類返回的都是肯定的類型,而mvc項目的返回的ActionResult或者core mvc裏返回的IActionResult則是一個高度封裝的對象,想對它進行很細緻的測試並非一件很容易的事.所以在編寫代碼的時候建議儘可能把業務邏輯的代碼單元寫到單獨類中,Controller裏只進行簡單的前端請求參數檢驗以及各自http狀態和數據的返回.還有一點就是Controller是在http請求到達後動態建立的,單元測試的時候不少對象諸如Httpcontext,Modelstate,request,response,routedata,uri,MetadataProvider等都是不存在的,和在http請求環境中有很大差異.可是咱們仍然能經過對Controller進行單元測試作不少工做,確保結果是咱們想要的.前端

確保Action返回正確View和ViewModel

咱們使用HomeController裏面的Index方法,代碼稍做修改後端

public IActionResult Index()
        {
            return View("Index","hello");
        }

它的測試代碼以下瀏覽器

[Fact]
        public void ViewTest()
        {
            HomeController hc = new HomeController();
            var result = (ViewResult)hc.Index();
            var viewName = result.ViewName;
            var model = (string)result.Model;
            Assert.True(viewName == "Index" && model == "hello");
        }

首先咱們先建立一個Controller類,因爲業務上咱們須要這個方法返回一個View,這是提早預知的,因此咱們把hc.Index的結果轉爲ViewResult,若是轉換失敗則說明程序中存在bug.mvc

下面是分別獲取View的名稱的數據模型,而後咱們斷言View名稱是Index,model的值是hello,固然以上代碼比較簡單顯然是能經過的,在實際業務中咱們還要對Model進行更爲複雜的斷言.async

須要注意的是,Action返回的view並非都有名稱的,若是是返回的本方法對應的view,默認名稱是能夠省略的,這樣以上斷言就會失敗,所以若是名稱不寫的時候咱們能夠斷言ViewName是空,一樣返回的是本方法默認的view.ide

確保Action返回了正確的viewData

咱們把HomeController裏的Index方法再稍改下以下:佈局

public IActionResult Index()
        {
            ViewBag.name = "sto";
            return View("Index","hello");
        }

測試方法以下單元測試

HomeController hc = new HomeController();
            var name= result.ViewData["name"];
            Assert.True(name=="sto");

看到以上有些同事可能會有疑惑,爲何設置的是ViewBag而能用ViewData獲取到呢,不少都從網上看到過有人說兩者一個是dynamic類型,一個是字典類型,這只是它們外在的表現,其實才者運行時是同一個對象.因此能夠經過ViewData[xxx]方式獲取到它的值.測試

確保程序進入的正確的分支

咱們經常會看到以下代碼

public IActionResult Index(Student stud)
        {
            if (!ModelState.IsValid) return BadRequest();
            return View("Index","hello");
        }

Student類咱們加上註解,改爲以下

public class Student
    {
        public string Name { get; set; }
        [Range(3,10,ErrorMessage ="年齡必須在三到十歲之間")]
        public int Age { get; set; }
        public byte Gender { get; set; }
        public string School { get; set; }
    }

咱們對年齡進行註解,標識它必須是3到10之間的一個值.

咱們編寫如下測試來測試若是若是有模型綁定錯誤的時候返回 BadRequest

[Fact]
        public async Task ViewTest()
        {
            HomeController hc = new HomeController();
            var result = hc.Index(new Student{Age=1});
            Assert.IsType<BadRequestResult>(result);
        }

以上測試咱們把stud的年齡設置爲1,根據程序邏輯它不在3到10之間,所以應該返回BadRequest(其實是一個BadRequestResult類型對象),然而運行以上測試會發現測試並無經過,經過單步調試咱們發現實際上返回的是一個ViewResult對象.爲何會是這樣呢?其實緣由很簡單,由於Modelstate.IsValid是在模型綁定的時候若是模型驗證有錯誤,就會寫稿Modelstate對象裏,然而控制器並非動態建立的,模型數據也不是動態綁定的,沒有向Modelstate裏添加錯誤信息的動做,因此單元測試裏它啓動返回True,那是否是就沒有辦法測試了呢,其實也不是,由於ModelState不只程序能夠在模型綁定的時候動態添加,咱們也能夠在控制器裏面根據本身的業務邏輯添加.

咱們把代碼改成以下

[Fact]
        public async Task ViewTest()
        {
            HomeController hc = new HomeController();
            hc.ModelState.AddModelError("Age", "年齡不在3到10範圍內");
            var result = hc.Index(new Student{Age=1});
             Assert.IsType<BadRequestResult>(result);
        }

因爲咱們知道這裏的Age值是不合法的,所以顯式在controller的Modelstate對象裏顯式寫入一個錯誤,這樣Model.Isvalid就應該返回False,邏輯應該走入BadRequest裏.以上測試經過.

確保程序重定向到正確Action

咱們把Index方法改成以下

public IActionResult Index(int? id)
        {
            if (!id.HasValue) return RedirectToAction("Contact","Home");
            return View("Index","hello");
        }

若是id爲null的時候,就會返回一個RedirectToActionResult,導到Home控制器下的Contact方法下.

[Fact]
        public async Task ViewTest()
        {
            HomeController hc = new HomeController();
            var result = hc.Index(null);
            var redirect = (RedirectToActionResult) result;
            var controllerName = redirect.ControllerName;
            var actionName = redirect.ActionName;
            Assert.True(controllerName == "Home" && actionName == "Contact");
        }

固然以上的代碼並非頗有意義,由於RediRectToAction裏面傳入的參數每每是兩個字符串,並不須要特別複雜的計算,而redirect.ControllerName,redirect.ActionName獲取的也並非真正控制器的Action的名稱,而是上面方法賦值來的.所以它們的值老是相等.

咱們能夠經過如下改造來使測試變得更有意義

[Fact]
        public async Task ViewTest()
        {
            HomeController hc = new HomeController();
            var result = hc.Index(null);
            var redirect = (RedirectToActionResult) result;
            var controllerName = redirect.ControllerName;
            var actionName = redirect.ActionName;
            Assert.True(
                controllerName.Equals(nameof(HomeController).GetControllerName(),
                    StringComparison.InvariantCultureIgnoreCase) && actionName.Equals(nameof(HomeController.Contact),
                    StringComparison.InvariantCultureIgnoreCase));
        }

以上代碼咱們使用nameof獲取類型或者方法的名稱,而後判斷手動寫的和經過nameof獲取到的是否是同樣,這樣若是咱們手寫有錯誤就會被發現,可是有一個問題是咱們經過nameof獲取的HomeController的名稱是字符串HomeController而不是Home,其它類型也是如此,可是這個很容易處理,由於它們都是以Controller結尾,咱們只要對它進行一下處理就好了.咱們來看GetControllerName方法,它是一個String類的擴展方法

public static class ControllerNameExtension
    {
        public static string GetControllerName(this string str)
        {
            if (string.IsNullOrWhiteSpace(str) || !str.EndsWith("Controller",StringComparison.InvariantCultureIgnoreCase))
            {
                throw new InvalidOperationException("沒法獲取指定類型的ControllerName");
            }

            string controllerName =
                str.Replace("Controller", string.Empty, StringComparison.InvariantCultureIgnoreCase);
            return controllerName;
        }
    }

這個方法很是簡單,就是把Controller類的結果'Controller'字符串去掉

因爲ControllerFactory在建立Controller的時候是並不區分大小寫的,所以咱們的equals都加上了不區分大小寫的選項,這致使方法看上去特別長,咱們也進行一下簡單封裝.

public static class StringComparisionIgnoreCaseExtension
    {
        public static bool EqualsIgnoreCase(this string str, string other)
        {
            return str.Equals(other, StringComparison.InvariantCultureIgnoreCase);
        }
    }

以上方法很是簡單,就是在比較的時候加上StringComparison.InvariantCultureIgnoreCase

最終Assert的斷言代碼變成以下:

Assert.True(
                controllerName.EqualsIgnoreCase(nameof(HomeController).GetControllerName()) && actionName.EqualsIgnoreCase(nameof(HomeController.Contact)));

這樣若是咱們由於手寫錯誤把名稱拼錯或者多空格就很容易被識別出來,而且若是方法名稱改掉這裏會出現編譯錯誤,方便咱們定位錯誤.

確保程序重定向到正確路由

有些時候咱們重定向到指定路由,下面看看如何測試

public IActionResult Index(int? id)
        {
            if (!id.HasValue) return RedirectToRoute(new{controller="Home",action="Contact"});
            return View("Index","hello");
        }

以上方法若是id爲null就重定向到一個路由,這裏簡單說一下爲何建立這樣一個匿名對象,爲何對象的名稱爲controller,和action而不是controllername和actionname?咱們能夠運行一下mvc程序,看看RouteData裏的鍵值對的名稱是什麼,就會明白了.

測試方法以下

[Fact]
        public async Task ViewTest()
        {
            HomeController hc = new HomeController();
            var result = hc.Index(null);
            var redirect = (RedirectToRouteResult) result;
            var data = redirect.RouteValues;
            var controllerName = data?["controller"]?.ToString();
            var actionName = data?["action"]?.ToString();
            Assert.True(!string.IsNullOrWhiteSpace(controllerName));
            Assert.True(!string.IsNullOrWhiteSpace(actionName));
            Assert.True(controllerName.EqualsIgnoreCase(nameof(HomeController).GetControllerName()));
            Assert.True(actionName.EqualsIgnoreCase(nameof(HomeController.Contact)));
        }

以上方法實際上和上面的RedirectToAction測試本質上差很少,都是肯定導向到了正確的controller和action裏,不一樣的是值的獲取方法.

RedirectToAction和RedirecttoRoute均可以傳路由值,和上面以樣經過索引鍵獲取到值,這裏再也不展開講解.

確保正確重定向到指定短url

.net core裏新增了一個LocalRedirect(以及對應的永久重寫向,永久重定向保持方法等,其它重定向也都有這些相似方法族).它相似於RedirecttoRoute,只不過是參數並非RouteData,而是一個短路由(不帶主機名和ip,由於默認而且只能內部重定向).

咱們把HomeController下的Index方法改成以下:

public IActionResult Index(int? id)
        {
            if (!id.HasValue) return LocalRedirect("/Home/Hello");
            return View("Index","hello");
        }

若是Id是null就重定向到/home/Hello想必你們在頁面向後端請求的時候寫過很多這樣的相似代碼,這裏就再也不詳細解釋了.

測試方法以下:

[Fact]
        public async Task ViewTest()
        {
            HomeController hc = new HomeController();
            var result = hc.Index(null);
            var redirect = (LocalRedirectResult) result;
            var url = redirect.Url.Split("/").Where(a=>!string.IsNullOrEmpty(a));
        }

這裏主要是經過Url獲取到這個地址,而後把它分紅若干部分.默認狀況下第一部分是控制器名,第二部分是action名.後面的代碼再也不寫了,你們本身嘗試一下.

須要注意的是,以上全部的示例只處理了默認路由的狀況,並無處理路由參數,自定義路由以及aera中的路由等.若是不是默認路由,則以上內容的第一部分就不必定是controller名了,這裏還須要根據實際業務來處理.

view測試

上一節知識算是對mvc控制器測試的補充知識.這節正式開始講解關於mvc裏view的集成測試.

有一點須要弄明白的是經過發送http請求進行集成測試是沒法獲取到程序裏的Controller對象的,咱們只能能View的頁面進行集成測試.

對頁面的測試主要包含了對返回狀態的測試和頁面內容的測試.產生確保正確響應,而且返回了正確頁面,前面單元測試裏主要測試的是返回的view名稱是正確的,至於可否到達這個頁面則不必定.集成測試裏咱們要根據當前頁面的特徵來肯定當前頁面的身份.也就是這個頁面有不同凡響的,能區分它和別的頁面不一樣的特徵.

咱們仍然用HomeController下的Index來做爲案例講解.對Index方法改成出廠設置,內容以下

public IActionResult Index()
        {
            return View();
        }

這裏返回的首先頁面裏面包含了一個輪播圖,咱們能夠斷言返回的頁面中包含有carousel關鍵字,測試代碼以下

[Fact]
        public async Task ViewIntegrityTest()
        {
            var response = await _client.GetAsync("/Home/Index");
            response.EnsureSuccessStatusCode();
            var responseStr = await response.Content.ReadAsStringAsync();
            Assert.Contains("carousel", responseStr);
        }

以上測試返回的內容(就是整個view頁面)中包含carousel這樣的字樣.

須要注意的是以上內容在實際項目中遠不能區分這個頁面就是home頁面,可能還須要其它的判斷,須要根據實際狀況酌情考慮,若是以特定id,名稱等可能會變的內容做爲判斷則會給集成測試帶來維護上的麻煩.有時候頁面太多改動又太大致使單元測試大片報錯,可能在時間緊任務重的狀況下直接把單元測試放棄了,所以不是範圍越小,判斷的內容越精細越好,而是儘可能找到本頁面中不易變的,能區別其它頁面的東西.即使是區分不了,這裏至少能肯定頁面正確返回了而不是404頁面.這樣比上線後手動打開瀏覽器檢測頁面是否能正常打開要可靠的多.

仍然有一點須要注意的是並非集成測試經過了就萬事大吉,咱們仍然要在項目上線後對頁面進行抽檢,查看頁面佈局是否正常.固然這些也能夠自動化來完成.可是抽檢仍然是必要的,不要相信全部的方法都是完美無缺的.

相關文章
相關標籤/搜索