.net 核心和身份框架的集成测试
integration testing with .net core and identity framework
我在任何地方都找不到任何答案。
我阅读了多篇文章并查看了大量源代码,但其中 none 似乎有所帮助。
http://www.dotnetcurry.com/aspnet-core/1420/integration-testing-aspnet-core
https://docs.microsoft.com/en-us/aspnet/core/testing/integration-testing
我遇到的问题是解决服务问题,而不是使用 HttpClient 来测试控制器。
这是我的创业公司 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 个设置 classes。第一个是 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 class:
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 class,它看起来像这样:
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("tim@tim.com");
var user = await services.WhenGetUserAsync("tim@tim.com");
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.UserManager
1[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
的操作。然后,您断言该操作的用户 "posted" 实际上最终出现在数据库中。扔掉所有这些无用的代码。
我在任何地方都找不到任何答案。 我阅读了多篇文章并查看了大量源代码,但其中 none 似乎有所帮助。
http://www.dotnetcurry.com/aspnet-core/1420/integration-testing-aspnet-core
https://docs.microsoft.com/en-us/aspnet/core/testing/integration-testing
我遇到的问题是解决服务问题,而不是使用 HttpClient 来测试控制器。 这是我的创业公司 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 个设置 classes。第一个是 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 class:
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 class,它看起来像这样:
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("tim@tim.com");
var user = await services.WhenGetUserAsync("tim@tim.com");
user.Should().NotBe(null);
}
}
不幸的是,当我 运行 测试并指出时出错:
Message: System.InvalidOperationException : Unable to resolve service for type 'Microsoft.AspNetCore.Identity.IUserStore
1[vmpyr.Data.Models.User]' while attempting to activate 'Microsoft.AspNetCore.Identity.UserManager
1[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
的操作。然后,您断言该操作的用户 "posted" 实际上最终出现在数据库中。扔掉所有这些无用的代码。