一、前言
在以前的.net framework项目中,我们也写过一些单元测试的项目,而在asp.net core 这种web或者api应用程序中要做单元测试是很方便的。
这篇文章主要讲解如何使用xunit对asp.net core应用程序做单元测试。.net core中常用的测试工具还有nunit和mstest。
xunit是一个测试框架,可以针对.net/.net core项目进行测试。测试项目需要引用被测试的项目,从而对其进行测试。测试项目同时需要引用xunit库。测试编写好后,用test runner来运行测试。test runner可以读取测试代码,并且会知道我们所使用的测试框架,然后执行,并显示结果。目前可用的test runner包括vs自带的test explorer,或者dotnet core命令行,以及第三方工具,例如resharper等。
xunit可以支持多种平台的测试:
- .net framework
- .net core
- .net standard
- uwp
- xamarin
二、创建示例项目
为了使示例项目更加的贴近真实的项目开发,这里采用分层的方式创建一个示例项目,创建完成后的项目结构如下图所示:
下面讲解一下每层的作用,按照从上往下的顺序:
- testdemo:从名字就可以看出来,这是一个单元测试的项目,针对控制器进行测试。
- unittest.data:数据访问,封装与entityframeworkcore相关的操作。
- unittest.irepository:泛型仓储接口,封装基础的增删改查。
- unittest.model:实体层,定义项目中使用到的所有实体。
- unittest.repository:泛型仓储接口实现层,实现接口里面定义的方法。
- unittestdemo:asp.net core webapi,提供api接口。
1、unittest.model
实体层里面只有一个student类:
using system; using system.collections.generic; using system.text; namespace unittest.model { public class student { public int id { get; set; } public string name { get; set; } public int age { get; set; } public string gender { get; set; } } }
2、unittest.data
里面封装与ef core有关的操作,首先需要引入microsoft.entityframeworkcore、microsoft.entityframeworkcore.sqlserver、microsoft.entityframeworkcore.tools三个nuget包,直接在管理nuget程序包里面引入,这里不在讲述。
引入相关nuget包以后,我们创建数据上下文类,该类继承自ef core的dbcontext,里面设置表名和一些属性:
using microsoft.entityframeworkcore; using unittest.model; namespace unittest.data { /// <summary> /// 数据上下文类 /// </summary> public class appdbcontext : dbcontext { /// <summary> /// 通过构造函数给父类构造传参 /// </summary> /// <param name="options"></param> public appdbcontext(dbcontextoptions<appdbcontext> options) : base(options) { } public dbset<student> students { get; set; } protected override void onmodelcreating(modelbuilder modelbuilder) { modelbuilder.entity<student>().totable("t_student"); modelbuilder.entity<student>().haskey(p => p.id); modelbuilder.entity<student>().property(p => p.name).hasmaxlength(32); // 添加种子数据 modelbuilder.entity<student>().hasdata( new student() { id = 1, name = "测试1", age = 20, gender = "男" }, new student() { id = 2, name = "测试2", age = 22, gender = "女" }, new student() { id = 3, name = "测试3", age = 23, gender = "男" }); base.onmodelcreating(modelbuilder); } } }
这里采用数据迁移的方式生成数据库,需要在api项目中引入microsoft.entityframeworkcore、microsoft.entityframeworkcore.sqlserver、microsoft.entityframeworkcore.tools三个nuget包。引入方式同上。
然后在api项目的appsettings.json文件里面添加数据库链接字符串:
{ "logging": { "loglevel": { "default": "information", "microsoft": "warning", "microsoft.hosting.lifetime": "information" } }, "allowedhosts": "*", // 数据库连接字符串 "connectionstring": { "dbconnection": "initial catalog=testdb;user id=sa;password=1234;data source=.;connection timeout=10;" } }
在json文件中添加完连接字符串以后,修改startup类的configureservices方法,在里面配置使用在json文件中添加的连接字符串:
// 添加数据库连接字符串 services.adddbcontext<appdbcontext>(options => { options.usesqlserver(configuration.getsection("connectionstring").getsection("dbconnection").value); });
这样就可以使用数据迁移的方式生成数据库了。
3、unittest.irepository
该项目中使用泛型仓储,定义一个泛型仓储接口:
using system.collections.generic; using system.threading.tasks; namespace unittest.irepository { public interface irepository<t> where t:class,new() { task<list<t>> getlist(); task<int?> add(t entity); task<int?> update(t entity); task<int?> delete(t entity); } }
然后在定义istudentrepository接口继承自irepository泛型接口:
using unittest.model; namespace unittest.irepository { public interface istudentrepository: irepository<student> { } }
4、unittest.repository
这里是实现上面定义的仓储接口:
using system.collections.generic; using system.linq; using system.threading.tasks; using unittest.data; using unittest.irepository; using unittest.model; namespace unittest.repository { public class studentrepository : istudentrepository { private readonly appdbcontext _dbcontext; /// <summary> /// 通过构造函数实现依赖注入 /// </summary> /// <param name="dbcontext"></param> public studentrepository(appdbcontext dbcontext) { _dbcontext = dbcontext; } public async task<int?> add(student entity) { _dbcontext.students.add(entity); return await _dbcontext.savechangesasync(); } public async task<int?> delete(student entity) { _dbcontext.students.remove(entity); return await _dbcontext.savechangesasync(); } public async task<list<student>> getlist() { list<student> list = new list<student>(); list = await task.run<list<student>>(() => { return _dbcontext.students.tolist(); }); return list; } public async task<int?> update(student entity) { student student = _dbcontext.students.find(entity.id); if (student != null) { student.name = entity.name; student.age = entity.age; student.gender = entity.gender; _dbcontext.entry<student>(student).state = microsoft.entityframeworkcore.entitystate.modified; return await _dbcontext.savechangesasync(); } return 0; } } }
5、unittestdemo
先添加一个value控制器,里面只有一个get方法,而且没有任何的依赖关系,先进行最简单的测试:
using microsoft.aspnetcore.mvc; namespace unittestdemo.controllers { [route("api/[controller]")] [apicontroller] public class valuecontroller : controllerbase { [httpget("{id}")] public actionresult<string> get(int id) { return $"para is {id}"; } } }
6、testdemo
我们在添加测试项目的时候,直接选择使用xunit测试项目,如下图所示:
这样项目创建完成以后,就会自动添加xunit的引用:
<itemgroup> <packagereference include="microsoft.net.test.sdk" version="16.2.0" /> <packagereference include="xunit" version="2.4.1" /> <packagereference include="xunit.runner.visualstudio" version="2.4.0" /> </itemgroup>
但要测试 asp.net core 应用还需要添加两个 nuget 包:
install-package microsoft.aspnetcore.app install-package microsoft.aspnetcore.testhost
上面是使用命令的方式进行安装,也可以在管理nuget程序包里面进行搜索,然后安装。
千万不要忘记还要引入要测试的项目。最后的项目引入是这样的:
<project sdk="microsoft.net.sdk"> <propertygroup> <targetframework>netcoreapp3.1</targetframework> <ispackable>false</ispackable> </propertygroup> <itemgroup> <packagereference include="microsoft.aspnetcore.app" version="2.2.8" /> <packagereference include="microsoft.aspnetcore.testhost" version="3.1.2" /> <packagereference include="microsoft.net.test.sdk" version="16.2.0" /> <packagereference include="newtonsoft.json" version="12.0.3" /> <packagereference include="xunit" version="2.4.1" /> <packagereference include="xunit.runner.visualstudio" version="2.4.0" /> <packagereference include="coverlet.collector" version="1.0.1" /> </itemgroup> <itemgroup> <projectreference include="..\unittest.model\unittest.model.csproj" /> <projectreference include="..\unittestdemo\unittestdemo.csproj" /> </itemgroup> </project>
都添加完以后,重新编译项目,保证生成没有错误。
三、编写单元测试
单元测试按照从上往下的顺序,一般分为三个阶段:
- arrange:准备阶段。这个阶段做一些准备工作,例如创建对象实例,初始化数据等。
- act:行为阶段。这个阶段是用准备好的数据去调用要测试的方法。
- assert:断定阶段。这个阶段就是把调用目标方法的返回值和预期的值进行比较,如果和预期值一致则测试通过,否则测试失败。
我们在api项目中添加了一个value控制器,我们以get方法作为测试目标。一般一个单元测试方法就是一个测试用例。
我们在测试项目中添加一个valuetest测试类,然后编写一个单元测试方法,这里是采用模拟httpclient发送http请求的方式进行测试:
using microsoft.aspnetcore; using microsoft.aspnetcore.hosting; using microsoft.aspnetcore.testhost; using system.net; using system.net.http; using system.threading.tasks; using unittestdemo; using xunit; namespace testdemo { public class valuetests { public httpclient _client { get; } /// <summary> /// 构造方法 /// </summary> public valuetests() { var server = new testserver(webhost.createdefaultbuilder() .usestartup<startup>()); _client = server.createclient(); } [fact] public async task getbyid_shouldbe_ok() { // 1、arrange var id = 1; // 2、act // 调用异步的get方法 var response = await _client.getasync($"/api/value/{id}"); // 3、assert assert.equal(httpstatuscode.ok, response.statuscode); } } }
我们在构造函数中,通过testserver拿到一个httpclient对象,用它来模拟http请求。我们写了一个测试用例,完整演示了单元测试的arrange、act和assert三个步骤。
1、运行单元测试
单元测试用例写好以后,打开“测试资源管理器”:
在底部就可以看到测试资源管理器了:
在要测试的方法上面右键,选择“运行测试”就可以进行测试了:
注意观察测试方法前面图标的颜色,目前是蓝色的,表示测试用例还没有运行过:
测试用例结束以后,我们在测试资源管理器里面可以看到结果:
绿色表示测试通过。我们还可以看到执行测试用例消耗的时间。
如果测试结果和预期结果一致,那么测试用例前面图标的颜色也会变成绿色:
如果测试结果和预期结果不一致就会显示红色,然后需要修改代码直到出现绿色图标。我们修改测试用例,模拟测试失败的情况:
using microsoft.aspnetcore; using microsoft.aspnetcore.hosting; using microsoft.aspnetcore.testhost; using system.net; using system.net.http; using system.threading.tasks; using unittestdemo; using xunit; namespace testdemo { public class valuetests { public httpclient _client { get; } /// <summary> /// 构造方法 /// </summary> public valuetests() { var server = new testserver(webhost.createdefaultbuilder() .usestartup<startup>()); _client = server.createclient(); } [fact] public async task getbyid_shouldbe_ok() { // 1、arrange var id = 1; // 2、act // 调用异步的get方法 var response = await _client.getasync($"/api/value/{id}"); //// 3、assert //assert.equal(httpstatuscode.ok, response.statuscode); // 3、assert // 模拟测试失败 assert.equal(httpstatuscode.badrequest, response.statuscode); } } }
然后运行测试用例:
2、调试单元测试
我们也可以通过添加断点的方式在测试用例中进行调试。调试单元测试很简单,只需要在要调试的方法上面右键选择“调试测试”,如下图所示:
其它操作就跟调试普通方法一样。
除了添加断点调试,我们还可以采用打印日志的方法来快速调试,xunit可以很方便地做到这一点。我们修改valuetest类:
using microsoft.aspnetcore; using microsoft.aspnetcore.hosting; using microsoft.aspnetcore.testhost; using system.net; using system.net.http; using system.threading.tasks; using unittestdemo; using xunit; using xunit.abstractions; namespace testdemo { public class valuetests { public httpclient _client { get; } public itestoutputhelper output { get; } /// <summary> /// 构造方法 /// </summary> public valuetests(itestoutputhelper outputhelper) { var server = new testserver(webhost.createdefaultbuilder() .usestartup<startup>()); _client = server.createclient(); output = outputhelper; } [fact] public async task getbyid_shouldbe_ok() { // 1、arrange var id = 1; // 2、act // 调用异步的get方法 var response = await _client.getasync($"/api/value/{id}"); // 3、assert // 模拟测试失败 //assert.equal(httpstatuscode.badrequest, response.statuscode); // 输出返回信息 // output var responsetext = await response.content.readasstringasync(); output.writeline(responsetext); // 3、assert assert.equal(httpstatuscode.ok, response.statuscode); } } }
这里我们在构造函数中添加了 itestoutputhelper 参数,xunit 会将一个实现此接口的实例注入进来。拿到这个实例后,我们就可以用它来输出日志了。运行(注意不是 debug)此方法,运行结束后在测试资源管理器里面查看:
点击就可以看到输出的日志了:
在上面的例子中,我们是使用的简单的value控制器进行测试,控制器里面没有其他依赖关系,如果控制器里面有依赖关系该如何测试呢?方法还是一样的,我们新建一个student控制器,里面依赖istudentrepository接口,代码如下:
using system.collections.generic; using system.threading.tasks; using microsoft.aspnetcore.mvc; using unittest.irepository; using unittest.model; namespace unittestdemo.controllers { [route("api/student")] [apicontroller] public class studentcontroller : controllerbase { private readonly istudentrepository _repository; /// <summary> /// 通过构造函数注入 /// </summary> /// <param name="repository"></param> public studentcontroller(istudentrepository repository) { _repository = repository; } /// <summary> /// get方法 /// </summary> /// <returns></returns> [httpget] public async task<actionresult<list<student>>> get() { return await _repository.getlist(); } } }
然后在startup类的configureservices方法中注入:
public void configureservices(iservicecollection services) { // 添加数据库连接字符串 services.adddbcontext<appdbcontext>(options => { options.usesqlserver(configuration.getsection("connectionstring").getsection("dbconnection").value); }); // 添加依赖注入到容器中 services.addscoped<istudentrepository, studentrepository>(); services.addcontrollers(); }
在单元测试项目中添加studenttest类:
using microsoft.aspnetcore; using microsoft.aspnetcore.hosting; using microsoft.aspnetcore.testhost; using newtonsoft.json; using system.collections.generic; using system.net.http; using system.threading.tasks; using unittest.model; using unittestdemo; using xunit; using xunit.abstractions; namespace testdemo { public class studenttest { public httpclient client { get; } public itestoutputhelper output { get; } public studenttest(itestoutputhelper outputhelper) { var server = new testserver(webhost.createdefaultbuilder() .usestartup<startup>()); client = server.createclient(); output = outputhelper; } [fact] public async task get_shouldbe_ok() { // 2、act var response = await client.getasync($"api/student"); // output string context = await response.content.readasstringasync(); output.writeline(context); list<student> list = jsonconvert.deserializeobject<list<student>>(context); // assert assert.equal(3, list.count); } } }
然后运行单元测试:
可以看到,控制器里面如果有依赖关系,也是可以使用这种方式进行测试的。
post方法也可以使用同样的方式进行测试,修改控制器,添加post方法:
using system.collections.generic; using system.threading.tasks; using microsoft.aspnetcore.mvc; using unittest.irepository; using unittest.model; namespace unittestdemo.controllers { [route("api/student")] [apicontroller] public class studentcontroller : controllerbase { private readonly istudentrepository _repository; /// <summary> /// 通过构造函数注入 /// </summary> /// <param name="repository"></param> public studentcontroller(istudentrepository repository) { _repository = repository; } /// <summary> /// get方法 /// </summary> /// <returns></returns> [httpget] public async task<actionresult<list<student>>> get() { return await _repository.getlist(); } /// <summary> /// post方法 /// </summary> /// <param name="entity"></param> /// <returns></returns> [httppost] public async task<bool> post([frombody]student entity) { int? result = await _repository.add(entity); if(result==null) { return false; } else { return result > 0 ? true : false; } } } }
在增加一个post的测试方法:
using microsoft.aspnetcore; using microsoft.aspnetcore.hosting; using microsoft.aspnetcore.testhost; using newtonsoft.json; using system.collections.generic; using system.net.http; using system.threading.tasks; using unittest.model; using unittestdemo; using xunit; using xunit.abstractions; namespace testdemo { public class studenttest { public httpclient client { get; } public itestoutputhelper output { get; } public studenttest(itestoutputhelper outputhelper) { var server = new testserver(webhost.createdefaultbuilder() .usestartup<startup>()); client = server.createclient(); output = outputhelper; } [fact] public async task get_shouldbe_ok() { // 2、act var response = await client.getasync($"api/student"); // output string context = await response.content.readasstringasync(); output.writeline(context); list<student> list = jsonconvert.deserializeobject<list<student>>(context); // assert assert.equal(3, list.count); } [fact] public async task post_shouldbe_ok() { // 1、arrange student entity = new student() { name="测试9", age=25, gender="男" }; var str = jsonconvert.serializeobject(entity); httpcontent content = new stringcontent(str); // 2、act content.headers.contenttype = new system.net.http.headers.mediatypeheadervalue("application/json"); httpresponsemessage response = await client.postasync("api/student", content); string responsebody = await response.content.readasstringasync(); output.writeline(responsebody); // 3、assert assert.equal("true", responsebody); } } }
运行测试用例:
这样一个简单的单元测试就完成了。
我们观察上面的两个测试类,发现这两个类都有一个共同的特点:都是在构造函数里面创建一个httpclient对象,我们可以把创建httpclient对象抽离到一个共同的基类里面,所有的类都继承自基类。该基类代码如下:
using microsoft.aspnetcore.hosting; using microsoft.aspnetcore.testhost; using system.io; using system.net.http; using unittestdemo; namespace testdemo { /// <summary> /// 基类 /// </summary> public class apicontrollertestbase { /// <summary> /// 返回httpclient对象 /// </summary> /// <returns></returns> protected httpclient getclient() { var builder = new webhostbuilder() // 指定使用当前目录 .usecontentroot(directory.getcurrentdirectory()) // 使用startup类作为启动类 .usestartup<startup>() // 设置使用测试环境 .useenvironment("testing"); var server = new testserver(builder); // 创建httpclient httpclient client = server.createclient(); return client; } } }
然后修改studenttest类,使该类继承自上面创建的基类:
using newtonsoft.json; using system.collections.generic; using system.net.http; using system.threading.tasks; using unittest.model; using xunit; using xunit.abstractions; namespace testdemo { public class studenttest: apicontrollertestbase { public httpclient client { get; } public itestoutputhelper output { get; } public studenttest(itestoutputhelper outputhelper) { // var server = new testserver(webhost.createdefaultbuilder() //.usestartup<startup>()); // client = server.createclient(); // 从父类里面获取httpclient对象 client = base.getclient(); output = outputhelper; } [fact] public async task get_shouldbe_ok() { // 2、act var response = await client.getasync($"api/student"); // output string context = await response.content.readasstringasync(); output.writeline(context); list<student> list = jsonconvert.deserializeobject<list<student>>(context); // assert assert.equal(3, list.count); } [fact] public async task post_shouldbe_ok() { // 1、arrange student entity = new student() { name="测试9", age=25, gender="男" }; var str = jsonconvert.serializeobject(entity); httpcontent content = new stringcontent(str); // 2、act content.headers.contenttype = new system.net.http.headers.mediatypeheadervalue("application/json"); httpresponsemessage response = await client.postasync("api/student", content); string responsebody = await response.content.readasstringasync(); output.writeline(responsebody); // 3、assert assert.equal("true", responsebody); } } }
文章中的示例代码地址:https://github.com/jxl1024/unittest
到此这篇关于asp.net core项目使用xunit进行单元测试的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持。