Moq 单元测试访问局部变量

Moq unit testing access local variable

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

在常规使用中,代码会出去并创建一个报告记录,并在第 3 方系统中产生其他影响。 通过 Moq 测试,来自 RestSharp IRestClient 的 Execute 调用可以替代不执行任何操作的虚拟方法。对于成功的 INTEGRATION 测试,有 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;
        }
    }

}

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

您可以使用 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

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

// 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
}

如果测试是异步的,那么让它 return 一个 Task 而不是 async void

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

但鉴于题目看起来不完整,不能确定它是否真的使用async flow作为以下方法

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

不包含等待的方法。