.netcore持续集成测试篇之MVC层单元测试

前面我们讲的很多单元测试的的方法和技巧不论是在.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.

下面是分别获取View的名称的数据模型,然后我们断言View名称是Index,model的值是hello,当然以上代码比较简单显然是能通过的,在实际业务中我们还要对Model进行更为复杂的断言.

需要注意的是,Action返回的view并不是都有名称的,如果是返回的本方法对应的view,默认名称是可以省略的,这样以上断言就会失败,因此如果名称不写的时候我们可以断言ViewName是空,同样返回的是本方法默认的view.

确保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页面.这样比上线后手动打开浏览器检测页面是否能正常打开要可靠的多.

仍然有一点需要注意的是并不是集成测试通过了就万事大吉,我们仍然要在项目上线后对页面进行抽检,查看页面布局是否正常.当然这些也可以自动化来完成.但是抽检仍然是必要的,不要相信所有的方法都是天衣无缝的.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438200.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SqlServer学习之触发器

什么是触发器&#xff1f; 根据百度百科的解释&#xff0c;触发器是SqlServer提供给程序员和数据分析员来保证数据完整性的一种方法&#xff0c;它是与表事件相关的特殊的存储过程&#xff0c;他的执行不是有程序调用&#xff0c;也不是手工启动&#xff0c;而是由事件来出发&a…

base64原理及其编解码的python实现

base64原理及其编解码的python实现base64base64简介base64编码表base64编码原理base64编解码的python实现其他base编码base16base32base36、base58、 base62、 base85、base91、 base92base64 base64简介 base64是一种基于64个可打印字符来表示二进制数据的表示方法。2664&am…

Microsoft Visual Studio 文件识别及其用途简述

VS2008 新建项目后会产生相应的文件sln、ncb、suo、以及相应项目工程文件夹&#xff0c;项目工程文件夹下包括vcproj、filters、ReadMe.txt、targetver.h、stdafx.h、stdafx.cpp及其他文件&#xff1b;VS2010/VS2010/VS2012/VS2013 新建项目后会产生文件sln、suo、sdf、ipch文件…

REVERSE-PRACTICE-JarvisOJ-1

REVERSE-PRACTICE-JarvisOJ-1[61dctf]androideasy[61dctf]stheasyDD - Android NormalDD - Android Easy[61dctf]androideasy apk文件&#xff0c;用jadx-gui打开 主要的逻辑为&#xff0c;获取输入&#xff0c;检验输入的长度&#xff0c;输入异或23后与已知数组比较&#xf…

NuGet学习笔记(1) 初识NuGet及快速安装使用

关于NuGet园子里已经有不少介绍及使用经验&#xff0c;本文仅作为自己研究学习NuGet一个记录。 初次认识NuGet是在去年把项目升级为MVC3的时候&#xff0c;当时看到工具菜单多一项Library Package Manager&#xff0c;右键项目文件多了一项Manage Nuget Packages...&#xff0c…

NuGet学习笔记(2) 使用图形化界面打包自己的类库

上文NuGet学习笔记(1) 初识NuGet及快速安装使用说到NuGet相对于我们最重要的功能是能够搭建自己的NuGet服务器&#xff0c;实现公司内部类库的轻松共享更新。在安装好NuGet扩展后&#xff0c;我们已经能够通过NuGet轻松下载自己需要的类库&#xff0c;下面来说一说如何将自己的…

REVERSE-PRACTICE-JarvisOJ-2

REVERSE-PRACTICE-JarvisOJ-2DD - HelloAPK_500DebugMeFindPassDD - Hello macos文件&#xff0c;无壳&#xff0c;ida分析 start函数和sub_100000C90函数没什么作用 主要的逻辑在sub_100000CE0函数&#xff0c;反调试检测和byte_100001040数组的循环变换&#xff0c;最后打印…

NuGet学习笔记(3) 搭建属于自己的NuGet服务器

文章导读 创建NuGetServer Web站点 发布站点到IIS 添加本地站点到包包数据源 在上一篇NuGet学习笔记(2) 使用图形化界面打包自己的类库 中讲解了如何打包自己的类库&#xff0c;接下来进行最重要的一步&#xff0c;从零开始搭建属于自己的NuGet服务器&#xff0c;诚然园子里…

REVERSE-PRACTICE-JarvisOJ-3

REVERSE-PRACTICE-JarvisOJ-3爬楼梯软件密码破解-1Classical CrackMe2Smali爬楼梯 apk文件&#xff0c;放到模拟器里运行一下 “爬一层楼”按钮可按&#xff0c;每按一下&#xff0c;“已爬的楼层”加1层 “爬到了&#xff0c;看FLAG”按钮不可按&#xff0c;应该是“已爬的楼…

管理全局包、缓存和临时文件夹

每当安装、更新或还原包时&#xff0c;NuGet 将管理项目结构多个文件夹之外的包和包信息&#xff1a; name说明和位置&#xff08;每个用户&#xff09;global‑packagesglobal-packages 文件夹是 NuGet 安装任何下载包的位置。 每个包完全展开到匹配包标识符和版本号的子文件…

REVERSE-PRACTICE-JarvisOJ-4

REVERSE-PRACTICE-JarvisOJ-4Classical CrackmeFindKeyClassical Crackme exe程序&#xff0c;输入注册码&#xff0c;输入错误弹窗 查壳发现是.Net程序&#xff0c;dnSpy打开&#xff08;要不要用de4dot unpack无所谓&#xff0c;这里打开的是解包过的程序&#xff09; 按下“…

在解决方案中所使用 NuGet 管理软件包依赖

使用程序包恢复功能可以在提交源代码时, 不需要将 packages 中的程序集文件提交到源代码管理中&#xff0c;大幅减少项目的尺寸。所有NuGet程序包都存储在解决方案的Packages文件夹中。 要启用程序包恢复功能&#xff0c;可右键单击解决方案&#xff08;注意&#xff0c;不是右…

REVERSE-PRACTICE-BUUCTF-1

REVERSE-PRACTICE-BUUCTF-1easyrereverse1reverse2内涵的软件easyre exe程序&#xff0c;无壳&#xff0c;ida分析 左侧函数窗找到main函数&#xff0c;flag提交即可 reverse1 exe程序&#xff0c;运行后提示输入flag&#xff0c;ida分析 左侧函数窗无明显的main函数&#…

REVERSE-PRACTICE-BUUCTF-2

REVERSE-PRACTICE-BUUCTF-2新年快乐[BJDCTF 2nd]guessgamehellowordxor新年快乐 exe程序&#xff0c;运行后提示输入正确的flag&#xff0c;有upx壳&#xff0c;常规工具脱壳即可 ida分析&#xff0c;左侧函数窗找到main函数&#xff0c;F5反汇编&#xff0c;简单的输入比较逻…

UML口诀

接口继承用三角&#xff0c; 聚合组合用菱形&#xff0c; 依赖关联是箭头&#xff0c; 虚弱实强指源头。

REVERSE-PRACTICE-BUUCTF-3

REVERSE-PRACTICE-BUUCTF-3reverse3不一样的flagSimpleRevJava逆向解密reverse3 exe程序&#xff0c;无壳&#xff0c;用ida分析 左侧函数窗最后找到main函数&#xff0c;F5反汇编 先base64&#xff0c;然后顺序加上对应的下标&#xff0c;最后比较&#xff0c;写逆脚本即可得…

console对象的方法log、info、warn、error的区别及几个实用的方法

先说一下console&#xff0c;JavaScript 原生中默认是没有 Console 对象&#xff0c;这是某些浏览器提供的内置对象。 在不同的浏览器里显示可能不同 下面的运行截图中&#xff0c;图中的灰色是运行的这句&#xff0c;灰色下面是此句运行的结果 log()、info()、warn()、error()…

认识ASP.NET 5项目结构和项目文件xproj

ASP.NET 5 在项目结构上做了很大的改变&#xff0c;我们以前熟悉的目录结构与项目文件内容都不太一样了&#xff0c;本篇文章带大家了解 ASP.NET 5 到底跟以前有哪些不一样的地方。 我们先用 Visual Studio 2015 建立一个全新的 ASP.NET 5 网站项目&#xff0c;打开VS2015&…

REVERSE-PRACTICE-BUUCTF-4

REVERSE-PRACTICE-BUUCTF-4刮开有奖[BJDCTF 2nd]8086[GKCTF2020]Check_1n[GXYCTF2019]luck_guy刮开有奖 exe文件&#xff0c;运行后无任何提示&#xff0c;无壳&#xff0c;用ida分析 WinMain->DialogBoxParamA->DialogFunc DialogBoxParamA函数&#xff1a; DialogFun…

REVERSE-PRACTICE-BUUCTF-5

REVERSE-PRACTICE-BUUCTF-5findit简单注册器[GWCTF 2019]pyre[BJDCTF2020]JustREfindit apk文件&#xff0c;直接上jadx-gui分析 验证输入的逻辑比较简单&#xff0c;写脚本即可 安装这个apk&#xff0c;将字符串输入即可得到flag&#xff0c;当然也可以直接写输出flag的脚本…