简体   繁体   中英

Moq unit testing access local variable

I am trying to create an INTEGRATION Test using Moq for some code that involves a REST request.

In regular usage, the code would go out and create a Report record and have other impacts in a 3rd party system. With the Moq test, the Execute call from the RestSharp IRestClient can be substituted for a dummy method that does nothing. For a successful INTEGRATION test, there are 2 requirements: (a) REQUEST xml looks correct (b) RESPONSE json is returned. I'd like to be able to execute most of the code involved in the integration and inspect a local variable from the system under test in the xUnit code assertions. However, I can't seem to access the local variable using Moq, unless I add some code artifacts around testing.

I have created two projects to illustrate. Hope you can point me in the right direction. Perhaps the code needs to be restructured or a new Mock object for the CommandHandler needs to be created?

Thanks!

TEST PROJECT

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

SYSTEM UNDER TEST

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

}

Apart from the poorly designed subject and test, ( which appears to be more of a unit test than an integration test ), the mocked dependency can be used to retrieve the provided input.

You can either use a 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);

Or do the same directly in the Returns delegate

//...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);

There is no need to modify the subject under test specifically for the purposes of testing. The injected abstraction should be enough to provided access to desired variables via the mock.

In the commented out constructor of the subject

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

should not be done as it tightly couples the class to the rest client. There is also no need for the overload. Move the abstraction to the initial constructor. It is already accepting abstractions.

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

If the test is meant to be async then have it return a Task and not async void

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

But given that the subject appears incomplete, it is not certain that it is actually using async flow as the following method

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

contains no awaited methods.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM