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

时间:2018-11-27 23:08:45

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

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

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

  1. 验证是否使用了正确的URL
  2. 验证是否使用了适当的标头,例如请求是否为JSON。
  3. 验证是否使用了POST请求(我有一个HttpMessageHandler,并使用委托作为实际代码中的依赖项来拦截和模拟互联网)
  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 /代码气味,但它对我来说最合乎逻辑。但是,通过这种设计,您似乎永远无法拥有私有方法,这也可能会增加类的复杂性。

我是否想得太多?我很想听听一些建议...

2 个答案:

答案 0 :(得分:1)

您总是必须模拟所需的依赖项。

这里的一个挑战是IHttpService的功能类似于服务定位器。您对其的要求或其响应均未严格键入。这使它成为一种依赖关系,您可以从技术上要求任何事情或告诉做任何事情,这就是为什么我将其与服务定位器进行比较。

如果您有一个完全符合您的班级需要的强类型接口,则将有所帮助。因为您有两个请求,所以它可能是带有两个方法的接口,或者是两个完全独立的接口。您还可以使用委托。

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

首先是一个抽象,仅说明您要执行的操作。没有实现细节,也没有提及Rest API。 (我给它起的名字很la脚。几年前,我将其命名为IHttpService,但更通用。)

ITicketService

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

然后,实现可以是HttpClient。我使用了RestSharp NuGet软件包。我尚未对此进行测试(由于我没有API,因此无法进行测试),因此请以此为起点。它的作用是在很大程度上消除了测试某些测试内容的需要。

您可以使用任何其他HTTP客户端库执行此操作。我之所以使用它是因为它很熟悉,而且我知道它可以在后台正确地处理HTTP客户端的创建和处理。 (这并不是每次使用时都要创建和处理它们。)

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

查看您要测试的内容:

它使用正确的URL吗?
您不需要进行测试,因为已注入URL。它不是来自此类。它使用您告诉它的任何URL。

这也解决了URL将被硬编码的问题。您可以为开发人员配置一个,为生产环境配置一个,等等,然后根据环境注入正确的配置。因为此类是一个普通的类,所以它需要知道URL的其他部分,但是它将使用您告诉它的任何基本URL。

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

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

见上文。

现在,无论哪个类需要更新票证,它都可以依赖于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); } } ,这真的很容易模拟。

对于测试ITicketRepository,不再有任何可模仿的东西。唯一要做的就是使用HTTP与API通讯,因此您将使用集成测试对其进行测试,将其指向该API的实际实例,并验证您是否获得了预期的结果。集成测试涵盖了诸如标头和HTTP方法是否正确之类的东西。

答案 1 :(得分:0)

我想出了一种设计,可以使我做自己想做的事,而不会说任何“骇客”。

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等),并使每个类承担单个责任,还可以让我轻松地模拟测试中的依赖项