.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页面.这样比上线后手动打开浏览器检测页面是否能正常打开要可靠的多.
仍然有一点需要注意的是并不是集成测试通过了就万事大吉,我们仍然要在项目上线后对页面进行抽检,查看页面布局是否正常.当然这些也可以自动化来完成.但是抽检仍然是必要的,不要相信所有的方法都是天衣无缝的.
.netcore持续集成测试篇之MVC测试的更多相关文章
- .netcore持续集成测试篇之Xunit结合netcore内存服务器发送post请求
系列目录 Web项目中,很多与用户数据交互的请求都是Post请求,想必大家都用过HttpClient构造过post请求,这里并不对HttpClient做详细介绍,只介绍一些常用的功能.并结合AutoF ...
- .netcore持续集成测试篇之Xunit数据驱动测试一
系列目录 Nunit里提供了丰富的数据测试功能,虽然Xunit里提供的比较少,但是也能满足很多场景下使用了,如果数据场景非常复杂,Nunit和Xunit都是无法胜任的,有不少测试者选择自己编写一个数据 ...
- .netcore持续集成测试篇之开篇简介及Xunit基本使用
系列目录 为了支持跨平台,微软为.net平台提供了.net core test sdk,这样第三方测试框架诸如Nunit,Xunit等只需要按照sdk提供的api规范进行开发便可以被dotnet cl ...
- .netcore持续集成测试篇之搭建内存服务器进行集成测试一
系列目录 在web项目里,我们把每一层的代码的单元测试都通过并不代表程序能正常运行,因为这个过程缺失了http管道,很多时候我们还还需要把项目布在iis环境中或者在vs里启动iis express服务 ...
- .netcore持续集成测试篇之测试方法改造
系列目录 通过前面两节讲解,我们的测试类中已经有两个测试方法了,总体上如下 public class mvc20 { private readonly HttpClient _client; publ ...
- .netcore持续集成测试篇之 .net core 2.1项目集成测试
系列目录 从.net到.net core以后,微软非常努力,以每年一到两个大版本的频率在演进.net core,去年相继发布了.net core 2.1和2.2,其中2.1是长期支持版,不断的快速更新 ...
- .netcore持续集成测试篇之web项目验收测试
系列目录 通过前面的单元测试,我们能够保证项目的基本模块功能逻辑是正常的,通过集成测试能够保证接口的请求是正常的.然而最终项目交付我们还需要对项目进行页面的行为进行测试,比如页面布局是否正常,按钮是否 ...
- .net持续集成测试篇之Nunit 测试配置
系列目录 在开始之前我们先看一个陷阱 用到的Person类如下 public class Person:IPerson { public string Name { get; set; } publi ...
- .net持续集成测试篇之Nunit参数化测试
系列目录 在进行单元测试的时候,很多时候,很多时候我们都是在单元测试方法内部提供特定的值,但是这样测试往往造成样本数不足从而导致覆盖的结果不够全面,很多时候我们更想提供来自外部的,满足条件的一组值来进 ...
随机推荐
- c++学习书籍推荐《C++程序设计语言(特别版)》下载
百度云及其他网盘下载地址:点我 编辑推荐 <C++程序设计语言(特别版•十周年中文纪念版)>编辑推荐:十周年纪念版,体味C++语言的精妙与魅力,享受与大师的心灵对话.1979年,Biarn ...
- Java NIO学习系列四:NIO和IO对比
前面的一些文章中我总结了一些Java IO和NIO相关的主要知识点,也是管中窥豹,IO类库已经功能很强大了,但是Java 为什么又要引入NIO,这是我一直不是很清楚的?前面也只是简单提及了一下:因为性 ...
- 如何在vue中监听scroll,从而实现滑动加载更多
首先需要明确3个定义: 文档高度:整个页面的高度 可视窗口高度:你看到的浏览器可视屏幕高度 滚动条滚动高度: 滚动条下滑过的高度 当 文档高度 = 可视窗口高度 + 滚动条高度 时,滚动条正好到底. ...
- .NET多线程之Thread、Task、ThreadPool、Timer
下表为多线程操作常见对象: 对象 方法/属性 描述 用途 用法 性能 Thread(线程) Start 启动线程,启动后线程处于System.Threading.ThreadState.Running ...
- python如何将一个多位数数值转换为列表类型
现在:a = 10,由于暂时没找到更好的方法,且使用下面的方法进行转换. 目标:转化为['10'] 以下为错误尝试: 1.直接转换,提示整型对象不可迭代. 2.先转换为字符串,再转换为列表,发现被分成 ...
- c++小游戏——杀手
杀手小游戏 会有一个存活者:(1 2 3 4 5),如果出现(1 0 3 4 5),代表二号已经死了. 一号有3次复活权 且有一次随机诅咒权(即当自己被杀死时,会随机诅咒另外一个人,当然不是死人或自己 ...
- 《ElasticSearch6.x实战教程》之复杂搜索、Java客户端(下)
第八章-复杂搜索 黑夜给了我黑色的眼睛,我却用它寻找光明. 经过了解简单的API和简单搜索,已经基本上能应付大部分的使用场景.可是非关系型数据库数据的文档数据往往又多又杂,各种各样冗余的字段,组成了一 ...
- python3.x 与 python2.x 差别记录
从2.x过渡到3.x的时候,遇到了大大小小的坑,于是便记录下来- 1.print: 3.x 所有print都要加 "( )",print更像(就是)一个函数了. 2.x 可以加& ...
- Linux AUFS 文件系统
AUFS 的英文全称为 Advanced Mult-Layered Unification Filesystem,曾经是 Another Mult-Layered Unification Filesy ...
- spark 源码分析之十九 -- DAG的生成和Stage的划分
上篇文章 spark 源码分析之十八 -- Spark存储体系剖析 重点剖析了 Spark的存储体系.从本篇文章开始,剖析Spark作业的调度和计算体系. 在说DAG之前,先简单说一下RDD. 对RD ...