代码之家  ›  专栏  ›  技术社区  ›  r3plica

与的集成测试。net核心和标识框架

  •  2
  • r3plica  · 技术社区  · 7 年前

    我就是找不到任何答案。 我已经阅读了多篇文章并查看了大量的源代码,但似乎都没有帮助。

    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://docs.microsoft.com/en-us/aspnet/core/testing/integration-testing

    我遇到的问题是解决服务而不是使用 HttpClient 测试控制器。 这是我的创业课程:

    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个安装类。第一个是 测试启动 :

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

    它处理我的所有db创建和服务配置。 第二个是我的 测试夹具 类别:

    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 应该解析我的服务的方法。 现在是我的测试。 我创建了一个 用户上下文 类,如下所示:

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

    不幸的是,当我运行测试并声明:

    消息:系统。InvalidOperationException:无法解析类型“Microsoft”的服务。AspNetCore。身份IUserStore 1[vmpyr.Data.Models.User]' while attempting to activate 'Microsoft.AspNetCore.Identity.UserManager 1[vmpyr.Data.Models.User]'。

    我想这是在告诉我 中央层 服务,找不到 用户存储区 构造函数中使用的依赖项。 我已经看过了 services.AddIdentityCore<User>(null) 可以看到它没有出现在寄存器中 用户存储区 :

    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>() 方法,并且似乎也没有注册 用户存储区 :

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

    有人知道我如何解决 中央层 ? 任何帮助都将不胜感激。

    1 回复  |  直到 7 年前
        1
  •  3
  •   Chris Pratt    7 年前

    您在这里所做的就是测试您为测试代码而编写的代码。即便如此,您最终希望测试的代码是 框架代码 ,您首先不应该测试它。Identity包含在广泛的测试套件中。您可以放心地假设 FindByNameAsync 作品这是对时间和精力的巨大浪费。

    要真正进行集成测试,您应该使用 TestServer 打一个像 Register 行动然后,您断言“发布”到该操作的用户实际上在数据库中结束。扔掉所有其他无用的代码。