【问题标题】:Simple example of a complex situation in Unit Testing, how should this be designed?单元测试中复杂情况的简单示例,应该如何设计?
【发布时间】:2018-11-27 23:08:44
【问题描述】:

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

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

  1. 验证是否使用了正确的 URL
  2. 验证是否使用了适当的标头,例如请求是 JSON 格式。
  3. 验证是否使用了 POST 请求(我有一个 HttpMessageHandler 并使用委托来拦截和模拟 Internet 作为实际代码中的依赖项)
  4. 验证序列化的 JSON 值没有任何未填写的额外属性。

代码示例如下:

class RESTAPI
{
    private IHttpService _webService;

    public void ChangeAssignedAgent(ITicket ticket, string agentName)
    {
        string agentID = GetAgentIDFromName(agentName);
        _webService.PostRequest($"https://localhost/{agentID}", new StringContent(JsonConvert.SerializeObject(ticket), Encoding.UTF8,"application/json"));
    }

    private string GetAgentIDFromName(string agentName)
    {
        return JsonConvert.DeserializeObject<JObject>(_webService.GetStringContent($"https://localhost/{agentName}"))["sys_id"].Value<string>();
    }
}

理论上,这些测试应该彼此完全隔离。 但不是因为在每次测试中,我都必须设置和配置 GetAgentFromName(),即使它不相关。

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

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

  2. 将私有方法更改为受保护的内部方法,让一个模拟框架替换 GetAgentIDFromName() 方法的实现,并简单地调用任何未被模拟的方法的基本实现。

  3. 更改 ChangeAssignedAgent() 方法的方法签名,改为引用获取 agentID 而不是名称,公开 GetAgentIDFromName() 并期望您的类的使用者使用它来使用 ChangeAssignedAgent () 方法。

第一个选项可能是解决这种情况的最佳方法,但有些事情告诉我这可能不是正确的解决方案,因为从技术上讲,基类具有误导性......它需要一个 agentName......而不是一个 ID。

第二个选项对我来说似乎更像是一种 hack 和代码味道,它正在有效地测试一种私有方法...我不确定...欢迎提出建议。

最后,最后一个选项...它与第二个选项相似,我觉得它可能是一种 hack/code 味道,但它对我来说最合乎逻辑。然而,这种设计感觉就像你永远不能拥有私有方法,而且它还可能增加你的类的复杂性。

我是不是想多了?我很想听听一些建议...

【问题讨论】:

    标签: c# .net unit-testing oop single-responsibility-principle


    【解决方案1】:

    你总是要模拟你需要的依赖。

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

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

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

    首先是一个抽象,它只是说明你想要做什么。没有实现细节或提及 Rest API。 (我给它起的名字很蹩脚。几年前我会叫它ITicketService,但这更通用。)

    public interface ITicketRepository
    {
        void ChangeAssignedAgent(ITicket ticket, string agentName);
        string GetAgentIDFromName(string agentName);
    }
    

    我将第二个方法作为界面的一部分。您将要单独测试它或能够单独模拟它,以便完成它。如果您不希望它成为同一界面的一部分,您可以随时创建另一个。我也喜欢使用多个委托而不是单个接口。更多关于 here.

    那么实现可以是一个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。

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

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

    见上文。

    现在,无论课程需要更新票证,它都可以只依赖于 ITicketRepository,这真的很容易模拟。

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

    【讨论】:

    • 谢谢斯科特,有点困惑。您提供了两种具有相同签名但无法编译的方法。在我使用基本 URL 的真实课程中,我只是省略了此处以使代码示例尽可能简单,因为这不是我的问题所关注的。我已经为我提到的所有 4 个步骤编写了单元测试。我意识到人们对究竟需要测试什么有不同的看法,但只是对如何在调用私有方法时使事情更可模拟的想法持开放态度。
    • 对不起,在 VS 和 SO 之间复制代码时出错。固定。
    • 早上,斯科特,我发布了一个我认为解决了我的问题的答案。除了修复明显的类似标头和更好的依赖注入以及可能的空对象客户端/延迟加载之外,您对这个解决方案有何看法?
    【解决方案2】:

    我想出了一个设计,让我可以做我想做的事,而不用说任何“黑客”。

    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 等)并使每个类承担单一职责,它还允许我在测试中轻松模拟依赖关系

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-30
      • 1970-01-01
      相关资源
      最近更新 更多