Делал модульные тесты для проекта ASP.NET CORE . Через Moq делал заглушки... Вообщем в коде самого теста всё как надо. Ошибка NullReferenceException
возникает при вызове строки Response.Cookies.Append(userName, account.ExternalId);
. Вот пример кода контроллера LoginController
:
public async Task<IActionResult> Login(string userName) {
var account = await _db.FindByUserNameAsync(userName);
Response.Cookies.Append(userName, account.ExternalId);
return Ok();
}
Вот пример кода из теста:
public async Task WhenLoginUserBobNameThenPageReturnOkAndCookieExist() {
string userName = "Имя пользователя, которое есть в базе данных,
хотя это не важно, я же использую mock";
var db = new Mock<ИнтерфейсБд>();
var account = new Account() {
ExternalId = userName,
};
db.Setup(repo => repo.FindByUserNameAsync(userName)).ReturnsAsync(account);
var controller = new LoginController(db.Object);
var loginResult = await controller.Login(userName);
//var expected = controller.Request.Cookies[userName];
Assert.IsType<OkResult>(loginResult);
Assert.Equal(expected, userName);
}
Основная задача теста была проверить, что куки правильно сохранились (это и так понятно, но я новичок и поэтому всё равно хотел затестить), но столкнулся с такой ошибкой. Я так понимаю ошибка вызывается при обращении к полю Response
- оно равно null
. Но при сборке проекта сервис работает. Дело в неправильном построении теста. Может надо запускать объект Startup.cs
?
Похоже, дело в строке:
db.Setup(repo => repo.FindByUserNameAsync(userName)).ReturnsAsync(account);
Здесь вы просите библиотеку Moq при вызове FindByUserNameAsynс
с определёнными параметрами возвращать определённое значение. Если вы хотите поставить в соответствие конкретное значение, передавайте в качестве параметра константу. В данном случае можно поставить в соответствие любое строковое значение, поскольку метод будет вызван ровно один раз и у нас не будет нескольких вариантов. Любой значение записывается как It.IsAny<string>()
:
db.Setup(repo => repo.FindByUserNameAsync(It.IsAny<string>())).ReturnsAsync(account);
После этого изменения метод начнёт подставляться при любом значение параметра и будет возвращать существующее значение account
.
UPDATE
Дописываю ответ после комментария. Если речь идёт о поле Request
, то тестировать его изменение можно несколькими способами.
Способ номер один: если свойство Request
публичное и изменяемое, мы можем установить его значение перед тестом.
var controller = new LoginController(db.Object);
controller.Request = new HttpRequest { … };
Этот метод не сработает в нашем случае, потому что свойство Request
только для чтения. Мы могли бы попробовать способ номер два: если свойство только для чтения, но оно виртуальное, библиотека Moq может его переопределить.
var controllerMock = new Mock<LoginController>(db.Object);
Здесь возникает проблема: Moq переопределяет свойство Request
, но также переопределяет и все остальные виртуальные методы. Если бы метод Login
был виртуальным, мы не могли бы его протестировать.
В этом случае мы должны попросить библиотеку вызывать старые методы. Часть методов нам нужна для тестов и мы их должны явно перечислить:
controllerMock.SetupGet(x => x.Request)
.Returns(new HttpRequest { … });
controllerMock.CallBase = true;
Невиртуальные методы будут вызываться, как и раньше. Виртуальные тоже будут взываться, как раньше, из-за CallBase
. Свойство Request
будет возвращать указанное значение.
Вроде подходящий вариант, но, заглянув в документацию, мы выясняем, что свойство Request
невиртуальное, и мы не можем его переопределить. Как быть в этом случае?
Если код ваш, то вы можете сделать свойство виртуальным. Если вы при разработке библиотеки покрываете её тестами, все такие неправильные свойства и методы всплывают быстро. Но код базового котроллера System.Web.Mvc.Controller
не ваш, изменить его вы не можете.
Используем способ три: избавимся от прямой зависимости. Добавим в LoginController
метод:
public async Task<IActionResult> Login(string userName)
{
var account = await _db.FindByUserNameAsync(userName);
StoreUserInCookied(userName, account.ExternalId);
return Ok();
}
public virtual void StoreUserInCookies(string username, string externalId)
{
Response.Cookies.Append(username, account.ExternalId);
}
Для того, чтобы протестировать такой код, соберём всё, что мы уже знаем:
var controllerMock = new Mock<LoginController>(db.Object);
controllerMock.Setup(x => x.StoreUserInCookies("john doe", It.IsAny<string>());
controllerMock.CallBase = true;
. . .
await controllerMock.Object.Login("john doe");
Видим, что код не очень простой. Это произошло в том числе и потому, что мы перегрузили наш контроллер ответственностью. Логику складывания пользовательских данных в куки и их извлечения можно вынести совсем в отдельный класс:
public class CookieStorage
{
public virtual void StoreUser(HttpCookie cookie, string name, string id)
{
cookie.Append(name, id);
}
. . .
}
Внедрить такой класс в LoginController
можно через конструктор. Метод Login
станет выглядеть так:
public async Task<IActionResult> Login(string userName)
{
var account = await _db.FindByUserNameAsync(userName);
_cookieStorage.StoreUser(Request.Cookie, userName, account.ExternalId);
return Ok();
}
Поскольку мы сделали метод StoreUser
виртуальным, мы можем использовать его в качестве заглушки.
Здесь у нас получается случай, когда тестирование подсказывает то, как улучшить дизайн системы. К методу StoreUser
надо написать парный GetUser
, который, вероятно, будет вызываться из самых разных контроллеров в нескольких местах.
Создав CookieStorage
мы инкапсулировали логику работы с куками в одном месте и можем вносить согласованные изменения.
Виртуальный выделенный сервер (VDS) становится отличным выбором
Необходимо написать простейший проводник или файловый менеджер, который будет работать с НМЖД и внешним USB накопителемДолжны быть функции...
При попытки подключения к MySQL из PHP Yii2, да и просто из PHP методом PDO Происходит ошибка: SQLSTATE[HY000] [2054] The server requested authentication method unknown to the client