Visual Studio Online中的数据库集成测试

时间:2016-01-05 00:11:34

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

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

在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中进行集成测试

4 个答案:

答案 0 :(得分:19)

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

答案 1 :(得分:2)

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

InMemory 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中运行具有数据库依赖性的集成测试,我还希望能够将我的WebAPI托管在CI代理中,并共享API DBcontext和我的Persister对象之间建立数据库(Persister对象是一个帮助器类,允许我自动生成任何种类的实体并将其保存到数据库中。)


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

集成测试

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

端到端测试

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


.net EF Core和WebHostBuilder之前

到目前为止,我知道唯一可行的方法是拥有专用的服务器,VM或docker映像,负责为API提供服务,还必须可以从Web或Azure DevOps对其进行访问。 设置我的集成测试以重新创建数据库,或者足够聪明/有限以完全忽略现有数据,并确保每个测试都可以抵抗数据损坏和完全可靠(没有错误的否定或肯定的结果)。 然后,我必须配置构建定义才能运行测试。

通过cache = shared和WebHostBuilder在内存中利用SQLite

下面,我首先描述我使用的两种不可思议的技术,然后添加一些代码来演示如何实现。

SQLite文件::内存:?cache =共享

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,该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

对不起,这最终比预期的要长。

答案 2 :(得分:1)

您可能想尝试在市场中使用VSTS的“Redgate SQL CI”扩展。有关详细信息,请参阅此链接:

  

在扩展程序中,有四种可用的操作:

     

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

     

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

     

•同步 - 将程序包同步到集成数据库

     

•发布 - 将包发布到NuGet流

答案 3 :(得分:0)

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

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

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

即使您没有使用发布工具发布,这对您也有用。