C# 单元测试中复杂情况的简单示例,应该如何设计?
我简化了示例中的代码以缩短问题,但基本上我正在努力设计更多可测试代码并将它们彼此隔离 下面列出了两种方法,我已经准确地概述了我需要做的测试 验证是否使用了正确的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方法 第一种选择可能是解决这种情况的最佳方法,但某种情况告诉我,它可能不是正确的解决方案,因为从技术上讲,基类是误导性的。。。它需要一个代理名。。。没有身份证 第二个选项在我看来更像是黑客和代码的味道,它有效地测试了一个私有方法。。。我不确定。。。欢迎提出建议 最后,最后一个选择。。。这与第二个选项类似,我觉得这可能是一种黑客/代码的味道,但对我来说,这是最合乎逻辑的。然而,这种设计让人觉得您永远无法拥有私有方法,而且它还可能增加类的复杂性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
我是不是想得太多了?我很想听到一些建议…你总是要嘲笑你需要的依赖关系 这里的一个挑战是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头和更好的依赖注入以及空对象客户机/延迟加载之外,您对这个解决方案有何感想?