C# 单元测试中复杂情况的简单示例,应该如何设计?

C# 单元测试中复杂情况的简单示例,应该如何设计?,c#,.net,unit-testing,oop,single-responsibility-principle,C#,.net,Unit Testing,Oop,Single Responsibility Principle,我简化了示例中的代码以缩短问题,但基本上我正在努力设计更多可测试代码并将它们彼此隔离 下面列出了两种方法,我已经准确地概述了我需要做的测试 验证是否使用了正确的URL 验证是否使用了适当的头,例如请求是JSON格式的。 验证是否使用了POST请求我有一个HttpMessageHandler,并使用委托在实际代码中拦截和模拟internet作为依赖项 验证序列化的JSON值没有任何未填写的额外属性。 代码示例如下所示: 类RESTAPI { 私人IHttpService(ihuwebservice

我简化了示例中的代码以缩短问题,但基本上我正在努力设计更多可测试代码并将它们彼此隔离

下面列出了两种方法,我已经准确地概述了我需要做的测试

验证是否使用了正确的URL 验证是否使用了适当的头,例如请求是JSON格式的。 验证是否使用了POST请求我有一个HttpMessageHandler,并使用委托在实际代码中拦截和模拟internet作为依赖项 验证序列化的JSON值没有任何未填写的额外属性。 代码示例如下所示:

类RESTAPI { 私人IHttpService(ihuwebservice),; public void changesassignedAgenticket票据,字符串代理名称 { 字符串agentID=getagentId fromNameagentName; _webService.PostRequest$https://localhost/{agentID},新的StringContentJsonConvert.SerializeObjectticket,Encoding.UTF8,application/json; } 私有字符串GetAgentIDFromNamestring agentName { 返回JsonConvert.DeserializeObject\u webService.GetStringContent$https://localhost/{agentName}[sys\u id]。值; } } 理论上,这些测试应该彼此完全隔离。 但这并不是因为在每个测试中,我必须设置和配置GetAgentFromName,即使它不相关

下面是我解决这个问题的想法,但我不确定最好的解决方案是什么,以使某些东西更加面向SRP和可测试

利用装饰器或适配器将agentName简单地转换为agentID,然后将此信息传递给基类以发布请求

将private方法更改为protectedinternal,使用模拟框架替换GetAgentIDFromName方法的实现,并简单地为任何未被模拟的方法调用基本实现

将ChangeAssignedAgent方法的方法签名改为引用获取agentID而不是名称,将GetAgentIDFromName公开,并期望类的使用者使用它以使用ChangeAssignedAgent方法

第一种选择可能是解决这种情况的最佳方法,但某种情况告诉我,它可能不是正确的解决方案,因为从技术上讲,基类是误导性的。。。它需要一个代理名。。。没有身份证

第二个选项在我看来更像是黑客和代码的味道,它有效地测试了一个私有方法。。。我不确定。。。欢迎提出建议

最后,最后一个选择。。。这与第二个选项类似,我觉得这可能是一种黑客/代码的味道,但对我来说,这是最合乎逻辑的。然而,这种设计让人觉得您永远无法拥有私有方法,而且它还可能增加类的复杂性


我是不是想得太多了?我很想听到一些建议…

你总是要嘲笑你需要的依赖关系

这里的一个挑战是IHttpService的功能类似于服务定位器。您向它请求的内容或它的响应都不是强类型的。这使得它成为一种依赖关系,从技术上讲,您可以要求任何东西,也可以告诉您做任何事情,这就是为什么我将它与服务定位器进行比较的原因

如果您有一个完全满足类需要的强类型接口,而不是IHttpService,这将有所帮助。因为您有两个请求,所以它可以是具有两个方法的接口,也可以是两个完全独立的接口。也可以使用委托

这里有一个粗略的方法,可能会有所帮助,或者可能会引发其他事情

首先是一个抽象概念,它只说明了你想做什么。没有实现细节,也没有提到RESTAPI。我给它起的名字很蹩脚。几年前,我会称之为ITicketService,但这更通用

public interface ITicketRepository
{
    void ChangeAssignedAgent(ITicket ticket, string agentName);
    string GetAgentIDFromName(string agentName);
}
我将第二种方法作为接口的一部分。您需要单独测试它,或者能够单独模拟它,这样就完成了。如果您不希望它成为同一接口的一部分,您可以创建另一个接口。我还喜欢使用多个委托,而不是单个接口。更多关于这个

然后实现可以是一个HttpClient。我用的是RestSharp NuGet软件包。我没有测试过这个,也不能,因为我没有API,所以把它作为一个起点。它所做的是,在很大程度上,消除了测试您将要测试的部分内容的需要

您可以使用任何其他HTTP客户端库来实现这一点。我之所以使用它,是因为它很熟悉,而且我知道它在幕后以正确的方式处理HTTP客户端的创建和处理。它不是在每次使用时创建和处理它们

public class HttpClientTicketRepository : ITicketRepository
{
    private readonly string _baseUrl;

    public HttpClientTicketRepository(string baseUrl)
    {
        if(string.IsNullOrEmpty(baseUrl))
            throw new ArgumentException($"{nameof(baseUrl)} cannot be null or empty.");
        _baseUrl = baseUrl;
    }

    public void ChangeAssignedAgent(ITicket ticket, string agentName)
    {
        var agentId = GetAgentIDFromName(agentName);
        var client = new RestClient(_baseUrl);
        var request = new RestRequest(agentId, dataFormat:DataFormat.Json);
        request.AddJsonBody(ticket);
        client.Post(request);
    }
}
看看你想测试的东西:

它是否使用正确的URL? 您不需要进行测试,因为URL是注入的。它不是从这个班来的。它使用你告诉它的任何URL。

这也解决了URL硬编码的问题。您可以有一个用于开发,一个用于生产,等等,并根据环境注入正确的一个。因为这个类充当客户端,所以它需要知道URL的其他部分,但它将使用您告诉它的任何基本URL

验证是否使用了适当的头,例如请求是JSON格式的。 您不需要测试它,因为它是由库处理的。即使您使用的是.NETFramework类,我也不认为这是您需要测试的东西,因为您将测试框架,而不是您自己的代码。这类事情可以在集成测试中处理,以确保一切都端到端地工作

验证是否使用了POST请求我有一个HttpMessageHandler,并使用委托在实际代码中拦截和模拟internet作为依赖项 验证序列化的JSON值没有任何未填写的额外属性。 见上文

现在,无论什么类需要更新票证,它都可以依赖于ITicketRepository,这非常容易模仿


至于测试HttpClientTicketPropository,已经没有什么可嘲笑的了。这样做的唯一一件事就是使用HTTP与API对话,因此您可以使用集成测试对其进行测试,将其指向API的实际实例,并验证您是否获得了预期的结果。集成测试涵盖了诸如头文件和HTTP方法是否正确之类的内容。

我提出了一种设计,它允许我做我想做的事情,而无需任何黑客攻击

public class TicketService
{
    private readonly IHttpService _httpService;

    public TicketService(IHttpService httpService)
    {
        _httpService = httpService;
    }

    public async Task CreateNewTicket()
    {
        var message = new TicketRESTLinks().CreateNewTicket("Sample");
       await _httpService.SendMessage(message);
    }
}
public class HttpService : IHttpService, IDisposable
{
    private readonly HttpClient _client = new HttpClient();

    public Task<HttpResponseMessage> SendMessage(HttpRequestMessage message)
    {
        if (message.Method == HttpMethod.Get)
        {
            return _client.GetAsync(message.RequestUri);
        }
        if (message.Method == HttpMethod.Post)
        {
            return _client.PostAsync(message.RequestUri, message.Content);
        }
        if (message.Method == HttpMethod.Get)
        {
            return _client.DeleteAsync(message.RequestUri);
        }
        if (message.Method == HttpMethod.Patch)
        {
            return _client.PatchAsync(message.RequestUri, message.Content);
        }
        else
        {
            throw new InvalidOperationException();
        }
    }

    public void Dispose()
    {
        _client.Dispose();
    }
}

public interface IHttpService
{
    Task<HttpResponseMessage> SendMessage(HttpRequestMessage message);
}

public class TicketRESTLinks
{


    public HttpRequestMessage CreateNewTicket(string description)
    {
      return new  HttpRequestMessage()
        {
          Content =  new StringContent("sample JSON"),
            Method = HttpMethod.Post,
            RequestUri =  new Uri("https://localhost/api/example"),


        };
    }
}

这使我能够单独测试REST配置是否正确,即它必须是一个POST等,并使每个类都有一个单独的责任,它还允许我在测试中轻松模拟依赖关系

感谢Scott,有点困惑。您提供了两个具有相同签名但无法编译的方法。在实际的类中,我使用了一个基本URL,我只是在这里省略了,以使代码示例尽可能简单,因为这不是我的问题的重点。我已经为我提到的所有4个步骤编写了单元测试。我意识到人们对需要测试的内容有不同的看法,但我只是想知道如何在调用私有方法时使事情更具可模仿性。对不起,在VS和SO之间复制代码时出现了错误。修正。早上好,斯科特,我发布了一个我觉得解决了我问题的答案。除了修复明显的like头和更好的依赖注入以及空对象客户机/延迟加载之外,您对这个解决方案有何感想?