ASP.NET Core 入门(3)(单元测试Xunit及Shouldly的使用)
一、本篇简单介绍下在ASP.NET Core项目如何使用单元测试,例子是使用VS自带的Xunit来测试Web API接口,加上一款开源的断言工具Shouldly,方便写出更简洁、可读行更好的测试代码。
1、添加xUnit项目
由于我使用VS Code开发,所以操作是按VS Code的来,右键项目选择“Add new project”,接着选择“XUnit test project” 回车即可。可以看到引用了三个包,除此之外,还需要添加Microsoft.AspNetCore.App、Microsoft.AspNetCore.TestHost这两个包,另外我们再添加Shouldly的包。这样xUnit项目就建好了。
2、编写单元测试
对于接口怎么进行单元测试呢,一般做法都是针对接口项目的具体情况编写,比如封装测试基类,这里简单介绍基本的测试单元写法。
测试接口,需要注意做好两点,调用时怎么传参,测试结果怎么检验。对于接口具体方法传参,这个比较好处理,是什么就模拟什么数据,但如果接口的Controller构造函数带参数,比如有注入,那么这里就需要在调用的时候构建一样的注入参数。对于测试结果断言,我们可以针对接口的统一返参格式进行封装断言,这里用上Shouldly来封装。具体的看代码。
接口代码:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DYDGame.Application; using DYDGame.Application.DTOs; using DYDGame.Utility; using DYDGame.Web.Host; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace DYDGame.Web.Host.Controllers { /// <summary> /// 问答接口 /// </summary> [Route ("api/[controller]")] [ApiController] public class QuestionController : ControllerBase { private APIConfig _apiConfig; private QuestionService _questionService; private string _connectionString; public QuestionController (IOptions<APIConfig> apiConfig) { _apiConfig = apiConfig.Value; _connectionString = _apiConfig.RDSExternalConStrAESDecrypt (); _questionService = new QuestionService (_connectionString); } /// <summary> /// 判断答题是否正确 /// </summary> /// <param name="input"></param> [HttpPost ("JudgeAnswer")] public ResultObject JudgeAnswer (JudgeAnswerInput input) { dynamic obj = input; int questionId = obj.QuestionId; int answerId = obj.AnswerId; int flag = 0; try { flag = _questionService.JudgeAnswer (questionId, answerId); } catch (System.Exception ex) { Log4Net.LogInfo (_connectionString + ex.Message); } if (flag == -1) { return ResultObject.Failure ("没有该条问题", ErrCode.NoData); } else if (flag == 1) { return ResultObject.Ok ("恭喜答对了!", ErrCode.OK); } else { return ResultObject.Failure ("答案错误"); } } } }
接口需要用到注入的配置参数
using System; using System.Collections.Generic; using System.Data; using System.Linq; using DYDGame.Utility; using Microsoft.Extensions.Options; namespace DYDGame.Application { /// <summary> /// 读取appsettings.json的APIConfig /// </summary> /// <typeparam name="APIConfig"></typeparam> public class APIConfig: IOptions<APIConfig> { public APIConfig Value => this; public string ApiUrl { get; set; } public string OrgCode { get; set; } public string OrgKye { get; set; } public string RDSIntranetConStr { get; set; } public string RDSExternalConStr { get; set; } } public static class APIConfigModelExtension { public static string RDSIntranetConStrAESDecrypt (this APIConfig connectionStringModel) { return DESEncrypt.AESDecrypt (connectionStringModel.RDSIntranetConStr); } public static string RDSExternalConStrAESDecrypt (this APIConfig connectionStringModel) { return DESEncrypt.AESDecrypt (connectionStringModel.RDSExternalConStr); } } }
测试基类
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using DYDGame.Application; using DYDGame.Utility; namespace DYDGame.Tests { public class UnitBaseTest { private const string EncryptString = "N+edZ0vP9f5PdV1o7EkZgAbsowFPcQ7dUEx5W7DdJ5f30"; //方便调用controller,controller构造函数需要注入APIConfig public static APIConfig OptionAPIConfig = new APIConfig { RDSExternalConStr = EncryptString }; } }
针对接口统一返回对象 ResultObject 进行断言封装
/// <summary> /// 表示调用执行结果反馈 /// </summary> public class ResultObject { #region 公共属性 /// <summary> /// 调用是否成功,1成功0失败 /// </summary> public string retStatus { get; set; } /// <summary> /// 调用响应代码:0000:成功;1112:参数不能为空;1118:请传入约定参数;1212:参数值错误;1116:失败;1123:接口出错;1124 查无数据 /// </summary> public string errCode { get; set; } /// <summary> /// 调用响应消息 /// </summary> public string errMsg { get; set; } /// <summary> /// 调用结果数据 /// </summary> public object result { get; set; } }
using Shouldly; using System; using DYDGame.Web.Host; namespace DYDGame.Tests.Extensions { public static class ApiResultObjectExtensions { /// <summary> /// ResultObject["retStatus"] /// </summary> /// <param name="retObj"></param> /// <returns></returns> public static string Get_retStatus(this ResultObject retObj) { return retObj.retStatus; } /// <summary> /// ResultObject["errMsg"] /// </summary> /// <param name="retObj"></param> /// <returns></returns> public static string Get_errMsg(this ResultObject retObj) { return retObj.errMsg; } /// <summary> /// ResultObject["errCode"] /// </summary> /// <param name="retObj"></param> /// <returns></returns> public static string Get_errCode(this ResultObject retObj) { return retObj.errCode; } /// <summary> /// ResultObject["result"] /// </summary> /// <param name="retObj"></param> /// <returns></returns> public static string Get_result(this ResultObject retObj) { return retObj.result.ToString(); } /// <summary> /// 显示ResultObject中状态字符串 /// </summary> /// <param name="retObj"></param> /// <returns></returns> public static string Show_StatusCodeMsg(this ResultObject retObj) { return string.Format("retStatus:{0},errCode:{1},errMsg:{2}", retObj.Get_retStatus(), retObj.Get_errCode(), retObj.Get_errMsg()); } /// <summary> /// 显示ResultObject中状态字符串以及result /// </summary> /// <param name="retObj"></param> /// <returns></returns> public static string Show_StatusCodeMsg_And_result(this ResultObject retObj) { return string.Format("retStatus:{0},errCode:{1},errMsg:{2}|{3}", retObj.Get_retStatus(), retObj.Get_errCode(), retObj.Get_errMsg(), retObj.Get_result()); } /// <summary> /// 断言retStatus等于"1",或 显示ResultObject中状态字符串以及result /// </summary> /// <param name="retObj"></param> public static void retStatus_ShouldBe_1(this ResultObject retObj) { retObj.retStatus.ShouldBe("1", retObj.Show_StatusCodeMsg_And_result()); } /// <summary> /// 断言retStatus等于期望值 /// </summary> /// <param name="retObj"></param> /// <param name="expected"></param> public static void retStatus_ShouldBe(this ResultObject retObj, string expected) { retObj.retStatus.ShouldBe(expected, retObj.Show_StatusCodeMsg_And_result()); } } }
编写测试用例
using System; using System.Collections.Generic; using DYDGame.Application.DTOs; using DYDGame.Tests.Extensions; using DYDGame.Web.Host; using DYDGame.Web.Host.Controllers; using Xunit; using DYDGame.Application; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; namespace DYDGame.Tests { public class QuestionControllerTest : UnitBaseTest { private readonly QuestionController controller = new QuestionController (OptionAPIConfig); public static IEnumerable<object[]> JudgeAnswer_TestData () { var objValue = new JudgeAnswerInput (); objValue.QuestionId = 1; objValue.AnswerId = 0; yield return new object[] { objValue }; } [Xunit.Theory (DisplayName = "判断答题是否正确 JudgeAnswer()")] [Xunit.MemberData ("JudgeAnswer_TestData")] [Xunit.Trait ("业务", "答题")] [Xunit.Trait ("By", "robin")] public void JudgeAnswer_Test (JudgeAnswerInput input) { var result = controller.JudgeAnswer (input); //验证返回的结果状态是否等于1 result.retStatus_ShouldBe_1 (); } } }
右键xUnit项目,选择Test 即可运行测试。