Как мне написать модульный тест Web Api, который я должен получить из UserManager

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

Например, я хочу протестировать этот контроллер веб-API

public IHttpActionResult GetFavorites()
{
    var userApplication = UserManager.FindById(User.Identity.GetUserId());
    var user = db.Users.FirstOrDefault(u => u.Username == userApplication.UserName);
    if (user == null)
        return Unauthorized();

    return Ok(db.Favorites.Where(fav => fav.UserID == user.UserID));
}

Я попытался смоделировать контекст для моего контроллера, а также попытался изменить Thread.CurrentPrincipal.

Некоторые могли бы помочь мне в этом, что я делаю не так?

3 ответа

Приведенный выше контроллер очень сложен для модульного тестирования в текущем состоянии Вы должны издеваться над UserManager, controller.User, db.Users а также db.Favorites, Это приводит к перегруженному модульному тесту, который сложно писать и поддерживать. Это идеальный признак того, что контроллер делает больше, чем должен:

  • Возврат неавторизованного ответа, если пользователь не найден
  • Возврат списка избранных для авторизованного пользователя

Как пользователи хранятся и как получить избранное, являются задачами для бизнес-логики. Упрощенное действие контроллера может выглядеть следующим образом:

public IHttpActionResult GetFavorites()
{
  string userName = this.User.Identity.Name;
  if (!this.userService.IsAuthorized(userName))
  {
    return this.Unauthorized();
  }
  var favs = this.userService.GetFavorites(userName);
  return Ok(favs);
}

Модульный тест, который проверяет, возвращает ли действие несанкционированный доступ:

  // arrange
  var userService = Substitute.For<IUserService>();
  var controller = new FavoritesController(userService);

  // act
  var result = controller.GetFavorites();

  // assert
  result.Should().BeOfType<UnauthorizedResult>();

Тест, который проверяет, возвращает ли действие избранное пользователя:

  // arrange
  var userService = Substitute.For<IUserService>();
  var controller = new FavoritesController(userService);

  var favorites = new[] { "fav1", "fav2" };
  userService.IsAuthorized("John").Returns(true);
  userService.GetFavorites("John").Returns(favorites);

  controller.User = Substitute.For<IPrincipal>();
  controller.User.Identity.Name.Returns("John");

  // act
  var result = controller.GetFavorites();

  // assert
  result.Should().BeOfType<OkNegotiatedContentResult<string[]>>();
  ((OkNegotiatedContentResult<string[]>)result).Content.Should().BeSameAs(favorites);

И, наконец, IUserService интерфейс:

public interface IUserService
{
  bool IsAuthorized(string userName);

  string[] GetFavorites(string userName);
}

Вам нужно смоделировать все, что вы на самом деле не тестируете.

Первый в этом списке UserManager.FindById() и вы заметите, что вы не можете издеваться над этим напрямую, так как это метод расширения. Вы можете издеваться IUserStor0хотя и использовать реальный UserManager против этого.


То же самое относится и к IIdentity.GetUserId но мы можем посмеяться над этим тоже. Увидеть ниже

редактировать

Вероятно, стоит указать, что вы пытаетесь найти свой IdentityUser используя Id, а затем запрашивает вашу другую пользовательскую таблицу, используя его UserName, Вы могли бы просто использовать User.Identity.Name с самого начала и сэкономил себе некоторые из следующих усилий.


Дразнящий DbSet немного сложнее, но выполнимо. Вам нужно будет объявить свой DbContext используя виртуальный IDbSet вместо обычного DbSet

например

public class MyContext {
    public virtual IDbSet<User> Users { get; set;}
    public virtual IDbSet<Favourite> Favourites { get; set; }
}

Чтобы смоделировать их так, чтобы ваши запросы работали в методе контроллера, нам нужна вспомогательная функция

    public Mock<IDbSet<T>> CreateMockDbSet<T>(IQueryable<T> data) where T:     
      class
    {
        var dbSet = new Mock<IDbSet<T>>();
        dbSet.Setup(m => m.Provider).Returns(data.Provider);
        dbSet.Setup(m => m.Expression).Returns(data.Expression);
        dbSet.Setup(m => m.ElementType).Returns(data.ElementType);
        dbSet.Setup(m => m.GetEnumerator()).Returns(data.GetEnumerator());

        return dbSet;
    }

Это позволяет нам создать макет IDbSet<T> и предварительно загрузить его с нашими тестовыми данными.

(используя NUnit и Moq - но используйте все, что вы хотите)

[Test]
public void TestGetFavourites()
{
    var user = new ApplicationUser {Id = 1, UserName = "Test"};

    //Mock UserStore so our UserManager calls work
    var userStore = new Mock<IQueryableUserStore<ApplicationUser, string>>();
    userStore.Setup(m => m.FindByIdAsync(It.IsAny<int>)))
      .Returns(Task.FromResult(user));

    var userManager = new UserManager<ApplicationUser, int>(userStore.Object);

    // Mock our db context

    var users = new List<User>
    {
        new Api.User()
        {
            UserId = 1,
            UserName = "Test"
        }
    }.AsQueryable();

    var favourites = new List<Favourite>
    {
        new Favourite
        {
            UserId = 1
        }
    }.AsQueryable();

    var dbUsers = CreateMockDbSet(users);
    var dbFavourites = CreateMockDbSet(favourites);

    var dbContext = new Mock<MyContext>();
    dbContext.Setup(m => m.Favourites).Returns(dbFavourites.Object);
    dbContext.Setup(m => m.Users).Returns(dbUsers.Object);

    //You didn't specify how your controller got it's UserManager 
    // and db instances so I'm assuming constructor injection 
    var controller = new TestController(userManager, dbContext.Object);

    // Allows User.Identity.GetUserId() to work.
    var identity = new ClaimsIdentity();
    identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, "1"));
    controller.User = new ClaimsPrincipal(identity);


    // Run our tests

    var response = controller.GetFavourites();

    Assert.IsInstanceOf<OkNegotiatedContentResult<
      IQueryable<Favourite>>>(response);
    var responseObject = (OkNegotiatedContentResult<
      IQueryable<Favourite>>) response;
    Assert.That(responseObject.Content.Count(), Is.EqualTo(1));(response);
}

Сейчас. Там очень много кода, который мы должны смоделировать, чтобы протестировать один метод. Как правило, это признак того, что вам нужно абстрагироваться от логики. Хорошее начало было бы реализовать UserRepository и FavouritesRepository это обрабатывает получение пользователей и получение избранных соответственно.

Затем вы можете протестировать их отдельно и макетировать оба при тестировании этого метода.

Вы можете добавить сервис в свой контроллер

public interface ICurrentUserService
{
    int? GetUserId();
}

и реализовать его для работы в вашем приложении

public HttpUserService : ICurrentUserService
{
    public int? GetUserId()
    {
        return User.Identity.GetUserId();
    }
}

и от вас контроллер сделать это

var userApplication = UserManager.FindById(_myService.GetUserId());

а потом смейся в своем тесте

public HardCodedUserService : ICurrentUserService
{
    public int? GetUserId()
    {
        return 1;
    }
}

или любым другим способом вы издеваетесь над своими услугами.

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