【问题标题】:Moq unit testing access local variableMoq 单元测试访问局部变量
【发布时间】:2018-04-09 07:08:01
【问题描述】:

我正在尝试使用 Moq 为一些涉及 REST 请求的代码创建一个集成测试。

在常规使用中,代码会发出并创建报告记录,并对第 3 方系统产生其他影响。 通过 Moq 测试,来自 RestSharp IRestClient 的 Execute 调用可以替换为不执行任何操作的虚拟方法。对于成功的集成测试,有 2 个要求:(a) REQUEST xml 看起来正确 (b) 返回 RESPONSE json。我希望能够执行集成中涉及的大部分代码,并在 xUnit 代码断言中检查来自被测系统的局部变量。但是,我似乎无法使用 Moq 访问局部变量,除非我在测试周围添加一些代码工件。

我创建了两个项目来说明。希望你能指出我正确的方向。也许代码需要重构或者需要为 CommandHandler 创建一个新的 Mock 对象?

谢谢!

测试项目

using Mocking;  // System Under Test
using Moq;
using Newtonsoft.Json.Linq;
using RestSharp;
using System.Net;
using System.Threading;
using Xunit;

namespace MockingTest
{
    public class UnitTest1
    {
        [Fact]
        public async void SubmitReport_WithPerson_CanProcessSubmitSuccessfully()
        {
            // ----------------------------------------------------------------------------------
            // Arrange
            // ----------------------------------------------------------------------------------
            Person person = new Person();
            person.Name = "Test";
            string testRequestXML = GetTestRequestXML(person);
            string testResponseXML = "OK";

            // Construct the Mock Rest Client.  This should allow most of the submission process to be run - 
            // but the actual Execute to call CMS will not be done - instead the Mock framework will return 
            // an arbitrary response as defined below.
            var mockRestClient = new Mock<IRestClient>();
            RestResponse testRestResponse = GetTestRestResponse(System.Net.HttpStatusCode.OK, string.Empty, ResponseStatus.Completed, testResponseXML);
            mockRestClient.Setup(rc => rc.Execute(It.IsAny<IRestRequest>()))
                                       .Returns(testRestResponse);

            // ----------------------------------------------------------------------------------
            // Act
            // ----------------------------------------------------------------------------------
            Command command = new Command(person);
            CancellationToken cancellationToken = new CancellationToken();
            CommandHandler commandHandler = new CommandHandler(mockRestClient.Object);  // CommandHandler is the "System Under Test"

            string result = await commandHandler.Handle(command, cancellationToken);

            JToken responseToken = JToken.Parse(result);
            string responseXML = responseToken.SelectToken("response").ToString();
            string requestXML = responseToken.SelectToken("request").ToString();  // Normally this would not be available.

            // ----------------------------------------------------------------------------------
            // Assert
            // ----------------------------------------------------------------------------------
            Assert.Equal(requestXML, testRequestXML);                       // Handed back in JSON - normally this would not be the case.
            Assert.Equal(commandHandler.ReportXMLRequest, testRequestXML);  // Handed back in Property - normally this would not be the case.
        }

        private RestResponse GetTestRestResponse(HttpStatusCode httpStatusCode, string httpErrorMessage, ResponseStatus httpResponseStatus, string responseXML)
        {
            RestResponse testRestResponse = new RestResponse();
            testRestResponse.StatusCode = httpStatusCode;
            testRestResponse.ErrorMessage = httpErrorMessage;
            testRestResponse.ResponseStatus = httpResponseStatus;
            testRestResponse.Content = responseXML;
            return testRestResponse;
        }

        private string GetTestRequestXML(Person person)
        {
            // Sample XML.
            string xml = string.Empty;
            xml = xml + "<xml>";
            xml = xml + "<report>";
            xml = xml + "<status>" + "Initialized" + "</status>";
            xml = xml + "<person>" + person.Name + "</person>";
            xml = xml + "</report>";
            return xml;
        }
    }
}

正在测试的系统

using Newtonsoft.Json.Linq;
using RestSharp;
using System;
using System.Threading;
using System.Threading.Tasks;

// System Under Test
namespace Mocking
{
    public class Person
    {
        public string Name { get; set; }
    }

    public class ReportStatus
    {
        public string Status { get; private set; }

        public ReportStatus ()
        {
            this.Status = "Initialized";
        }
    }

    public class Report
    {
        public Person Person { get; private set; }

        public ReportStatus ReportStatus { get; private set; }

        public Report (Person person)
        {
            Person = person;
            ReportStatus = new ReportStatus();
        }
    }

    public class Command
    {
        public Person Person { get; private set; }

        public Command (Person person)
        {
            this.Person = person;
        }
    }
    public class CommandHandler
    {
        public string ReportXMLRequest { get; private set; }  //  Property to permit validation. 
        private readonly IRestClient RestClient;

        //// Using DI to inject infrastructure persistence Repositories - this is the normal call.
        //public CommandHandler(IMediator mediator, IReportRepository reportRepository, IIdentityService identityService)
        //{
        //    ReportXMLRequest = string.Empty;
        //    RestClient = new RestClient();
        //}

        // MOQ Addition - Overload constructor for Moq Testing.
        public CommandHandler(IRestClient restClient)
        {
            ReportXMLRequest = string.Empty;
            RestClient = restClient;
        }

        public async Task<string> Handle(Command command, CancellationToken cancellationToken)
        {
            Report report = new Report(command.Person);
            string reportResult = Submit(report);
            return reportResult;
        }

        private string Submit(Report report)
        {
            string responseXML = string.Empty;
            string localVariableForRequestXML = GetRequestXML(report);

            // MOQ Addition - Set Property to be able to inspect it from the integration test.
            this.ReportXMLRequest = localVariableForRequestXML;

            IRestClient client = RestClient;
            string baseType = client.GetType().BaseType.FullName;

            client.BaseUrl = new Uri("http://SampleRestURI");
            RestRequest request = new RestRequest(Method.POST);
            request.AddParameter("application/xml", localVariableForRequestXML, ParameterType.RequestBody);

            // Normally, this REST request would go out and create a Report record and have other impacts in a 3rd party system.
            // With Moq, the Execute call from the RestSharp IRestClient can be substituted for a dummy method.
            // For a successful INTEGRATION test, there are 2 requirements:
            //     (a) REQUEST xml looks correct (b) RESPONSE json is returned.
            **IRestResponse response = client.Execute(request);**
            responseXML = response.Content;

            // MOQ Addition - Do something... e.g. return JSON response with extra information.
            JObject json = null;
            if (baseType.ToLowerInvariant().Contains("moq"))
            {
                json = new JObject(
                    new JProperty("response", responseXML),
                    new JProperty("request", localVariableForRequestXML)
                    );
            }
            else
            {
                json = new JObject(new JProperty("response", responseXML));
            }

            string jsonResponse = json.ToString();
            return jsonResponse;
        }

        private string GetRequestXML(Report report)
        {
            // Sample XML - normally this would be quite complex based on Person and other objects.
            string xml = string.Empty;
            xml = xml + "<xml>";
            xml = xml + "<report>";
            xml = xml + "<status>" + report.ReportStatus.Status + "</status>";
            xml = xml + "<person>" + report.Person.Name + "</person>";
            xml = xml + "</report>";
            return xml;
        }
    }

}

【问题讨论】:

    标签: c# scope integration-testing moq


    【解决方案1】:

    除了设计不佳的主题和测试(这似乎更像是一个单元测试而不是集成测试),模拟的依赖项可用于检索提供的输入。

    您可以使用Callback

    //...code removed for brevity
    
    string requestXML = string.Empty;
    mockRestClient
        .Setup(_ => _.Execute(It.IsAny<IRestRequest>()))
        .Callback((IRestRequest request) => {
            var parameter = request.Parameters.Where(p => p.Name == "application/xml").FirstOrDefault();
            if(parameter != null && parameter.Value != null) {
                requestXML = parameter.Value.ToString();
            }
        })
        .Returns(testRestResponse);
    
    //...code removed for brevity
    
    
    Assert.Equal(requestXML, testRequestXML);
    

    或者直接在Returns委托中做同样的事情

    //...code removed for brevity
    
    string requestXML = string.Empty;
    mockRestClient
        .Setup(_ => _.Execute(It.IsAny<IRestRequest>()))
        .Returns((IRestRequest request) => {
            var parameter = request.Parameters.Where(p => p.Name == "application/xml").FirstOrDefault();
            if(parameter != null && parameter.Value != null) {
                requestXML = parameter.Value.ToString();
            }
    
            return testRestResponse;
        });
    
    //...code removed for brevity
    
    
    Assert.Equal(requestXML, testRequestXML);
    

    无需专门为测试目的修改被测对象。注入的抽象应该足以通过模拟提供对所需变量的访问。

    在被注释掉的主题构造函数中

    RestClient = new RestClient(); /<-- don't do this
    

    不应这样做,因为它将类与其余客户端紧密耦合。也不需要过载。将抽象移动到初始构造函数。它已经在接受抽象了。

    // Using DI to inject infrastructure persistence Repositories - this is the normal call.
    public CommandHandler(IMediator mediator, IReportRepository reportRepository, 
        IIdentityService identityService, IRestClient restClient) {
    
        RestClient = restClient;
    
        //...assign other local variables
    }
    

    如果测试是异步的,那么让它返回 Task 而不是 async void

    public async Task SubmitReport_WithPerson_CanProcessSubmitSuccessfully() {
        //...
    }
    

    但鉴于主题看起来不完整,不确定它实际上是使用异步流作为以下方法

    public async Task<string> Handle(Command command, CancellationToken cancellationToken)
    {
        Report report = new Report(command.Person);
        string reportResult = Submit(report);
        return reportResult;
    }
    

    不包含等待的方法。

    【讨论】:

    • 非常感谢恩科西!您的建议适用于 Returns 重载 - 我还将研究您的其他重构建议。非常感激!对于其他想知道带起订量的 Returns() 的人 - (geekswithblogs.net/abhi/archive/2013/11/18/…) 很有用。
    • @AdrianWarbeck 参考Moq Quickstart 以更好地了解如何使用模拟框架。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-07-29
    • 1970-01-01
    • 1970-01-01
    • 2015-04-25
    相关资源
    最近更新 更多