Модульное тестирование HTTP-запросов в С#
Я пишу код, который вызывает веб-сервис, читает ответ и что-то делает с ним. Мой код выглядит номинально следующим образом:
string body = CreateHttpBody(regularExpression, strategy);
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_url);
request.Method = "POST";
request.ContentType = "text/plain; charset=utf-8";
using (Stream requestStream = request.GetRequestStream())
{
requestStream.Write(Encoding.UTF8.GetBytes(body), 0, body.Length);
requestStream.Flush();
}
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
byte[] data = new byte[response.ContentLength];
using (Stream stream = response.GetResponseStream())
{
int bytesRead = 0;
while (bytesRead < data.Length)
{
bytesRead += stream.Read(data, bytesRead, data.Length - bytesRead);
}
}
return ExtractResponse(Encoding.UTF8.GetString(data));
}
Единственные части, в которых я фактически выполняю произвольные манипуляции, находятся в методах ExtractResponse
и CreateHttpBody
. Однако это не так просто unit test эти методы, и надеемся, что остальная часть кода будет собрана правильно. Можно ли каким-либо образом перехватить HTTP-запрос и подавать его вместо данных?
EDIT. Эта информация устарела. Гораздо проще построить такой код, используя библиотеки System.Net.Http.HttpClient.
Ответы
Ответ 1
В коде вы не можете перехватить вызовы на HttpWebRequest
, потому что вы создаете объект в том же методе. Если вы разрешите другому объекту создать HttpWebRequest
, вы можете передать объект-макет и использовать его для тестирования.
Итак, вместо этого:
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_url);
Используйте это:
IHttpWebRequest request = this.WebRequestFactory.Create(_url);
В unit test вы можете передать WebRequestFactory
, который создает макет.
Кроме того, вы можете разделить код чтения потока в отдельной функции:
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
byte[] data = ReadStream(response.GetResponseStream());
return ExtractResponse(Encoding.UTF8.GetString(data));
}
Это позволяет тестировать ReadStream()
отдельно.
Чтобы выполнить больше теста интеграции, вы можете настроить свой собственный HTTP-сервер, который возвращает тестовые данные, и передать URL-адрес этого сервера вашему методу.
Ответ 2
Я бы, вероятно, начинал с рефакторинга кода, чтобы сделать его более слабо связанным с фактическим HTTP-запросом. Прямо сейчас этот код, похоже, выполняет очень много.
Это можно сделать, введя абстракцию:
public interface IDataRetriever
{
public byte[] RetrieveData(byte[] request);
}
Теперь класс, который вы пытаетесь использовать unit test, может быть отделен от фактического запроса HTTP с использованием шаблона проектирования инверсии управления:
public class ClassToTest
{
private readonly IDataRetriever _dataRetriever;
public Foo(IDataRetriever dataRetriever)
{
_dataRetriever = dataRetriever;
}
public string MethodToTest(string regularExpression, string strategy)
{
string body = CreateHttpBody(regularExpression, strategy);
byte[] result = _dataRetriever.RetrieveData(Encoding.UTF8.GetBytes(body));
return ExtractResponse(Encoding.UTF8.GetString(result));
}
}
Это уже не ответственность ClassToTest за фактический HTTP-запрос. Теперь он отделен. Тестирование MethodToTest
становится тривиальной задачей.
И последняя часть, очевидно, должна иметь реализацию абстракции, которую мы ввели:
public class MyDataRetriever : IDataRetriever
{
private readonly string _url;
public MyDataRetriever(string url)
{
_url = url;
}
public byte[] RetrieveData(byte[] request)
{
using (var client = new WebClient())
{
client.Headers[HttpRequestHeader.ContentType] = "text/plain; charset=utf-8";
return client.UploadData(_url, request);
}
}
}
Затем вы можете настроить свою любимую структуру DI для вставки экземпляра MyDataRetriever
в конструктор класса ClassToTest
в вашем фактическом приложении.
Ответ 3
Если высмеивать HttpWebRequest и HttpWebResponse становится слишком громоздким, или если вам когда-либо понадобится протестировать код в приемочном тесте, где вы вызываете свой код из "внешнего", то создание поддельного сервиса, вероятно, является наилучшим способом перейти.
Я действительно написал библиотеку с открытым исходным кодом под названием MockHttpServer, чтобы помочь с этим, издеваются над любыми внешними службами, которые обмениваются данными через HTTP.
Вот пример использования его с RestSharp для вызова конечной точки API с ним, но HttpWebRequest будет работать так же хорошо.
using (new MockServer(3333, "/api/customer", (req, rsp, prm) => "Result Body"))
{
var client = new RestClient("http://localhost:3333/");
var result = client.Execute(new RestRequest("/api/customer", Method.GET));
}
На странице GitHub есть довольно подробное чтение, которое проходит через все доступные для его использования опции, а сама библиотека доступна через NuGet.
Ответ 4
Если вы с удовольствием перейдете на HttpClient (официальная, переносная, клиентская клиентская библиотека), я написал библиотеку некоторое время назад что может помочь вызвать MockHttp. Он предоставляет свободный API, который позволяет вам отвечать на запросы, согласованные с использованием диапазона атрибутов.