Подтвердить что ты не робот

Модульное тестирование 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.

4b9b3361

Ответ 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, который позволяет вам отвечать на запросы, согласованные с использованием диапазона атрибутов.