Mocking Session не работает в MVC 5

Я сохраняю значения в сеансе в моем контроллере. Действие тестируется. Я прочитал несколько статей о том, как издеваться над сеансом, и я пытаюсь выполнить ответ Милокс на Установка текущего сеанса httpcontext в unit test. Но когда я сверлюсь в Locals | this | base | HttpContext Sessions, все равно null, и при выполнении теста с ошибкой Null Reference при установке переменной сеанса HttpContext.Session["BsAcId"] = vM.BusAcnt.Id;

Это рабочий производственный код. vM.BusAcnt.Id возвращает допустимый int, и если я подставляю его значением int, тест все равно терпит неудачу, потому что Session имеет значение null, и поэтому в нем не может быть сохранено значение.

Я использую MVC5, EF6 и последние версии xUnit, Moq и тестировщика Resharper.

Действие:

public ActionResult Details(int id)
{
  var vM = new BusAcntVm();
  vM.BusAcnt = _db.BusAcnts.FirstOrDefault(bA => bA.Id == id);

  if ((User.IsInRole("Admin"))) return RedirectToAction("Action");

  HttpContext.Session["BsAcId"] = vM.BusAcnt.Id;

  return View(vM);
}

MockHelpers:

  public static class MockHelpers
  {
    public static HttpContext FakeHttpContext()
    {
      var httpRequest = new HttpRequest("", "http://localhost/", "");
      var stringWriter = new StringWriter();
      var httpResponce = new HttpResponse(stringWriter);
      var httpContext = new HttpContext(httpRequest, httpResponce);

      var sessionContainer = new HttpSessionStateContainer("id", new SessionStateItemCollection(),
                                              new HttpStaticObjectsCollection(), 10, true,
                                              HttpCookieMode.AutoDetect,
                                              SessionStateMode.InProc, false);

      httpContext.Items["AspSession"] = typeof(HttpSessionState).GetConstructor(
                                  BindingFlags.NonPublic | BindingFlags.Instance,
                                  null, CallingConventions.Standard,
                                  new[] { typeof(HttpSessionStateContainer) },
                                  null)
                          .Invoke(new object[] { sessionContainer });

      return httpContext;
    }
  }

Тест:

[Fact] 
public void AdminGetBusAcntById()
{
  HttpContext.Current = MockHelpers.FakeHttpContext();
  var mockMyDb = MockDbSetup.MockMyDb();
  var controller = new BusAcntController(mockMy.Object);
  var controllerContextMock = new Mock<ControllerContext>();
  controllerContextMock.Setup( x => x.HttpContext.User.
    IsInRole(It.Is<string>(s => s.Equals("Admin")))).Returns(true);
  controller.ControllerContext = controllerContextMock.Object;

  var viewResult = controller.Details(1) as ViewResult;
  var model = viewResult.Model as BusAcntVm;

  Assert.NotNull(model);
  Assert.Equal("Company 1", model.CmpnyName);
}

Код Milox, похоже, имеет смысл, но я не могу заставить его работать. Я что-то пропустил? Есть ли изменения в MVC5, которые нарушают этот код?

РЕШЕНИЕ:

Реализация ответа Дарина. У меня теперь есть сеанс для записи значений (хотя значения на самом деле не записываются в него, но которые не нужны для тестирования) и тестовые проходы.

Тест:

[Fact] 
public void AdminGetBusAcntById()
{
  var mockMyDb = MockDbSetup.MockMyDb();
  var controller = new BusAcntController(mockMy.Object);
  var context = new Mock<HttpContextBase>();
  var session = new Mock<HttpSessionStateBase>();
  var user = new GenericPrincipal(new GenericIdentity("fakeUser"), new[] { "Admin" });
  context.Setup(x => x.User).Returns(user);
  context.Setup(x => x.Session).Returns(session.Object);
  var requestContext = new RequestContext(context.Object, new RouteData());
  controller.ControllerContext = new ControllerContext(requestContext, controller);


  var viewResult = controller.Details(1) as ViewResult;
  var model = viewResult.Model as BusAcntVm;

  Assert.NotNull(model);
  Assert.Equal("Company 1", model.CmpnyName);
}

Ответ 1

В вашем unit test вы установили HttpContext.Current = MockHelpers.FakeHttpContext();, но ASP.NET MVC не использует это статическое свойство вообще. Забудьте о HttpContext.Current в ASP.NET MVC. Это устаревшее и модульное тестирование недружественное (да, в вашем случае вы используете его только внутри вашего unit test, но ASP.NET MVC не использует его и является причиной, почему ваш код не работает).

Все дело в том, что ASP.NET MVC работает с абстракциями, такими как HttpContextBase, HttpRequestBase, HttpResponseBase, HttpSessionStateBase,... которые вы можете легко высмеять в unit test.

Возьмем пример контроллера:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        if ((this.User.IsInRole("Admin")))
        {
            return RedirectToAction("Action");
        }

        this.HttpContext.Session["foo"] = "bar";

        return View();
    }
}

и как может выглядеть соответствующий unit test, высмеивая необходимые абстракции с помощью Moq:

// arrange
var controller = new HomeController();
var context = new Mock<HttpContextBase>();
var session = new Mock<HttpSessionStateBase>();
var user = new GenericPrincipal(new GenericIdentity("john"), new[] { "Contributor" });
context.Setup(x => x.User).Returns(user);            
context.Setup(x => x.Session).Returns(session.Object);
var requestContext = new RequestContext(context.Object, new RouteData());
controller.ControllerContext = new ControllerContext(requestContext, controller);

// act
var actual = controller.Index();

// assert
session.VerifySet(x => x["foo"] = "bar");
...

И если вы хотите ввести условие User.IsInRole("Admin"), все, что вам нужно сделать, это обеспечить правильную роль в издевательском удостоверении.

Ответ 2

Кстати, я бы применил Mocking of Sessions, используя MOQ, следующим образом.

Я бы создал базовый класс в проекте UnitTests. Структура будет

[TestFixture]
public class BaseClass
{
    public Mock<ControllerContext> controllerContext;
    public Mock<HttpContextBase> contextBase;

    public BaseClass()
    {
        controllerContext = new Mock<ControllerContext>();
        contextBase = new Mock<HttpContextBase>();

        controllerContext.Setup(x => x.HttpContext).Returns(contextBase.Object);
        controllerContext.Setup(cc => cc.HttpContext.Session["UserId"]).Returns(1);
    }
}

См.: Я возвращаю 1 как значение сеанса для UserId в последней строке. Вы можете изменить его в соответствии с требованием.

Для простой справки я бы назвал мой TestClass как "ControllerClassTest". Поэтому я бы наследовал ControllerClassTest с помощью BaseClass, как этот

[TestFixture]
class ControllerClassTest : BaseClass
{
}

Затем в моем тестовом классе я бы инициализировал ControllerContext в методе Setup, подобном этому

[SetUp]
public void Setup()
{
    controller.ControllerContext = controllerContext.Object;
}

Не забывайте, что мы должны сначала объявить и инициализировать контроллер.

Надеюсь, это поможет вам