为了简化问题,我简化了示例中的代码,但是基本上我正努力设计更多可测试的代码并将它们彼此隔离。
下面仅列出了两种方法,我已经概述了我需要做的测试。
代码示例如下:
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面向和可测试的最佳解决方案是什么。
使用装饰器或适配器将agentName转换为agentID,然后将此信息传递给基类以发布请求。
将私有方法更改为受保护的内部方法,让模拟框架替代GetAgentIDFromName()方法的实现,并为未模拟的任何方法简单地调用基本实现。
将ChangeAssignedAgent()方法的方法签名更改为改为引用获取agentID而不是名称,将GetAgentIDFromName()公开,并期望类的使用者使用它以便使用ChangeAssignedAgent ()方法。
第一种选择可能是解决此情况的最佳方法,但有消息告诉我这可能不是正确的解决方案,因为从技术上讲,基类具有误导性……它想要的是agentName……而不是ID。
第二种选择对我来说似乎更像是一种hack和代码味道,它正在有效地测试一种私有方法...我不确定...愿意提出建议。
最后,最后一个选择...就第二种选择而言,它在我看来可能是一种hack /代码气味,但它对我来说最合乎逻辑。但是,通过这种设计,您似乎永远无法拥有私有方法,这也可能会增加类的复杂性。
我是否想得太多?我很想听听一些建议...
答案 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等),并使每个类承担单个责任,还可以让我轻松地模拟测试中的依赖项