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 即可运行测试。

posted @ 2019-06-12 17:18  OhMyJie  阅读(718)  评论(0编辑  收藏  举报