-
-
Notifications
You must be signed in to change notification settings - Fork 196
Unit tests project. How it works
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)
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.
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()");
}
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 = "[email protected]", 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" }
);
}
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();
}
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
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...
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 ...)
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
andSqlite
- Both on TCP using a simple
SyncAgent
and on HTTP using a comboWebProxyServerProvider
/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);
});
...
}
- 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 correctProviderType
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);
}
}
- SqlServerBasicTests.cs : Class that will inherits from BasicTestsBase class and will define the the tests you want to ...test !
[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 !
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);
}