首页 > 解决方案 > Simple example of a complex situation in Unit Testing, how should this be designed?

问题描述

I've simplified the code in the example to keep the question shorter, but basically I am struggling with designing more testable code and isolating them from each other.

With just two methods here listed below I've outlined exactly what tests I need to do.

  1. Verify that the correct URL was utilized
  2. Verify the appropriate headers were used, such as the request was in JSON.
  3. Verify a POST request was used (I have a HttpMessageHandler and using delegates to intercept and mock out the internet as a dependency in the actual code)
  4. Verify that the serialized JSON value doesn't have any extra properties not filled out.

The code example is below:

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>();
    }
}

In theory, these tests should be completely isolated from each other. But they're not because in each test, I must setup and configure GetAgentFromName() even when it is not relevant.

Here are my ideas in fixing this problem, but I am not sure what the best resolution would be to make something more SRP oriented and testable.

  1. Utilize a decorator or adapter to simply convert the agentName to an agentID, then pass this information to the base class to post the request.

  2. Change the private method to protected internal, have a mocking framework replace the implementation of the GetAgentIDFromName() method, and simply call the base implementation for any method that's not been mocked.

  3. Change the method signature of the ChangeAssignedAgent() method to instead refer to getting an agentID instead of a name, make the GetAgentIDFromName() public and expect the consumers of your class to utilize it in order to use the ChangeAssignedAgent() method.

It's possible that the first option is the best way to tackle this scenario, but something tells me it may not be the right solution because technically the base class is misleading... it wants an agentName... not an ID.

The second option seems more of a hack and code smell to me, it is effectively testing a private method... I am not sure... open for suggestions.

Lastly, the last option... it's similar to the second option in terms how I feel it may be a hack/code smell but it makes most logical sense to me. However, with this design it feels like you can never have private methods and it may also increase complexity of your class.

Am I overthinking this? I would love to hear some suggestions...

标签: c#.netunit-testingoopsingle-responsibility-principle

解决方案


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

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

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

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

首先是一个抽象,它只是说明你想要做什么。没有实现细节或提及 Rest API。(我给它起的名字很蹩脚。几年前我会叫它,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 格式。
你不需要测试它,因为它是由库处理的。即使您使用的是 .NET 框架类,我认为这不是您需要测试的,因为您将测试框架,而不是您自己的代码。这类事情可以在集成测试中处理,以确保一切都是端到端的。

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

看上面。

现在,无论哪个班级需要更新一张票,它都可以取决于ITicketRepository哪个真的很容易模拟。

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


推荐阅读