c# - 与.net core和身份框架的集成测试

标签 c# asp.net-core integration-testing asp.net-core-identity

我在任何地方都找不到任何答案。 我读过多篇文章并查看了大量源代码,但似乎都没有帮助。

http://www.dotnetcurry.com/aspnet-core/1420/integration-testing-aspnet-core

https://www.davepaquette.com/archive/2016/11/27/integration-testing-with-entity-framework-core-and-sql-server.aspx

https://learn.microsoft.com/en-us/aspnet/core/testing/integration-testing

我遇到的问题是解决服务而不是使用 HttpClient 来测试 Controller 。 这是我的启动类(class):

public class Startup: IStartup
{
    protected IServiceProvider _provider;
    private readonly IConfiguration _configuration;
    public Startup(IConfiguration configuration) => _configuration = configuration;

    // This method gets called by the runtime. Use this method to add services to the container.
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.Configure<MvcOptions>(options => options.Filters.Add(new RequireHttpsAttribute()));

        SetUpDataBase(services);
        services.AddMvc();
        services
            .AddIdentityCore<User>(null)
            .AddDefaultTokenProviders();
        return services.BuildServiceProvider();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app)
    {
        var options = new RewriteOptions().AddRedirectToHttps();

        app.UseRewriter(options);
        app.UseAuthentication();
        app.UseMvc();

        using(var scope = app.ApplicationServices.GetRequiredService<IServiceScopeFactory>().CreateScope())
        {
            var context = scope.ServiceProvider.GetService<DatabaseContext>();
            EnsureDatabaseCreated(context);
        }
    }

    protected virtual void SetUpDataBase(IServiceCollection services) => services.AddDbContext(_configuration);

    protected virtual void EnsureDatabaseCreated(DatabaseContext dbContext)
    {
        dbContext.Database.Migrate();
    }
}

然后在我的集成测试中,我创建了 2 个安装类。第一个是TestStartup:

public class TestStartup: Startup, IDisposable
{

    private const string DatabaseName = "vmpyr";

    public TestStartup(IConfiguration configuration) : base(configuration)
    {
    }

    protected override void EnsureDatabaseCreated(DatabaseContext dbContext)
    {
        DestroyDatabase();
        CreateDatabase();
    }

    protected override void SetUpDataBase(IServiceCollection services)
    {
        var connectionString = Database.ToString();
        var connection = new SqlConnection(connectionString);
        services
            .AddEntityFrameworkSqlServer()
            .AddDbContext<DatabaseContext>(
                options => options.UseSqlServer(connection)
            );
    }

    public void Dispose()
    {
        DestroyDatabase();
    }

    private static void CreateDatabase()
    {
        ExecuteSqlCommand(Master, $@"Create Database [{ DatabaseName }] ON (NAME = '{ DatabaseName }', FILENAME = '{Filename}')");
        var connectionString = Database.ToString();
        var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
        optionsBuilder.UseSqlServer(connectionString);
        using (var context = new DatabaseContext(optionsBuilder.Options))
        {
            context.Database.Migrate();
            DbInitializer.Initialize(context);
        }
    }

    private static void DestroyDatabase()
    {
        var fileNames = ExecuteSqlQuery(Master, $@"SELECT [physical_name] FROM [sys].[master_files] WHERE [database_id] = DB_ID('{ DatabaseName }')", row => (string)row["physical_name"]);
        if (!fileNames.Any()) return;
        ExecuteSqlCommand(Master, $@"ALTER DATABASE [{ DatabaseName }] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; EXEC sp_detach_db '{ DatabaseName }'");
        fileNames.ForEach(File.Delete);
    }

    private static void ExecuteSqlCommand(SqlConnectionStringBuilder connectionStringBuilder, string commandText)
    {
        using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString))
        {
            connection.Open();
            using (var command = connection.CreateCommand())
            {
                command.CommandText = commandText;
                command.ExecuteNonQuery();
            }
        }
    }

    private static List<T> ExecuteSqlQuery<T>(SqlConnectionStringBuilder connectionStringBuilder, string queryText, Func<SqlDataReader, T> read)
    {
        var result = new List<T>();
        using (var connection = new SqlConnection(connectionStringBuilder.ConnectionString))
        {
            connection.Open();
            using (var command = connection.CreateCommand())
            {
                command.CommandText = queryText;
                using (var reader = command.ExecuteReader())
                {
                    while (reader.Read())
                    {
                        result.Add(read(reader));
                    }
                }
            }
        }
        return result;
    }

    private static SqlConnectionStringBuilder Master => new SqlConnectionStringBuilder
    {
        DataSource = @"(LocalDB)\MSSQLLocalDB",
        InitialCatalog = "master",
        IntegratedSecurity = true
    };

    private static SqlConnectionStringBuilder Database => new SqlConnectionStringBuilder
    {
        DataSource = @"(LocalDB)\MSSQLLocalDB",
        InitialCatalog = DatabaseName,
        IntegratedSecurity = true
    };

    private static string Filename => Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), $"{ DatabaseName }.mdf");
}

它处理我所有的数据库创建和服务配置。 第二个是我的 TestFixture 类:

public class TestFixture<TStartup> : IDisposable where TStartup : class
{
    private readonly IServiceScope _scope;
    private readonly TestServer _testServer;

    public TestFixture()
    {
        var webHostBuilder = new WebHostBuilder().UseStartup<TStartup>();  

        _testServer = new TestServer(webHostBuilder);
        _scope = _testServer.Host.Services.CreateScope();
    }

    public TEntity Resolve<TEntity>() => _scope.ServiceProvider.GetRequiredService<TEntity>();

    public void Dispose()
    {
        _scope.Dispose();
        _testServer.Dispose();
    }
}

这(如您所见)创建了测试服务器,但也公开了 Resolve应该解决我的服务的方法。 现在我的测试来了。 我创建了一个 UserContext 类,如下所示:

public class UserContext
{
    private readonly UserManager<User> _userManager;
    private UserContext(TestFixture<TestStartup> fixture) => _userManager = fixture.Resolve<UserManager<User>>();

    public static UserContext GivenServices() => new UserContext(new TestFixture<TestStartup>());

    public async Task<User> WhenCreateUserAsync(string email)
    {
        var user = new User
        {
            UserName = email,
            Email = email
        };
        var result = await _userManager.CreateAsync(user);
        if (!result.Succeeded)
            throw new Exception(result.Errors.Join(", "));
        return user;
    }

    public async Task<User> WhenGetUserAsync(string username) => await _userManager.FindByNameAsync(username);
}

然后我创建了一个测试:

[TestFixture]
public class UserManagerTests
{

    [Test]
    public async Task ShouldCreateUser()
    {
        var services = UserContext.GivenServices();
        await services.WhenCreateUserAsync("<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="cebaa7a38ebaa7a3e0ada1a3" rel="noreferrer noopener nofollow">[email protected]</a>");
        var user = await services.WhenGetUserAsync("<a href="https://stackoverflow.com/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="3f4b56527f4b5652115c5052" rel="noreferrer noopener nofollow">[email protected]</a>");
        user.Should().NotBe(null);
    }
}

不幸的是,当我运行测试时它出错并指出:

Message: System.InvalidOperationException : Unable to resolve service for type 'Microsoft.AspNetCore.Identity.IUserStore1[vmpyr.Data.Models.User]' while attempting to activate 'Microsoft.AspNetCore.Identity.UserManager1[vmpyr.Data.Models.User]'.

我认为这告诉我,虽然它找到了我的 UserManager 服务,但它找不到构造函数中使用的 UserStore 依赖项。 我看过services.AddIdentityCore<User>(null)可以看到没有出现注册UserStore:

public static IdentityBuilder AddIdentityCore<TUser>(this IServiceCollection services, Action<IdentityOptions> setupAction) where TUser : class
{
  services.AddOptions().AddLogging();
  services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
  services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
  services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
  services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
  services.TryAddScoped<IdentityErrorDescriber>();
  services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser>>();
  services.TryAddScoped<UserManager<TUser>, UserManager<TUser>>();
  if (setupAction != null)
    services.Configure<IdentityOptions>(setupAction);
  return new IdentityBuilder(typeof (TUser), services);
}

然后我查看了 .AddIdentity<User, IdentityRole>()方法,并且似乎也没有注册 UserStore:

public static IdentityBuilder AddIdentity<TUser, TRole>(this IServiceCollection services, Action<IdentityOptions> setupAction) where TUser : class where TRole : class
{
  services.AddAuthentication((Action<AuthenticationOptions>) (options =>
  {
    options.DefaultAuthenticateScheme = IdentityConstants.ApplicationScheme;
    options.DefaultChallengeScheme = IdentityConstants.ApplicationScheme;
    options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
  })).AddCookie(IdentityConstants.ApplicationScheme, (Action<CookieAuthenticationOptions>) (o =>
  {
    o.LoginPath = new PathString("/Account/Login");
    o.Events = new CookieAuthenticationEvents()
    {
      OnValidatePrincipal = new Func<CookieValidatePrincipalContext, Task>(SecurityStampValidator.ValidatePrincipalAsync)
    };
  })).AddCookie(IdentityConstants.ExternalScheme, (Action<CookieAuthenticationOptions>) (o =>
  {
    o.Cookie.Name = IdentityConstants.ExternalScheme;
    o.ExpireTimeSpan = TimeSpan.FromMinutes(5.0);
  })).AddCookie(IdentityConstants.TwoFactorRememberMeScheme, (Action<CookieAuthenticationOptions>) (o => o.Cookie.Name = IdentityConstants.TwoFactorRememberMeScheme)).AddCookie(IdentityConstants.TwoFactorUserIdScheme, (Action<CookieAuthenticationOptions>) (o =>
  {
    o.Cookie.Name = IdentityConstants.TwoFactorUserIdScheme;
    o.ExpireTimeSpan = TimeSpan.FromMinutes(5.0);
  }));
  services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
  services.TryAddScoped<IUserValidator<TUser>, UserValidator<TUser>>();
  services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>();
  services.TryAddScoped<IPasswordHasher<TUser>, PasswordHasher<TUser>>();
  services.TryAddScoped<ILookupNormalizer, UpperInvariantLookupNormalizer>();
  services.TryAddScoped<IRoleValidator<TRole>, RoleValidator<TRole>>();
  services.TryAddScoped<IdentityErrorDescriber>();
  services.TryAddScoped<ISecurityStampValidator, SecurityStampValidator<TUser>>();
  services.TryAddScoped<IUserClaimsPrincipalFactory<TUser>, UserClaimsPrincipalFactory<TUser, TRole>>();
  services.TryAddScoped<UserManager<TUser>, AspNetUserManager<TUser>>();
  services.TryAddScoped<SignInManager<TUser>, SignInManager<TUser>>();
  services.TryAddScoped<RoleManager<TRole>, AspNetRoleManager<TRole>>();
  if (setupAction != null)
    services.Configure<IdentityOptions>(setupAction);
  return new IdentityBuilder(typeof (TUser), typeof (TRole), services);
}

有人知道如何解决我的UserManager问题吗? 任何帮助将不胜感激。

最佳答案

您在这里所做的只是测试您编写的代码来测试您的代码。而且,即使如此,您最终希望测试的代码还是框架代码,您一开始就不应该对其进行测试。身份由广泛的测试套件涵盖。您可以放心地假设像 FindByNameAsync 这样的方法可以工作。这完全是对时间和精力的巨大浪费。

要真正进行集成测试,您应该使用 TestServer 来执行诸如 Register 操作之类的操作。然后,您断言“发布”到该操作的用户实际上最终进入了数据库。扔掉所有其他无用的代码。

关于c# - 与.net core和身份框架的集成测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/49273769/

相关文章:

c# - 如何轻松替换 Microsoft Fakes 程序集?

java - 独立 Wiremock + 使用的端口

java - 使用 JUnit 进行容器内测试

java - 如何测试飞路迁移?

c# - 使用 Prism 和 MVVM 模式在 WPF 中创建模态对话框的 "pretty"方法

c# - 将字符串转换为 mm/dd/yyyy 格式

c#变量的可见性

c# - 将强类型列表添加到数据库

javascript - 在 Angular 中渲染组件/HTML 文件之前获取数据

javascript - 将 blob 文件发送到服务器