【问题标题】:How do I create an HttpContext for my unit test?如何为我的单元测试创​​建 HttpContext?
【发布时间】:2017-03-10 02:53:24
【问题描述】:

我正在努力模拟单元测试所需的HttpContext

我已经使用SessionManager 接口将会话控制从我的Mvc 控制器中抽象出来,并使用一个名为CookieSessionManager 的类来实现它。 (早期开发阶段)。

CookieSessionManager 通过使用注入的单例 HttpContextAccessor(在 Startup.cs ConfigureServices 中)来使用 HttpContext

我正在使用在 Startup.cs 中使用app.UseCookieAuthentication 设置的 Cookie 身份验证。

在调试模式下手动测试可以正常工作

我为AccountController 类编写的MSUnit 测试与注入的MockSessionManager 类一起工作。

我遇到的真正问题是我为CookieSessionManager 班级编写的单元测试。我尝试设置HttpContext,如下所示;

[TestClass]
public class CookieSessionManagerTest
{
    private IHttpContextAccessor contextAccessor;
    private HttpContext context;
    private SessionManager sessionManager;

    [TestInitialize]
    public void Setup_CookieSessionManagerTest()
    {
        context = new DefaultHttpContext();

        contextAccessor = new HttpContextAccessor();

        contextAccessor.HttpContext = context;

        sessionManager = new CookieSessionManager(contextAccessor);
    }

错误

但是对sessionManager.Login(CreateValidApplicationUser()); 的调用似乎没有设置IsAuthenticated 标志并且测试CookieSessionManager_Login_ValidUser_Authenticated_isTrue 失败。

[TestMethod]
public void CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
{
    sessionManager.Login(CreateValidApplicationUser());

    Assert.IsTrue(sessionManager.isAuthenticated());
}

public ApplicationUser CreateValidApplicationUser()
{
    ApplicationUser applicationUser = new ApplicationUser();

    applicationUser.UserName = "ValidUser";

    //applicationUser.Password = "ValidPass";

    return applicationUser;
}

测试名称:CookieSessionManager_Login_ValidUser_Authenticated_isTrue

:第 43 行测试结果:失败测试持续时间:0:00:00.0433169

结果 StackTrace:在 ClaimsWebAppTests.Identity.CookieSessionManagerTest.CookieSessionManager_Login_ValidUser_Authenticated_isTrue()

CookieSessionManagerTest.cs:第 46 行结果消息:Assert.IsTrue 失败。

我的代码

会话管理器

using ClaimsWebApp.Models;

namespace ClaimsWebApp.Identity
{
    public interface SessionManager
    {
        bool isAuthenticated();

        void Login(ApplicationUser applicationUser);

        void Logout();
    }
}

CookieSessionManager

using ClaimsWebApp.Identity;
using ClaimsWebApp.Models;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Security.Claims;

namespace ClaimsWebApp
{
    public class CookieSessionManager : SessionManager
    {
        private List<ApplicationUser> applicationUsers;
        private IHttpContextAccessor ContextAccessor;
        private bool IsAuthenticated;

        public CookieSessionManager(IHttpContextAccessor contextAccessor)
        {
            this.IsAuthenticated = false;

            this.ContextAccessor = contextAccessor;

            IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;

            applicationUsers = new List<ApplicationUser>();

            applicationUsers.Add(new ApplicationUser { UserName = "ValidUser" });
        }
        public bool isAuthenticated()
        {
            return IsAuthenticated;
        }

        public void Login(ApplicationUser applicationUser)
        {
            if (applicationUsers.Find(m => m.UserName.Equals(applicationUser.UserName)) != null)
            {
                var identity = new ClaimsIdentity(new[] {
                new Claim(ClaimTypes.Name, applicationUser.UserName)
                },
                "MyCookieMiddlewareInstance");

                var principal = new ClaimsPrincipal(identity);

                ContextAccessor.HttpContext.Authentication.SignInAsync("MyCookieMiddlewareInstance", principal);

                IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
            }
            else
            {
                throw new Exception("User not found");
            }
        }

        public void Logout()
        {
            ContextAccessor.HttpContext.Authentication.SignOutAsync("MyCookieMiddlewareInstance");

            IsAuthenticated = ContextAccessor.HttpContext.User.Identity.IsAuthenticated;
        }
    }
}

Startup.cs

using ClaimsWebApp.Identity;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

namespace ClaimsWebApp
{
    public class Startup
    {
        // This method gets called by the runtime. Use this method to add services to the container.
        // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
            services.AddScoped<SessionManager, CookieSessionManager>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole();

            app.UseCookieAuthentication(new CookieAuthenticationOptions()
            {
                AuthenticationScheme = "MyCookieMiddlewareInstance",
                LoginPath = new PathString("/Account/Unauthorized/"),
                AccessDeniedPath = new PathString("/Account/Forbidden/"),
                AutomaticAuthenticate = true,
                AutomaticChallenge = true
            });

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Account}/{action=Login}/{id?}");
            });
        }
    }
}

CookieSessionManagerTest.cs

using ClaimsWebApp;
using ClaimsWebApp.Identity;
using ClaimsWebApp.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace ClaimsWebAppTests.Identity
{
    [TestClass]
    public class CookieSessionManagerTest
    {
        private IHttpContextAccessor contextAccessor;
        private HttpContext context;
        private SessionManager sessionManager;

        [TestInitialize]
        public void Setup_CookieSessionManagerTest()
        {
            context = new DefaultHttpContext();

            contextAccessor = new HttpContextAccessor();

            contextAccessor.HttpContext = context;

            sessionManager = new CookieSessionManager(contextAccessor);
        }

        [TestMethod]
        public void CookieSessionManager_Can_Be_Implemented()
        {
            Assert.IsInstanceOfType(sessionManager, typeof(SessionManager));
        }


        [TestMethod]
        public void CookieSessionManager_Default_Authenticated_isFalse()
        {
            Assert.IsFalse(sessionManager.isAuthenticated());
        }

        [TestMethod]
        public void CookieSessionManager_Login_ValidUser_Authenticated_isTrue()
        {
            sessionManager.Login(CreateValidApplicationUser());

            Assert.IsTrue(sessionManager.isAuthenticated());
        }

        public ApplicationUser CreateValidApplicationUser()
        {
            ApplicationUser applicationUser = new ApplicationUser();

            applicationUser.UserName = "ValidUser";

            //applicationUser.Password = "ValidPass";

            return applicationUser;
        }

        public ApplicationUser CreateInValidApplicationUser()
        {
            ApplicationUser applicationUser = new ApplicationUser();

            applicationUser.UserName = "InValidUser";

            //applicationUser.Password = "ValidPass";

            return applicationUser;
        }
    }
}

【问题讨论】:

标签: c# asp.net-mvc unit-testing asp.net-core


【解决方案1】:

不幸的是,用HttpContext 进行测试几乎是不可能的。这是一个不使用任何接口的密封类,所以你不能模拟它。通常,最好的办法是抽象出与 HttpContext 一起使用的代码,然后只测试其他更特定于应用程序的代码。

您似乎已经通过HttpContextAccessor 完成了此操作,但您使用不正确。首先,您要暴露 HttpContext 实例,这几乎违背了整个目的。这个类应该能够自己返回类似User.Identity.IsAuthenticated的东西,比如:httpContextAccessor.IsAuthenticated。在内部,该属性将访问私有 HttpContext 实例并返回结果。

一旦您以这种方式使用它,您就可以模拟 HttpContextAccessor 以简单地返回您的测试所需的内容,而您不必担心为其提供 HttpContext 实例。

当然,这意味着仍有一些未经测试的代码,即与 HttpContext 一起使用的访问器方法,但这些通常非常简单。例如,IsAuthenticated 的代码将类似于 return httpContext.User.Identity.IsAuthenticated。解决这个问题的唯一方法是,如果你用手指粗了一些东西,但编译器会警告你。

【讨论】:

  • 那么说测试 Cookie 身份验证中间件只能作为正在运行的 Web 进程上下文中的集成测试的一部分来实现吗?并且 HttpContext 交互的抽象是单元测试其余代码的关键,正如我认为你所建议的那样?
  • 是的。您的单元测试应该专注于小的离散功能“单元”,因此得名。已设置 cookie 的事实是一个实现细节,与您的测试代码无关。
  • @ChrisPratt 这个问题是关于 ASP.NET Core 的,OP 使用 DefaultHttpContext 这是抽象基类 HttpContext 的实现,完全可以在单元测试中模拟。但是,您关于更集中测试的观点是有效的。
  • @HenkMollema 是否意味着模拟DefaultHttpContext 我调用的方法,如ContextAccessor.HttpContext.User.Identity.IsAuthenticated,以按照我的预期行事?我知道我可以对CookieSessionManager 进行单元测试,但仍需要对 Cookie 身份验证中间件进行集成测试?
  • @EdwardComeau 我想自己实现HttpContext 类更容易,只需使用您感兴趣的部分。
【解决方案2】:

这并不能直接回答问题的上下文,但它提供了一种替代测试方法,当您开始使用它时,它会让生活变得更加轻松。

有一个可用于 ASP.NET Core 的集成测试包,有关它的文档可以在此处找到:

https://docs.asp.net/en/latest/testing/integration-testing.html

享受吧!

【讨论】:

  • 对 testserver 很不满意,因为它的维护成本很高,而且可能会错过真正的 http 问题.. 现在使用黑盒测试和 mockserver
【解决方案3】:

我为我的单元测试创​​建了这个助手功能,这让我可以测试那些需要部分 httpRequest 的特定方法。

public static IHttpContextAccessor GetHttpContext(string incomingRequestUrl, string host)
    {
        var context = new DefaultHttpContext();
        context.Request.Path = incomingRequestUrl;
        context.Request.Host = new HostString(host);

        //Do your thing here...

        var obj = new HttpContextAccessor();
        obj.HttpContext = context;
        return obj;
    }

【讨论】:

  • 我认为这当然应该再强调一点:在 AspNetCore 中,似乎获取 DefaultHttpContext() 然后添加你想要的所有内容是对代码进行单元测试时的正确方法您想向其中提供包含一些值的 HttpContext 实例。 defaultHttpContext 还添加了一个 defaultHttpRequest ,您也可以对其进行类似的修饰。 DefaultHttpRequest 本身是一个密封类(所以不能直接自己实例化)。使用 Net Core,这显然是要走的路。
【解决方案4】:

您可以创建一个像下面这样继承 HttpContext 的测试类。并在需要的地方使用测试类。您可以在代码中添加缺少的实现。

public class TestHttpContext : HttpContext
{
    [Obsolete]
    public override AuthenticationManager Authentication
    {
        get { throw new NotImplementedException(); }
    }

    public override ConnectionInfo Connection
    {
        get { throw new NotImplementedException(); }
    }

    public override IFeatureCollection Features
    {
        get { throw new NotImplementedException(); }
    }

    public override IDictionary<object, object> Items
    {
        get { throw new NotImplementedException(); }
        set { throw new NotImplementedException(); }
    }

    public override HttpRequest Request
    {
        get { throw new NotImplementedException(); }
    }

    public override CancellationToken RequestAborted
    {
        get { throw new NotImplementedException(); }
        set { throw new NotImplementedException(); }
    }

    public override IServiceProvider RequestServices
    {
        get { throw new NotImplementedException(); }
        set { throw new NotImplementedException(); }
    }

    HttpResponse _response;
    public override HttpResponse Response
    {
        get
        {
            if (this._response == null)
            {
                this._response = new TestHttpResponse();
                this._response.StatusCode = 999;
            }

            return this._response;
        }
    }

    public override ISession Session
    {
        get { throw new NotImplementedException(); }
        set { throw new NotImplementedException(); }
    }

    public override string TraceIdentifier
    {
        get { throw new NotImplementedException(); }
        set { throw new NotImplementedException(); }
    }

    public override ClaimsPrincipal User
    {
        get { throw new NotImplementedException(); }
        set { throw new NotImplementedException(); }
    }

    public override WebSocketManager WebSockets
    {
        get { throw new NotImplementedException(); }
    }

    public override void Abort()
    {
        throw new NotImplementedException();
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-03-29
    • 2022-01-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-30
    • 1970-01-01
    相关资源
    最近更新 更多