Как мне написать модульный тест 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;
}
}
или любым другим способом вы издеваетесь над своими услугами.