基于最小起订量框架的Web服务方法的模拟请求和响应

Mock Request and Response of Web service method with Moq Framework

提问人:Rocker2982 提问时间:11/9/2023 更新时间:11/9/2023 访问量:59

问:

总结:我希望能够模拟对远程服务器进行调用的 SOAP Web 方法的请求和响应。我希望能够测试此 SOAP Web 方法 CustomerCareResult 的整个请求和响应功能,而无需进行远程服务器调用。这意味着我将不得不使用嘲笑。我想使用 C#.NET 中的 Moq 框架模拟 CustomerCareResult 的传入请求和响应输出。我是用 C# 编写模拟测试的全新手,弄清楚这一点对我来说非常令人沮丧。任何帮助将不胜感激。

注意:这里有一些伪代码,因为整体代码非常复杂,我只想大致了解它在做什么,而不添加不必要的细节。

我有一个包含SOAP WebMethod和其他2个方法的C#.NET类,如下所示:

public class TransactionService : System.Web.Services.WebService, 
TransactionServiceNS.ITransactionServiceSoap
{
    //This is a SOAP webservice method
    [WebMethod()]
    public CustomerPutResponse CustomerCareResult(CustomerRequest request)
    {
         //The incoming request parameter of type 'CustomerRequest' 
         //contains the fields: Name, Address, City, State, and AccountNumber
         
         //Call the ApiCreateRequestObj method, passing in the request parameter. This makes 
         //a call to a remote server:
         var apiRequestObj = ApiCreateRequestObj(request);

         //Call the ApiCreateResponseObj method, passing in apiRequestObj, this makes
         //a call to a remote server:
         var apiResponseObj = ApiCreateResponseObj(apiRequestObj );
         return apiResponseObj;
    }

    private CustomerPutRequest ApiCreateRequestObj(CustomerRequest req)
    {
        // PSEUDOCODE:
        //Makes a remote server call, does some processing on the 
       //CustomerRequest req parameter, and returns a "CustomerPutRequest" 
      //object containing all the fields of the req object
        
        //So after processing:
        return new CustomerPutRequest{Name = _req.Name, Address = _req.Address,
        City = _req.City, State = _req.State}
    }

    private CustomerPutResponse ApiCreateResponseObj(CustomerPutRequest cpReq)
    {
        // PSEUDOCODE:
        //Makes a remote server call, does some processing on the cpReq object and returns a 
        //"CustomerPutResponse" object containing the response data
        

        //So after processing:
        return new CustomerPutResponse{/*contains response data fields*/}
    }
}

如您所见,SOAP Web 方法 CustomerCareResult 接受一个“CustomerRequest”参数,然后调用一个进行远程数据库调用的方法 ApiCreateRequestObject()。这将执行一些处理并返回一个“CustomerPutRequest”对象,该对象包含传入的 CustomerRequest 对象的字段。

然后,我将此 CustomerPutRequest 对象传递给 ApiCreateResponseObject() 方法,该方法又进行远程服务器调用,自行进行一些处理,然后返回包含响应数据的“CustomerPutResponse”对象,该对象由 Web 方法 CustomerCareResult 返回。

我想使用 C#.NET 中的 Moq 框架模拟 Web 方法 CustomerCareResult 的传入请求和响应输出。我是用 C# 编写模拟测试的全新手,弄清楚这一点对我来说非常令人沮丧。任何帮助将不胜感激。

C# SOAP 请求 响应 最小起订量

评论

0赞 stuartd 11/9/2023
你可能不必使用模拟 - 你可以编写一个 Web 服务的“假”实现并将其用于你的测试 - tyrrrz.me/blog/fakes-over-mocks

答:

0赞 jarmanso7 11/9/2023 #1

为了在不对外部服务进行实际调用的情况下测试类 TransactionService,请确保在伪调用中使用的服务位于接口后面。如果不是这种情况,您可能需要重构源代码以使其可测试。

public class TransactionService
{
    private readonly IDatabaseService _databaseService;

    public TransactionService(IDatabaseService databaseService)
    {
        _databaseService = databaseService;
    }

    public CustomerPutResponse CustomerCareResult(CustomerRequest request)
    {
        var apiRequestObj = ApiCreateRequestObj(request);

        var apiResponseObj = ApiCreateResponseObj(apiRequestObj);
        
        return apiResponseObj;
    }

    private CustomerPutRequest ApiCreateRequestObj(CustomerRequest req)
    {
        return _databaseService.createPutRequest(req);
    }

    private CustomerPutResponse ApiCreateResponseObj(CustomerPutRequest resp)
    {
        return _databaseService.createPutResponse(resp);
    }
}

如果要测试 TransactionService 内部的逻辑,而不是外部 DatabaseService 内部的逻辑,则应使用 TransactionService 中的 IDatabaseService,并从测试中模拟接口 IDatabaseService。真正的实现 DatabaseService 只会在您的应用程序中运行,但您的测试不会通过它:

public interface IDatabaseService
{
    public CustomerPutRequest createPutRequest(CustomerRequest request);

    public CustomerPutResponse createPutResponse(CustomerPutRequest putRequest); 
}

public class DatabaseService : IDatabaseService
{
    public CustomerPutRequest createPutRequest(CustomerRequest request)
    {
        //PSEUDOCODE: Real code for your real service that returns a customerPutRequest
    }

    public CustomerPutResponse createPutResponse(CustomerPutRequest putRequest)
    {
        //PSEUDOCODE: Real code for your real service that returns a customerPutResponse
    }
}

最后,在测试类中(在此示例中使用 xUnit),应测试类的公共方法,在本例中为 CustomerCareResult 方法:

public class TransactionServiceTests
{
    [Fact]
    public void CustomerCareResult_UponCalled_ReturnsExpectedResponse()
    {
        // Arrange (instantiate input parameters, mocks and the object to test (sut))
        var customerRequest = new CustomerRequest
        {
            Name = "Joe Biden",
            Address = "White House",
            City = "Washington DC",
            State = "Columbia",
            AccountNumber = "666"
        };

        //Create the mock for the external service we don't really want to call
        var dataServiceMock = new Mock<IDatabaseService>();

        //Set up the mock to behave as we would expect the real class to behave when calling its methods.
        //In this case, to return the expected value whenever the external IDatabaseService.createPutRequest is called
        dataServiceMock .Setup(m => m.createPutRequest(It.IsAny<CustomerRequest>()))
                        .Returns(new CustomerPutRequest
                        {
                            Name = "Joe Biden",
                            Address = "White House",
                            City = "Washington DC",
                            State = "Columbia"
                        });

        dataServiceMock.Setup(m => m.createPutResponse(It.IsAny<CustomerPutRequest>()))
                        .Returns(new CustomerPutResponse
                        {
                            Data = "this is the mocked data I expect my real external service to return"
                        });

        var sut = new TransactionService(dataServiceMock.Object);

        // Act (call a method on the sut)
        sut.CustomerCareResult(customerRequest);

        // Assert (verify that each of the 2 the methods in the IDatabaseService mock have been called exactly once)
        dataServiceMock.Verify(m => m.createPutRequest(It.IsAny<CustomerRequest>()), Times.Once());
        dataServiceMock.Verify(m => m.createPutResponse(It.IsAny<CustomerPutRequest>()), Times.Once());
    }
}

在测试方法中,使用 Moq 为服务 IDatabaseService 创建 2 个模拟。在设置部分,我们告诉 Moq,当 TransactionService 调用 _databaseService.createPutRequest(req) 和 _databaseService.createPutResponse(resp) 时,由于调用公共方法 CustomerCareResult,我们希望模拟如何响应。

在测试的最后一部分,我们断言每个方法都调用了一次。当然,如果 CustomerCareResult 中有更复杂的逻辑,则根据请求的信息,可能会为同一方法 CustomerCareResult 编写更多具有不同断言的测试。

评论

0赞 jarmanso7 11/9/2023
@Rocker2982,我已将带有示例代码的解决方案上传到 GitHub,以防您想查看它: github.com/jarmanso7/TestAClassWithMoqExample
0赞 user16606026 11/9/2023 #2

这可能取决于您如何进行远程服务器调用。例如,我使用的一个项目 - 它没有接口(通常你模拟接口而不是实际实现),所以我进一步将其抽象为提供等,然后使用下面的实现。然后我嘲笑我创造的。HttpClientIHttpClientService.Get().Post()HttpClientIHttpClientService

How to mock test

因此,在您的问题中,是您的测试范围,实际代码取决于可以向外部服务发出请求的东西(在您的代码示例中省略了)。这就是你想嘲笑的。TransactionService

当然,第 3 点可能表明另一个问题:通常不应该为了单元测试而更改代码。但在这种情况下,提供 的抽象可以帮助使 HTTP 请求更容易,而且因为它本身有点历史,HttpClient 的实现阻止了单元测试,所以在考虑之后,我认为用于发出 HTTP 请求的抽象对于我的项目来说是可以接受的。HttpClient