Модульное тестирование HTTP-запросов в c # - PullRequest
30 голосов
/ 01 февраля 2012

Я пишу некоторый код, который вызывает веб-сервис, читает ответ и что-то с ним делает. Мой код выглядит следующим образом:

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. Однако просто неправильно тестировать эти методы и надеяться, что остальная часть кода собрана правильно. Можно ли как-то перехватить HTTP-запрос и вместо этого передать имитирующие данные?

РЕДАКТИРОВАТЬ Эта информация устарела. Гораздо проще создать такой код с использованием библиотек System.Net.Http.HttpClient .

Ответы [ 4 ]

20 голосов
/ 01 февраля 2012

В вашем коде вы не можете перехватывать вызовы HttpWebRequest, потому что вы создаете объект тем же методом.Если вы позволите другому объекту создать HttpWebRequest, вы можете передать фиктивный объект и использовать его для проверки.

Так что вместо этого:

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(_url);

Используйте это:

IHttpWebRequest request = this.WebRequestFactory.Create(_url);

В своем модульном тесте вы можете передать WebRequestFactory, который создает фиктивный объект.

Кроме того, вы можете разделить код чтения потока в отдельной функции:

using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
    byte[] data = ReadStream(response.GetResponseStream());
    return ExtractResponse(Encoding.UTF8.GetString(data));
}

Это позволяет тестировать ReadStream() отдельно.

Чтобы выполнить больше интеграционного теста, вы можете настроить собственный HTTP-сервер, который возвращает тестовые данные, и передать URL-адрес этого сервера.к вашему методу.

7 голосов
/ 19 января 2016

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

6 голосов
/ 01 февраля 2012

Я бы, вероятно, начал бы с рефакторинга кода, чтобы сделать его более слабо связанным с реальным HTTP-запросом. Прямо сейчас этот код, кажется, делает довольно много вещей.

Это можно сделать, введя абстракцию:

public interface IDataRetriever
{
    public byte[] RetrieveData(byte[] request);
}

Теперь класс, который вы пытаетесь выполнить модульным тестом, можно отделить от фактического HTTP-запроса, используя шаблон проектирования Inversion of Control:

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 в вашем реальном приложении.

2 голосов
/ 19 января 2016

Если вы счастливы перейти на HttpClient (официальную, переносимую, http-клиентскую библиотеку), то некоторое время назад я написал библиотеку, которая может помочь под названием MockHttp . Он предоставляет свободный API, который позволяет вам предоставлять ответы на запросы, соответствующие диапазону атрибутов.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...