Модульное тестирование для асинхронной задачи <ActionResult>

Я очень новичок в ASP.NET CORE MVC, и мне было интересно, может ли кто-нибудь помочь мне с моей проблемой.

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

Вот мой код контроллера:

public async Task<ActionResult> Organization(string selectedOrg, string oauth)
{
    var client = new HttpClient();
    IndexViewModel model = new IndexViewModel();
    model.Organizations = OrganizationData.Data;
    if (selectedOrg == null)
    {
        selectedOrg = model.Organizations.FirstOrDefault().OrgName;
    }
    else
    {
        model.SelectedOrg = selectedOrg;
    }
    var token = _cache.Get<TokenModel>("Token" + HttpContext.Session.GetString("TokenGuid"));
    oauth = token.AccessToken;
    var url = "https://dev.azure.com/" + selectedOrg + "/_apis/projects?api-version=4.1";
    try
    {
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", oauth);

        var response = await client.GetAsync(url);
        var responseBody = response.Content.ReadAsStringAsync().Result;
        model.Projects = JsonConvert.DeserializeObject<ProjectsModel>(responseBody);

        client.Dispose();
        return View("Index", model);
    }
    catch(Exception e)
    {
        client.Dispose();
        return Json(Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(e.ToString()));
    }
}

Может ли кто-нибудь помочь, как провести модульное тестирование с этим? Или мне нужно реорганизовать это?

Однако код тесно связан с проблемами реализации, которые не позволяют проводить модульное тестирование изолированно. В идеале вы хотите иметь возможность имитировать / заглушать все зависимости и внедрять их в тестируемый объект.

Nkosi 24.10.2018 17:16

Спасибо за комментарий по этому поводу. Можно ли использовать модульный тест только для результата json?

art-a-game 24.10.2018 17:30

Вам нужно будет абстрагироваться от http-вызова, чтобы вы могли подделать ответ json.

Nkosi 24.10.2018 17:44

Я также только что понял, что вы смешиваете блокировку .Result с async-await, что может привести к тупикам.

Nkosi 24.10.2018 17:44

Как я могу абстрагироваться от http-вызова? Не могли бы вы помочь мне с этим?

art-a-game 24.10.2018 17:54

На этот вопрос довольно сложно ответить, поскольку есть ряд проблем, на которые следует обратить внимание. Во-первых, измените response.Content.ReadAsStringAsync().Result; на await response.Content.ReadAsStringAsync();, чтобы решить проблему .Result, как упомянуто @nkosi. Затем воспользуйтесь своей любимой поисковой системой, чтобы узнать, почему создавать такие HttpClient - плохая идея и как вы можете вместо этого использовать внедрение зависимостей. Наконец, абстракция @nkosi вероятно означает создание другого класса, который обрабатывает запрос к вашей конечной точке Azure и внедряет его в ваш контроллер.

serpent5 24.10.2018 19:48

Вы можете создать тест, проверьте шаги 7-8 в этом руководстве: codeproject.com/Articles/1264219/…

H. Herzl 24.10.2018 23:05
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
907
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

У вас есть способ для многих зависимостей.

Почему сигнатура метода передает значение oauth, которое никогда не используется?

Во-первых, вызов любой внешней зависимости через http внутри контроллера должен быть осужден. Все это должно быть абстрагировано в его собственный вызов. Поскольку похоже, что он получает данные, на самом деле они должны быть на вашем уровне данных. Охват всего n-уровневого подхода отдельными проектами, скорее всего, выходит за рамки, поэтому давайте просто рассмотрим минимум для модульного тестирования по моему мнению.

Сначала вам нужно абстрагироваться от HttpClient. Вы не можете использовать методы модульного тестирования, если они вызывают какие-либо внешние вызовы (по большей части), потому что тогда это не модульный тест, это интеграционный тест.

// I don't have a full grasp of your complete eco-system so based on the
// minimal information provided, this would at least get you close
public interface IAzureAPI
{
  public Task<string> GetOrgAsync(string org, string oauth);
}

public class AzureAPI : IDisposable
{
  public async Task<string> GetOrgAsync(string org, string oauth)
  {
    // use *using* not try/catch/finally/dispose
    using (var client = new HttpClient())
    {
      client.DefaultRequestHeaders.Accept.Clear();
      client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Bearer", oauth);

      var url = "https://dev.azure.com/" + org+ "/_apis/projects?api-version=4.1";

      var response = await client.GetAsync(url);
      // never use `.Result` unless you absolutely know what you are doing
      // always using async/wait if possible
      var result = await response.Content.ReadAsStringAsync(); 
      return result;
   }
  }
}

Надеюсь, вы используете DI Framework:

public class MyController
{
  private IAzureAPI _azureAPI;
  public MyController(IAzureAPI azureAPI)
  {
    _azureAPI = azureAPI;
  }
}

Теперь перейдем к самому сложному:

public async Task<ActionResult> Organization(string selectedOrg, string oauth)
{
    IndexViewModel model = new IndexViewModel();

    // I have no idea where model came from so
    // this appears to block "unit-testing"
    // strange that you don't validate `selectedOrg`, you just use it
    model.Organizations = OrganizationData.Data;
    if (selectedOrg == null)
    {
        selectedOrg = model.Organizations.FirstOrDefault().OrgName;
    }
    else
    {
        model.SelectedOrg = selectedOrg;
    }

    // no idea where `_cache` came from so 
    // also appears to block "unit-testing"
    // As does `HttpContext` because you aren't using the
    // Interface
    var token = _cache.Get<TokenModel>("Token" + HttpContext.Session.GetString("TokenGuid"));
    oauth = token.AccessToken;

    try
    {
        var orgInfo = await _azureAPI.GetOrgAsync(selectedOrg, oauth);

        model.Projects = JsonConvert.DeserializeObject<ProjectsModel>(orgInfo);

        // return a view here???
        return View("Index", model);
    }
    catch(Exception e)
    {
        // return JSON here instead????
        return Json(Newtonsoft.Json.JsonConvert.DeserializeObject<dynamic>(e.ToString()));
    }
}

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

public MyControllerTests
{
  // for 100% Cover Coverage you'd need all of these
  public async Task Organization_OrgAsString_ReturnsView
  {
    //...
  }

  public async Task Organization_OrgAsNull_ReturnsView
  {
    // Arrange
    var azureAPI = Substitute.For<IAzureAPI>();
    azureAPI.GetOrgAsync(null, null)
      .Returns("somestring");
    var controller = new MyController(azureAPI);

    // Act
    var result = await controller.Organization(null, null);

    // Assert
    Assert.That(result....);

  }

  public async Task Organization_WithException_ReturnsJson
  {
    //...
  }

}

С Nsubstitute вам не нужно явно возвращать Task для асинхронных методов: azureAPI.GetOrgAsync(null, null).Returns("somestring");

Fabio 25.10.2018 09:52

Другие вопросы по теме