Простой пример сложной ситуации в модульном тестировании, как это должно быть разработано? - PullRequest
0 голосов
/ 28 ноября 2018

Я упростил код в примере, чтобы сократить вопрос, но в основном я изо всех сил стараюсь разработать более тестируемый код и изолировать их друг от друга.

Имея всего два метода, перечисленных ниже, я обрисовал в общих чертах, какие тесты мне нужно делать.

  1. Убедитесь, что использовался правильный URL-адрес
  2. Убедитесь, что были использованы соответствующие заголовки, например, запрос был в JSON.
  3. Убедитесь, что использовался запрос POST (У меня есть HttpMessageHandler и я использую делегаты для перехвата и макета интернета как зависимости в реальном коде)
  4. Убедитесь, что сериализованное значение JSON не имеет никаких дополнительных свойств, не заполненных.

Пример кода приведен ниже:

    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 и тестируемое.

  1. Используйте декоратор или адаптер для простого преобразования agentName в agentID, а затем передайте эту информацию в базовый класс для отправки запроса.

  2. Измените закрытый метод на защищенный внутренний, замените платформу для имитации заменой реализации метода GetAgentIDFromName () и просто вызовите базовую реализацию для любого метода, который не был проверен.

  3. Измените сигнатуру метода ChangeAssignedAgent (), чтобы вместо этого ссылаться на получение agentID вместо имени, сделайте GetAgentIDFromName () общедоступным и ожидайте, что потребители вашего класса будут использовать его в порядкеиспользовать метод ChangeAssignedAgent ().

Возможно, что первый вариант - лучший способ справиться с этим сценарием, но что-то подсказывает мне, что это может быть неправильным решением, поскольку технически основакласс вводит в заблуждение ... он хочет agentName ... не ID.

Второй вариант кажется мне более хаком и запахом кода, он эффективно тестирует закрытый метод ... Я неконечно ... открыт для предложений.

Наконец, последний вариант ... он похож на второй вариантИон с точки зрения того, как я чувствую, это может быть запахом хака / кода, но это имеет для меня наиболее логичный смысл.Тем не менее, с таким дизайном кажется, что вы никогда не сможете использовать частные методы, и это также может увеличить сложность вашего класса.

Я слишком обдумываю это?Я хотел бы услышать некоторые предложения ...

Ответы [ 2 ]

0 голосов
/ 29 ноября 2018

Я придумал дизайн, который позволяет мне делать то, что я хотел, без всяких «хаков», скажем так.

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

0 голосов
/ 28 ноября 2018

Вам всегда придётся высмеивать зависимости, которые вам нужны.

Одна из проблем здесь заключается в том, что IHttpService функционирует как сервисный локатор.Ни то, что вы запрашиваете у него, ни его ответ строго напечатаны.Это делает его зависимостью в том, что вы можете технически попросить что-либо или сказать что-нибудь сделать, поэтому я сравниваю это с локатором службы.

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

Вот грубый подход, который может помочь или может вызвать что-то еще.

Сначала абстракция, которая просто говорит о том, что вы хотите сделать.Нет подробностей реализации или упоминания об API отдыха.(Имя, которое я назвал, это хромое. Несколько лет назад я бы назвал его 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 будет жестко задан.Вы можете иметь один для dev, один для производства и т. Д., И ввести правильный в зависимости от среды.Поскольку этот класс действует как клиент, ему необходимо знать другие сегменты URL-адреса, но он будет использовать любой базовый URL, который вы ему сообщаете.

Убедитесь, что были использованы соответствующие заголовки, такие как запросбыл в JSON.
Вам не нужно проверять его, потому что он обрабатывается библиотекой.Даже если вы использовали классы .NET Framework, я не думаю, что это то, что вам нужно было бы тестировать, потому что вы тестировали бы фреймворк, а не свой собственный код.Такого рода вещи можно обработать в интеграционном тесте, чтобы убедиться, что все работает непрерывно.

Убедитесь, что был использован запрос POST (у меня есть HttpMessageHandler и использующий делегаты для перехвата и макетаиз Интернета как зависимость в фактическом коде)
Убедитесь, что сериализованное значение JSON не имеет никаких дополнительных свойств, не заполненных.

См. выше.

Теперь любой класснужно обновить тикет, он может зависеть от ITicketRepository, который действительно легко подделать.

Что касается тестирования HttpClientTicketRepository, то здесь больше нечего высмеивать.Единственное, что это делает, - это общение с API, использующим HTTP, поэтому вы должны протестировать его с помощью интеграционного теста, нацелив его на фактический экземпляр API и убедившись, что вы получите ожидаемый результат.Этот интеграционный тест охватывает такие вещи, как правильность заголовков и метода HTTP.

...