在Visual Studio Online中进行数据库集成测试

15

我很喜欢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月:我在博客中介绍了如何在VSTS中进行集成测试。


不是关于Visual Studio的问题,但是你尝试过快照吗?你只需要创建一次数据库,在测试之前创建快照,然后在每次构建运行时使用“RESTORE DATABASE [dbname] FROM DATABASE_SNAPSHOT”即可。 - Alex Yu
@Ingaz 要进行快照,我需要在某个地方运行一个数据库 - 服务器。这就是我正在寻找的 - 在构建实例上的 dB 服务器。 - trailmax
4个回答

20
TFS构建服务器预装了MSSQL Server 2012和MSSQL Server 2014 LocalDB。只需将以下一行代码放入解决方案的后期生成事件中,即可创建一个名为MYTESTDB的LocalDB实例以满足您的需求。这将允许您连接到(LocalDB)\MYTESTDB并顺利运行数据库集成测试。

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

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

来源:SqlLocalDB 实用工具


现在我们开始谈论了!谢谢你 - 这正是我在寻找的。我会在周末尝试一下,如果一切顺利,我会接受你的答案。 - trailmax
我看到了已安装软件的列表,但不确定如何访问SQL Server。有文档可以参考吗?我阅读了相关博客,但它并不特定于VSO,并且进一步的细节也没有提供。 - trailmax
1
据我所知,没有这样的文档,您必须使用特定软件的文档。 "SqlLocalDB create" 就是您通常使用 LocalDB 的方式。 - Roman Pletnev
@trailmax 你的连接字符串是什么样子的? - Jose Alonso Monge
2
@JoseAlonsoMonge 像这样:Server=(localdb)\v12.0;Database=MyTestingDbName - trailmax

5
在Azure DevOps中,使用.net Core和EF Core,我采用了一种不同的技术。我使用SQLite内存数据库来执行集成测试和端到端测试。目前在.net Core中,您可以使用InMemory数据库和带有内存选项的SQLite,在默认的Azure DevOps CI代理中运行任何集成测试。
InMemory: https://learn.microsoft.com/en-us/ef/core/miscellaneous/testing/in-memory 请注意,InMemory数据库不是关系数据库,它是一个多用途数据库,只提及其中一种限制:
"InMemory将允许您保存违反关系数据库引用完整性约束的数据"
SQLite内存模式 https://learn.microsoft.com/en-us/ef/core/miscellaneous/testing/sqlite 这种方法为测试提供了更真实的平台。
现在,我进一步进行了操作,不仅希望能够在Azure DevOps中运行具有数据库依赖性的集成测试,还希望能够在CI Agent中托管我的WebAPI,并在API DBcontext和我的Persister对象之间共享数据库(Persister对象是一个帮助类,允许我自动生成任何类型的实体并将它们保存到数据库)。
关于集成测试和端到端测试的简要说明:

集成测试

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

端到端测试

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


在 .net EF Core 和 WebHostBuilder 之前

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

利用 SQLite 内存缓存=共享和 WebHostBuilder

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

SQLite file::memory:?cache=shared

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

WebHostBuilder

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

解决方案

当初始化端到端测试时,创建一个新客户端连接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<IHouseKeeperContext>(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;            
        }
    }
    [...]
}

示例测试,首先我使用持久器生成数据并将其种植在数据库中,然后我使用API获取数据,测试也可以反转,使用POST请求设置数据,然后使用DBContext读取数据库并确保创建成功。

[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(免费版)中此代码部分构建执行的屏幕截图。 enter image description here

很抱歉这篇文章比预期的要长。


1

市场上有一个名为“Redgate SQL CI”的VSTS扩展,您可能想要尝试。请参阅此链接以获取详细信息:

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

•构建 - 从源代码控制中的数据库脚本文件夹将您的数据库构建成NuGet包

•测试 - 运行tSQLt测试以针对数据库

•同步 - 将软件包与集成数据库同步

•发布 - 将软件包发布到NuGet流


谢谢你的回答,Eddie。但这并不是我需要的。实际上,我需要一个数据库来运行测试。我已经有了所有运行测试所需的代码/基础设施。 - trailmax

0

您应该将集成测试(需要应用程序实例的任何内容)推到作为发布流程一部分的环境中运行。

在您的构建中,只需进行编译和单元测试。如果一切正常,您应该触发一个发布,作为您发布流程的一部分,第一步应该是将数据库部署到 Azure 服务器上。

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

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


很抱歉,这不是一个简单的问题。数据库测试是我发布流程的一部分,使用虚拟机并不能使其更便宜或更快速地执行。我曾经从带有SQL Server的虚拟机开始,然后转移到了Azure SQL,但这两种情况都不太适用于这个项目。 - trailmax

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