如何为我的单元测试创建一个HttpContext?

11

我正在努力模拟所需的HttpContext,以进行我的单元测试。

我已经通过一个SessionManager接口将对话控制从我的Mvc控制器中抽象出来,并使用一个名为CookieSessionManager的类实现了这一点。(初始开发阶段)。

CookieSessionManager使用注入的单例HttpContextAccessor(位于Startup.cs ConfigureServices中)来使用HttpContext

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

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

我为我的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

结果堆栈跟踪:在ClaimsWebAppTests.Identity.CookieSessionManagerTest.CookieSessionManager_Login_ValidUser_Authenticated_isTrue()处

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

我的代码

SessionManager

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;
        }
    }
}

1
你有没有调查过使用TestServer?https://docs.asp.net/en/latest/testing/integration-testing.html - Brad
谢谢@Brad,我认为这正是我正在寻找的。我只是试图将问题制作成单元测试,而它实际上是一个实现问题。 - Edward Comeau
1
可以使用TypeMock Isolator模拟HttpContext,例如:https://www.typemock.com/docs?book=Isolator&page=Documentation%2FHtmlDocs%2Fsample1fakinghttpcontextandmodelstate.htm - Gregory Prescott
4个回答

11

很遗憾,使用HttpContext进行测试几乎是不可能的。它是一个密封类,没有使用任何接口,因此无法进行模拟。通常,最好的方法是将处理HttpContext的代码抽象出来,然后只测试其他更具体的应用程序代码。

看起来你已经通过HttpContextAccessor做到了这一点,但是你使用得不正确。首先,你暴露了HttpContext实例,这几乎失去了整个目的。这个类应该能够自行返回像User.Identity.IsAuthenticated这样的东西,例如:httpContextAccessor.IsAuthenticated。在内部,属性将访问私有的HttpContext实例并返回结果。

当你以这种方式使用它时,你可以模拟HttpContextAccessor,简单地返回你需要的测试结果,而不必担心提供HttpContext实例。

当然,这意味着仍有一些未经测试的代码,即处理HttpContext的访问器方法,但这些通常非常简单。例如,IsAuthenticated的代码只需像这样:return httpContext.User.Identity.IsAuthenticated。你唯一会搞砸的方法就是如果你打错了某些东西,但编译器会警告你。


那么,可以说测试Cookie身份验证中间件只能作为运行Web进程的集成测试的一部分来实现;而HttpContext交互的抽象是单元测试其余代码的关键,正如您所建议的那样吗? - Edward Comeau
是的。你的单元测试应该专注于小而离散的“单元”功能,因此得名。cookie已经被设置这一事实是一个实现细节,不应该对你的测试代码有影响。 - Chris Pratt
2
@ChrisPratt 这个问题涉及 ASP.NET Core,OP 使用的是实现抽象基类 HttpContextDefaultHttpContext,在单元测试中可以轻松地进行模拟。然而,你提到的更专注的测试点是有道理的。 - Henk Mollema
@HenkMollema,这是否意味着我需要模拟DefaultHttpContext的方法,比如ContextAccessor.HttpContext.User.Identity.IsAuthenticated,以便它们表现出我期望的行为?我理解这样我就可以对CookieSessionManager进行单元测试,但仍需要对Cookie身份验证中间件进行集成测试? - Edward Comeau
@EdwardComeau 我猜自己实现 HttpContext 类会更容易,只需要你感兴趣的部分即可。 - Henk Mollema

11

我为自己的单元测试创建了这个助手功能,它允许我测试那些需要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;
    }

2
我认为这一点应该更加突出:在AspNetCore中,获取DefaultHttpContext(),然后添加所有您想要的内容是进行单元测试的正确方式,以便为其提供带有某些值的HttpContext实例。 defaultHttpContext还会添加默认的defaultHttpRequest,您也可以类似地修改它。 DefaultHttpRequest本身是一个密封类(因此无法直接实例化)。在Net Core中,这显然是正确的方法。 - shivesh suman

2

一直不太满意测试服务器,维护成本高且可能会错过真正的HTTP问题...现在改用黑盒测试和模拟服务器。 - user1496062

0
您可以创建一个继承HttpContext的Test类,然后在需要的地方使用它。您可以在代码中添加缺失的实现。
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();
    }
}

你介意展示一下这个扩展类在测试中的使用示例吗? - jarodsmk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接