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;
}
不包含等待的方法。
我正在尝试使用 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;
}
不包含等待的方法。