Skip to content

Unit tests project. How it works

Sébastien Pertus edited this page Sep 8, 2018 · 6 revisions

Let's see how the new test project actually work

To be the most straightforward as possible, the test project is written with in mind several objectives:

  • Database agnostic
  • Write only one test (no more one test per provider)
  • Run on all providers for all configurations, on both HTTP and TCP
  • Test your provider with less than 20 lines of code (more or less)

Database agnostic code

All tests are written with Entityframework.Core to be sure we are agnostic to the database provider.

All class in the /Models directory are EntityFramework Core items (Table class and Database context)

The database model is a heavy modified AdventureWorks light model. Indeed, you will retrieve same tables as AdventureWorks, but adapted to all the situations we want to tests (plus some new tables)

The AdventureWorksContext class use a code first, fluent api and seeding data to generate the server database.

EntityFramework Core context compatible with all providers

To manage all specific providers situations, the EF Core Context has a special enum ProviderType to handle theses providers.

First example: Managing EF Core provider (and its ConnectionString):

switch (ProviderType)
{
    case ProviderType.Sql:
        optionsBuilder.UseSqlServer(ConnectionString);
        break;
    case ProviderType.MySql:
        optionsBuilder.UseMySql(ConnectionString);
        break;
    case ProviderType.Sqlite:
        optionsBuilder.UseSqlite(ConnectionString);
        break;
}

Second example, the ModifiedDate default value (of the entity Customer) is not the same on Sql and on MySql:

// Skiping code to see the revelant code section we want to highlight here
modelBuilder.Entity<Customer>(entity =>
{
    if (this.ProviderType == ProviderType.Sql)
        entity.Property(e => e.ModifiedDate).HasDefaultValueSql("(getdate())");
    else if (this.ProviderType == ProviderType.MySql)
        entity.Property(e => e.ModifiedDate).HasDefaultValueSql("CURRENT_TIMESTAMP()");
}

Seeding datas

The initial data are generated in a seeding method:

protected void OnSeeding(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Address>().HasData(
        new Address { AddressId = 1, AddressLine1 = "8713 Yosemite Ct.", City = "Bothell", StateProvince = "Washington", CountryRegion = "United States", PostalCode = "98011" },
        new Address { AddressId = 2, AddressLine1 = "1318 Lasalle Street", City = "Bothell", StateProvince = "Washington", CountryRegion = "United States", PostalCode = "98011" 
    );

    Guid customerId1 = Guid.NewGuid();

    modelBuilder.Entity<Customer>().HasData(
        new Customer { CustomerId = customerId1, NameStyle = false, Title = "Mr.", FirstName = "Orlando", MiddleName = "N.", LastName = "Gee", CompanyName = "A Bike Store", SalesPerson = @"adventure-works\pamela0", EmailAddress = "orlando0@adventure-works.com", Phone = "245-555-0173", PasswordHash = "L/Rlwxzp4w7RWmEgXX+/A7cXaePEPcp+KwQhl2fJL7w=", PasswordSalt = "1KjXYs4=" }
    );

    modelBuilder.Entity<CustomerAddress>().HasData(
        new CustomerAddress { CustomerId = customerId1, AddressId = 4, AddressType = "Main Office" },
        new CustomerAddress { CustomerId = customerId1, AddressId = 5, AddressType = "Office Depot" }
    );

    modelBuilder.Entity<ProductCategory>().HasData(
        new ProductCategory { ProductCategoryId = "BIKES", Name = "Bikes" },
        new ProductCategory { ProductCategoryId = "COMPT", Name = "Components" }
    );
}

Create a database for a specific provider

Using this agnostic provider made the database generation easy.

Here is the code used to generate the whole database (schema and seeding datas) on SQL Server:

using (var ctx = new AdventureWorksContext(ProviderType.Sql, connectionString))
{
 ctx.Database.EnsureCreated();
}

Here is the code used to generate the whole database (schema and seeding datas) on MySql:

using (var ctx = new AdventureWorksContext(ProviderType.MySql, connectionString))
{
 ctx.Database.EnsureCreated();
}

A test method should be tested on every providers on every configurations

What we want, when we are writing a test:

  • Write it only once (and not for all providers)
  • Run it for all SyncConfiguration options
  • On both HTTP and TCP

Write once, run on all providers

The abstract class BasicTests defines all the tests, in an agnostic way. Each of the tests methods are marked virtual to be able to override them in the real provider test class.

Indeed, your provider class which inherits BasicTests will be the class with all XUnit data annotations. Under the hood, your class will just call the base class with the correct provider.

Here is an example, inspired from the SqlServerBasicTests class:

[TestCaseOrderer("Dotmim.Sync.Tests.Misc.PriorityOrderer", "Dotmim.Sync.Tests")]
[Collection("SqlServer")]
public class SqlServerBasicTests : BasicTestsBase, IClassFixture<SqlServerFixture>
{
    public SqlServerBasicTests(SqlServerFixture fixture) : base(fixture)
    {
    }

    [Fact, TestPriority(1)]
    public override Task Insert_One_Table_From_Server()
    {
        return base.Insert_One_Table_From_Server();
    }
}

And an extract from the BasicTests class:

public async virtual Task Insert_One_Table_From_Server()
{
    foreach (var conf in TestConfigurations.GetConfigurations())
    {
        var name = GetRandomString().ToLowerInvariant();
        var productNumber = GetRandomString(10).ToUpperInvariant();

        Product product = new Product { ProductId = Guid.NewGuid(), Name = name, ProductNumber = productNumber };

        using (var serverDbCtx = GetServerDbContext())
        {
            serverDbCtx.Product.Add(product);
            await serverDbCtx.SaveChangesAsync();
        }

        var results = await this.testRunner.RunTestsAsync(conf);

        foreach (var trr in results)
            Assert.Equal(1, trr.Results.TotalChangesDownloaded);
    }
}

This method does not have any Sql or MySql script, anyway it performs an Insert into the server database. Thanks to EntityFramework Core this method is agnostic to all databases provider, and will work on both Sql, MySql, Sqlite and so on...

For all SyncConfiguration options

The foreach (var conf in TestConfigurations.GetConfigurations()) statement will create all the SyncConfiguration options we can use (use bulk operations or not, use a batch folder or not, use JSON on Binary and so on ...)

For all client providers

The await this.testRunner.RunTestsAsync(conf) statement will be in charge to launch your test method on:

  • Each client provider (for now, it will test on SQL Server, MySql and Sqlite
  • Both on TCP using a simple SyncAgent and on HTTP using a combo WebProxyServerProvider / WebProxyClientProvider

Behind the scene you have the class ProviderRun in charge to launch your test on both HTTP and TCP:

Code extract of the RunAsync() method:

// server proxy
var proxyServerProvider = new WebProxyServerProvider(serverFixture.ServerProvider);
var proxyClientProvider = new WebProxyClientProvider();

var syncTables = tables ?? serverFixture.Tables;

// local test, through tcp
if (NetworkType == NetworkType.Tcp)
{
    // create agent
    if (this.Agent == null || !reuseAgent)
        this.Agent = new SyncAgent(Provider, serverFixture.ServerProvider, syncTables);

    // copy conf settings
    if (conf != null)
        serverFixture.CopyConfiguration(this.Agent.Configuration, conf);
    Results = await this.Agent.SynchronizeAsync();
}

// -----------------------------------------------------------------------
// HTTP
// -----------------------------------------------------------------------

// tests through http proxy
if (NetworkType == NetworkType.Http)
{
    var syncHttpTables = tables ?? serverFixture.Tables;

    // client handler
    using (var server = new KestrellTestServer())
    {
        // server handler
        var serverHandler = new RequestDelegate(async context =>
        {
            // sync
            await proxyServerProvider.HandleRequestAsync(context);
        });
...
}

Writing your tests for your provider

  • Write your server fixture.
  • Write your server class tests.

Example from the Sql Server provider:

  • SqlServerFixture.cs : Just inherits from the base ProviderFixture<CoreProvider>, set the correct ProviderType and complete the method to retrieive your provider:
public class SqlServerFixture : ProviderFixture<CoreProvider>
{
    public override ProviderType ProviderType => ProviderType.Sql;

    public override CoreProvider NewServerProvider(string connectionString)
    {
        return new SqlSyncProvider(connectionString);
    }
}
[TestCaseOrderer("Dotmim.Sync.Tests.Misc.PriorityOrderer", "Dotmim.Sync.Tests")]
[Collection("SqlServer")]
public class SqlServerBasicTests : BasicTestsBase, IClassFixture<SqlServerFixture>
{
    public SqlServerBasicTests(SqlServerFixture fixture) : base(fixture)
    {
    }

    [Fact, TestPriority(0)]
    public override Task Initialize()
    {
        return base.Initialize();
    }

    [Fact, TestPriority(1)]
    public override Task Insert_One_Table_From_Server()
    {
        return base.Insert_One_Table_From_Server();
    }
}

Note : To be able to run all providers test in parrallel, and be sure there won't be any collision, please provide a une Collection("") attribute

And that's all !

Configuring the Dotmim.Sync.Test project

All the user configuration is made in the Setup class, especially in the OnConfiguring method.

You will be able to:

  • Setup the providers you want to test
  • Setup the network type (TCP / HTTP) you want to test
  • Setup the server database name & connection string
  • Setup the tables you want to test
internal static void OnConfiguring<T>(ProviderFixture<T> providerFixture) where T : CoreProvider
{
    // Set tables to be used for SQL Server (including schema)
    var sqlTables = new string[]
    {
        "SalesLT.ProductCategory", "SalesLT.ProductModel", "SalesLT.Product", "Customer", "Address", "CustomerAddress",
        "SalesLT.SalesOrderHeader", "SalesLT.SalesOrderDetail", "dbo.Sql", "Posts", "Tags", "PostTag"
    };

    // Set tables to be used for MySql
    var mySqlTables = new string[]
    {
        "productcategory", "productmodel", "product", "customer", "address","customeraddress",
        "salesorderheader", "salesorderdetail", "sql", "posts", "tags", "posttag"
    };

    // 1) Add database name
    providerFixture.AddDatabaseName(ProviderType.Sql, "SqlAdventureWorks");
    providerFixture.AddDatabaseName(ProviderType.MySql, "mysqladventureworks");

    // 2) Add tables
    providerFixture.AddTables(ProviderType.Sql, sqlTables);
    providerFixture.AddTables(ProviderType.MySql, mySqlTables);

    // 3) Add runs

    // Enable the test to run on TCP / HTTP and on various client
    // Example : EnableClientType((ProviderType.Sql, NetworkType.Tcp), ProviderType.Sql | ProviderType.MySql | ProviderType.Sqlite)
    // 1st arg : (NetworkType.Tcp, ProviderType.Sql) : For a server provider SQL and on TCP
    // 2nd arg : ProviderType.Sql | ProviderType.MySql | ProviderType.Sqlite : Enable tests on clients of type Sql, MySql and Sqlite

    // SQL Server provider
    providerFixture.AddRun((ProviderType.Sql, NetworkType.Tcp), ProviderType.Sql | ProviderType.Sqlite);
    providerFixture.AddRun((ProviderType.Sql, NetworkType.Http), ProviderType.Sqlite);

    // My SQL (disable http to go faster on app veyor)
    providerFixture.AddRun((ProviderType.MySql, NetworkType.Tcp),ProviderType.MySql | ProviderType.Sqlite);

    // Exception for App veyor to be more efficient :)
    if (!IsOnAppVeyor)
        providerFixture.AddRun((ProviderType.MySql, NetworkType.Http), ProviderType.Sql |ProviderType.MySql | ProviderType.Sqlite);
}