【问题标题】:Database integration tests in Visual Studio OnlineVisual Studio Online 中的数据库集成测试
【发布时间】:2016-01-05 00:11:34
【问题描述】:

我很喜欢 Visual Studio Online 中的新构建工具。允许我做我做本地构建服务器的几乎所有事情。但是我缺少的一件事是集成数据库测试:对于每次构建运行,我都会从脚本重新创建测试数据库并针对它运行 DB 测试。

在 Visual Studio Online 中,我似乎找不到任何可满足我需求的数据库实例。

我尝试为每次构建运行创建 Azure SQL 数据库(通过 PowerShell),然后在构建完成后将其删除。但是创建数据库需要很长时间(与构建过程的其余部分相比)。即使 PowerShell 脚本完成,数据库还没有准备好接受请求——我需要不断检查它是否真的准备好了。所以这个场景变得过于复杂,不可靠。

还有其他选项可以在 Visual Studio Online 中进行数据库 (SQL Server) 集成测试吗?

更新:我想我不是很清楚我需要什么 - 我需要一个免费(非常便宜)的 SQL Server 实例来连接到在 VSO 中的构建代理上运行的那个。 SQL Express 或 SQL CE 或 LocalDB 之类的东西,我可以在其中连接并重新创建数据库以运行 C# 测试。重新创建数据库或运行测试不是问题,有一个有效的连接字符串是一个问题。

2016 年 10 月更新:I've blogged 关于我如何在 VSTS 中进行集成测试

【问题讨论】:

  • 与 Visual Studio 无关,但是:您尝试过 SNAPSHOT 吗?您创建数据库 1 次,在测试之前创建快照,然后在每次构建时运行 RESTORE DATABASE [dbname] FROM DATABASE_SNAPSHOT
  • @Ingaz 要制作快照,我需要一个数据库来在某物上运行——一台服务器。这就是我正在寻找的 - 构建实例上的 dB 服务器

标签: sql-server database integration-testing azure-devops


【解决方案1】:

TFS 构建服务器预装了 MSSQL Server 2012 和 MSSQL Server 2014 LocalDB。

来源: TFS Service - Software on the hosted build server

因此,只需将以下单行代码放入解决方案的构建后事件中,即可根据您的需要创建一个 MYTESTDB LocalDB 实例。这将允许您连接到 (LocalDB)\MYTESTDB 并正常运行数据库集成测试。

"C:\Program Files\Microsoft SQL Server\120\Tools\Binn\SqlLocalDB.exe" create "MYTESTDB" 12.0 -s

来源:SqlLocalDB Utility

【讨论】:

  • 现在我们在讨论!!谢谢你——这正是我想要的。我会在周末试一试,如果一切顺利,我会接受你的回答。
  • 我看过这个已安装软件的列表,但不知道如何访问 SQL Server。在某处有可用的文档吗?我已经阅读了链接的博客,但它并不特定于 VSO,并且提供了更多详细信息。
  • 据我所知没有这样的文档,你必须使用特定软件的文档。 “SqlLocalDB 创建”就是您通常使用 LocalDB 的方式。
  • @trailmax 你的连接字符串是什么样子的?
  • @JoseAlonsoMonge 像这样:Server=(localdb)\v12.0;Database=MyTestingDbName
【解决方案2】:

Azure DevOps 以及 .net Core 和 EF Core 中,我使用了不同的技术。 我使用内存数据库中的 SQLite 来执行集成和端到端测试。 目前在 .net Core 中,您可以使用 InMemory 数据库和带有内存选项的 SQLite,在默认的 Azure DevOps CI 代理中运行任何集成测试。

内存中https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory 请注意,InMemory 数据库不是关系数据库,它是一种多用途数据库,仅提及一个限制:

InMemory 将允许您保存违反引用的数据 关系数据库中的完整性约束

SQLite 处于内存模式 https://docs.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite 这种方法提供了一个更现实的测试平台。

现在,我走得更远了,我不仅希望能够在 Azure DevOps 中运行具有数据库依赖关系的集成测试,还希望能够在 CI 代理中托管我的 WebAPI,并共享API DBcontext 和我的 Persister 对象之间的数据库(Persister 对象是一个帮助类,它允许我自动生成任何类型的实体并将它们保存到数据库中)。


关于集成测试和 Ent to End 测试的简要说明:

集成测试

涉及数据库的集成测试示例可以是数据访问层的测试。在这种情况下,通常会在开始测试时创建一个 DBContext,用一些数据填充目标数据库,使用被测组件来操作数据,然后再次使用 DBContext 来确保满足断言。 这个场景非常简单,在相同的代码中,您可以共享相同的 DBContext 来生成数据并将其注入到组件中。

端到端测试

想象一下,在我的例子中,您想要测试一个 RESTful .net Core WebAPI,确保您的所有 CRUD 操作都按预期工作,并且您想要测试过滤、分页等是否正确。 在这种情况下,在测试(数据设置和/或验证)和 WebAPI 堆栈之间共享相同的 DBContext 要复杂得多。


.net EF Core 和 WebHostBuilder 之前

到目前为止,我知道唯一可行的方法是拥有一个专用服务器、VM 或 docker 映像,负责提供 API,而这些 API 也必须可以从 Web 或 Azure DevOps 访问。 设置我的集成测试以重新创建数据库,或者足够聪明/有限以完全忽略现有数据,并确保每个测试对数据损坏具有弹性并且完全可靠(没有假阴性或阳性结果)。 然后我必须配置我的构建定义来运行测试。

利用内存中的 SQLite 与 cache=shared 和 WebHostBuilder

下面我首先描述我使用的两种主要技术,然后我添加一些代码来展示如何做到这一点。

SQLite 文件::内存:?cache=shared

SQLite 允许您在内存中工作,而不是使用传统的文件,这已经给我们带来了巨大的性能提升,消除了 I/O 瓶颈,但最重要的是,使用选项 cache=shared,我们可以使用同一进程内的多个连接访问相同的数据。如果您需要多个数据库,您可以指定一个名称。 更多信息: https://www.sqlite.org/inmemorydb.html

WebHostBuilder

.net Core 提供主机构建器,WebHostBuilder 允许我们创建一个服务器来启动和托管我们的 WebAPI,这样就可以像托管在真实服务器上一样访问它。 当您在测试类中使用 WebHostBuilder 时,这两个都生活在同一个进程中。 更多信息: https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilder?view=aspnetcore-2.2

解决方案

在初始化 E2E 测试时,创建一个新客户端来连接 api,创建一个 dbcontext,您将使用它来播种数据库并可能进行断言。

测试初始化​​

[TestClass]
public class CategoryControllerTests
{
    private TestServerApiClient _client;
    private Persister<Category> _categoryPersister;
    private Builder<Category> _categoryBuilder;
    private IHouseKeeperContext _context;
    protected IDbContextTransaction Transaction;

    [TestInitialize]
    public void TestInitialize()
    {            
        _context = ContextProvider.GetContext();
        _client = new TestServerApiClient();
        ContextProvider.ResetDatabase();
        _categoryPersister = new Persister<Category>(_context);
        _categoryBuilder = new Builder<Category>();
    }

    [TestCleanup]
    public void Cleanup()
    {
        _client?.Dispose();
        _context?.Dispose();
        _categoryPersister?.Dispose();
        ContextProvider.Dispose();            
    }
    [...]
}

TestServerApiClient 类:

public class TestServerApiClient : System.IDisposable
{
    private readonly HttpClient _client;
    private readonly TestServer _server;

    public TestServerApiClient()
    {            
        var webHostBuilder = new WebHostBuilder();
        webHostBuilder.UseEnvironment("Test");
        webHostBuilder.UseStartup<Startup>();

        _server = new TestServer(webHostBuilder);            
        _client = _server.CreateClient();
    }

    public void Dispose()
    {
        _server?.Dispose();
        _client?.Dispose();
    }
}

ContextProvider 类用于生成 DBContext,可用于播种数据或执行数据库查询以获取断言。

public static class ContextProvider
{
    private static bool _requiresDbDeletion;

    private static IConfiguration _applicationConfiguration;
    public static IConfiguration ApplicationConfiguration
    {
        get
        {
            if (_applicationConfiguration != null) return _applicationConfiguration;

            _applicationConfiguration = new ConfigurationBuilder()
                .AddJsonFile("Config/appsettings.json", optional: false, reloadOnChange: true)
                .AddEnvironmentVariables()
                .Build();

            return _applicationConfiguration;
        }
    }
    private static ServiceProvider _serviceProvider;
    public static ServiceProvider ServiceProvider
    {
        get
        {
            if (_serviceProvider != null) return _serviceProvider;

            var serviceCollection = new ServiceCollection();
            serviceCollection.AddSingleton<IConfiguration>(ApplicationConfiguration);
            var databaseType = ApplicationConfiguration?.GetValue<DatabaseType>("DatabaseType") ?? DatabaseType.SQLServer;                
            _requiresDbDeletion = databaseType == DatabaseType.SQLServer;

            IocConfig.RegisterContext(serviceCollection, null);

            _serviceProvider = serviceCollection.BuildServiceProvider();
            return _serviceProvider;
        }
        set
        {
            _serviceProvider = value;
        }
    }

    /// <summary>
    /// Generate the db context
    /// </summary>
    /// <returns>DB Context</returns>
    public static IHouseKeeperContext GetContext()
    {            
        return ServiceProvider.GetService<IHouseKeeperContext>();
    }

    public static void Dispose()
    {
        ServiceProvider?.Dispose();
        ServiceProvider = null;
    }

    public static void ResetDatabase()
    {
        if (_requiresDbDeletion)
        {
            GetContext()?.Database?.EnsureDeleted();
            GetContext()?.Database?.EnsureCreated();
        }
    }
}

IocConfig 类 是我在框架中用来设置依赖注入的辅助类。上面使用的方法 RegisterContext 负责注册 DBContext 并根据需要进行设置,并且因为这与 WebAPI 使用的类相同,所以使用配置 DatabaseType 来确定要做什么。 在这个类中,你可能会发现大部分的“复杂性”。 在内存中使用 SQLite 时,您必须记住:

  1. 连接不会像使用 SQL Server 时那样自动打开和关闭(这就是我使用的原因:context.Database.OpenConnection();
  2. 如果没有连接处于活动状态,则删除数据库(这就是我使用services.AddSingleton&lt;IHouseKeeperContext&gt;(s ... 的原因,重要的是保持一个连接打开,这样数据库不会被破坏,但另一方面你必须小心关闭测试结束时的所有连接,以便数据库最终被销毁,并且下一个测试将正确地创建一个新的空数据库。

课程的其余部分处理生产和测试设置的 SQL Server 配置。我可以随时设置测试以使用 SQL Server 的真实实例,所有测试都将保持完全独立于其他测试,但它肯定会很慢,并且可能仅适用于夜间构建(如果需要,它取决于系统的大小)。

public class IocConfig
{
    public static void RegisterContext(IServiceCollection services, IHostingEnvironment hostingEnvironment)
    {
        var serviceProvider = services.BuildServiceProvider();
        var configuration = serviceProvider.GetService<IConfiguration>();            
        var connectionString = configuration.GetConnectionString(Constants.ConfigConnectionStringName);
        var databaseType = DatabaseType.SQLServer;

        try
        {
            databaseType = configuration?.GetValue<DatabaseType>("DatabaseType") ?? DatabaseType.SQLServer;
        }catch
        {
            MyLoggerFactory.CreateLogger<IocConfig>()?.LogWarning("Missing or invalid configuration: DatabaseType");
            databaseType = DatabaseType.SQLServer;
        }

        if(hostingEnvironment != null && hostingEnvironment.IsProduction())
        {
            if(databaseType == DatabaseType.SQLiteInMemory)
            {
                throw new ConfigurationErrorsException($"Cannot use database type {databaseType} for production environment");
            }
        }

        switch (databaseType)
        {
            case DatabaseType.SQLiteInMemory:
                // Use SQLite in memory database for testing
                services.AddDbContext<HouseKeeperContext>(options =>
                {
                    options.UseSqlite($"DataSource='file::memory:?cache=shared'");
                });

                // Use singleton context when using SQLite in memory if the connection is closed the database is going to be destroyed
                // so must use a singleton context, open the connection and manually close it when disposing the context
                services.AddSingleton<IHouseKeeperContext>(s => {
                    var context = s.GetService<HouseKeeperContext>();
                    context.Database.OpenConnection();
                    context.Database.EnsureCreated();
                    return context;
                });
                break;
            case DatabaseType.SQLServer:
            default:
                // Use SQL Server testing configuration
                if (hostingEnvironment == null || hostingEnvironment.IsTesting())
                {
                    services.AddDbContext<HouseKeeperContext>(options =>
                    {
                        options.UseSqlServer(connectionString);
                    });

                    services.AddSingleton<IHouseKeeperContext>(s => {
                        var context = s.GetService<HouseKeeperContext>();
                        context.Database.EnsureCreated();
                        return context;
                    });

                    break;
                }

                // Use SQL Server production configuration
                services.AddDbContextPool<HouseKeeperContext>(options =>
                {
                    // Production setup using SQL Server
                    options.UseSqlServer(connectionString);
                    options.UseLoggerFactory(MyLoggerFactory);
                }, poolSize: 5);

                services.AddTransient<IHouseKeeperContext>(service =>
                    services.BuildServiceProvider()
                    .GetService<HouseKeeperContext>());
                break;            
        }
    }
    [...]
}

Sample Test,首先我使用持久化器生成的数据播种在数据库中,然后我使用 API 获取数据,测试也可以反转,使用 POST 请求设置数据,然后使用 DBContext 读取 db 并确保创建成功。

[TestMethod]
public async Task GET_support_orderBy_Id()
{
    _categoryPersister.Persist(3, (c, i) =>
    {
        c.Active = 1 % 2 == 0;
        c.Name = $"Name_{i}";
        c.Description = $"Desc_i";
    });

    var response = await _client.GetAsync("/api/category?&orderby=Id");
    var categories = response.To<List<Category>>();

    Assert.That.All(categories).HaveCount(3);
    Assert.IsTrue(categories[0].Id < categories[1].Id &&
                  categories[1].Id < categories[2].Id);

    response = await _client.GetAsync("/api/category?$orderby=Id desc");
    categories = response.To<List<Category>>();

    Assert.That.All(categories).HaveCount(3);
    Assert.IsTrue(categories[0].Id > categories[1].Id &&
                  categories[1].Id > categories[2].Id);
}

结论

我喜欢我可以在 Azure DevOps 中免费运行 E2E 测试这一事实,性能非常好,这给了我很大的信心,非常适合您想要设置持续交付环境时。 这是 Azure DevOps(免费版)中此代码的部分构建执行的屏幕截图。

很抱歉,这比预期的要长。

【讨论】:

    【解决方案3】:

    市场上有一个用于 VSTS 的“Redgate SQL CI”扩展,您可能想尝试一下。详情见此链接:

    在扩展中,有四个可用的操作:

    •Build – 将您的数据库从数据库构建到 NuGet 包中 源代码管理中的脚本文件夹

    •Test – 针对数据库运行 tSQLt 测试

    •Sync – 将包同步到集成数据库

    •发布 – 将包发布到 NuGet 流

    【讨论】:

    • 感谢您的回答,埃迪。但这不是我所需要的。我实际上需要数据库来运行测试。我有所有代码/基础设施来运行测试。
    【解决方案4】:

    您应该将集成测试(需要应用程序实例的任何东西)作为发布管道的一部分在环境中运行。

    在您的构建中,只需进行编译和单元测试。如果有竞争,您应该触发发布,并且作为发布管道的一部分,您的第一步应该是将数据库部署到 azure 服务器。

    您可以在 Azure 中创建一个已经存在并安装了 SQL Server 的 VM,而不是尝试使用 SQL Azure。使用远程脚本来部署数据库并执行您的测试。

    即使您不使用发布工具来发布,这也适合您。

    【讨论】:

    • 对不起,这不是 A-B 问题。数据库测试是我的发布管道的一部分,拥有一个虚拟机并不能让它更便宜或更快地执行。我确实从带有 SQL Server 的 VM 开始,然后转移到 Azure SQL,这两种情况都不适用于这个项目。
    猜你喜欢
    • 1970-01-01
    • 2022-06-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多