So in the last example we created a .NET Core project that connected to an Oracle database and displayed a list of products from Northwind. For this example we are going to write some tests to make sure our Linq2Db is working
Our goal here is to:
Let's get started!
We are going to use Sqlite as our database engine. The database will be stored in memory. That means we are going to need a build or DDL script to create the database objects for us.
We could create this from scratch, but using SqlWrangler is a lot faster.
First connect to your database using SQL Wrangler. Execute the following sql:
Some rows will return. Now click the button button.
select * from northwind.products
A window will appear.
/* CREATE TABLE northwind_products */
create table northwind_products (
PRODUCT_ID INT NOT NULL,
PRODUCT_NAME VARCHAR(40) NOT NULL,
SUPPLIER_ID INT NOT NULL,
CATEGORY_ID INT NOT NULL,
QUANTITY_PER_UNIT VARCHAR(20),
UNIT_PRICE FLOAT NOT NULL,
UNITS_IN_STOCK INT NOT NULL,
UNITS_ON_ORDER INT NOT NULL,
REORDER_LEVEL INT NOT NULL,
DISCONTINUED VARCHAR(1) NOT NULL
);
/* INSERT TABLE northwind_products */
insert into northwind_products (PRODUCT_ID, PRODUCT_NAME, SUPPLIER_ID, CATEGORY_ID, QUANTITY_PER_UNIT, UNIT_PRICE, UNITS_IN_STOCK, UNITS_ON_ORDER, REORDER_LEVEL, DISCONTINUED) values (1, 'Chai', 1, 1, '10 boxes x 20 bags', 18, 39, 0, 10, 'N');
insert into northwind_products (PRODUCT_ID, PRODUCT_NAME, SUPPLIER_ID, CATEGORY_ID, QUANTITY_PER_UNIT, UNIT_PRICE, UNITS_IN_STOCK, UNITS_ON_ORDER, REORDER_LEVEL, DISCONTINUED) values (2, 'Chang', 1, 1, '24 - 12 oz bottles', 19, 17, 40, 25, 'N');
insert into northwind_products (PRODUCT_ID, PRODUCT_NAME, SUPPLIER_ID, CATEGORY_ID, QUANTITY_PER_UNIT, UNIT_PRICE, UNITS_IN_STOCK, UNITS_ON_ORDER, REORDER_LEVEL, DISCONTINUED) values (76, 'Lakkalik??ri', 23, 1, '500 ml', 18, 57, 0, 20, 'N');
insert into northwind_products (PRODUCT_ID, PRODUCT_NAME, SUPPLIER_ID, CATEGORY_ID, QUANTITY_PER_UNIT, UNIT_PRICE, UNITS_IN_STOCK, UNITS_ON_ORDER, REORDER_LEVEL, DISCONTINUED) values (77, 'Original Frankfurter gr?ne So?e', 12, 2, '12 boxes', 13, 32, 0, 15, 'N');
Keep in mind SQLite doesn't have the concept of schemas. So to mirror that functionality we are using [SCHEMANAME]_[TABLENAME]. So NORTHWIND.PRODUCTS becomes NORTHWIND_PRODUCTS. This will cause some problems later on, but nothing major.
Armed with our build script it's time to write some code!
Open up the previous project and add a new Tests project
I renamed the default UnitTests1.cs to BasicTests.cs. We are going to need to install a package to talk to Sqlite, so using the PackageManagerConsole run:
Be sure to add it to the Tests project (The drop down in the PackageManagerConsole)! The other projects won't need it.
install-package Microsoft.Data.Sqlite
The code we are going to add is
Now let's add our build script by creating a new text document call it "build_northwind.sql" and copy and paste the output from the SqlWrangler output into it. Be sure to set the "Copy To Output Directory" to "Copy Always".
Our tests are going to need to share the database so we will need a fixture for the xUnit tests. So let's create one.
This may seem strange. I explain why we need this and how it works later on.
using System;
namespace Northwind.Tests
{
public class NorthwindDbFixture : IDisposable
{
public NorthwindSetup Setup { get; }
public NorthwindDbFixture()
{
//DataSource=:memory: makes Sqlite use in memory
DbNorthwindStartup.Init(new SqliteDbSettings("Data Source=:memory:;"));
//Let's run this which will create our database and change our mapping names
Setup = new NorthwindSetup("build_northwind.sql", "Northwind.Models");
}
public void Dispose()
{
}
}
}
Now in our BasicTests.cs let's write some tests!
using System.Linq;
using Xunit;
using Xunit.Abstractions;
namespace Northwind.Tests
{
public class BasicTests : IClassFixture
Using the fixture will maintain the value of the _setup variable across the tests. Basically every time a test runs in xUnit it will instantiate a new class (call the constructor) then call the individual test. By implementing IClassFixture
The ITestOutputHelper let's us display some output to test results by doing _output.WriteLine("Products: {0}", query.Count);
Now let's create a DbSettings for our Sqlite database so Linq2Db can talk to it.
This looks a lot like our DbSettings in the Northwind project for Oracle. The only real change is the provider is now Microsoft.Data.Sqlite.
using System.Collections.Generic;
using LinqToDB.Configuration;
namespace Northwind.Tests
{
public class ConnectionStringSettings : IConnectionStringSettings
{
public string ConnectionString { get; set; }
public string Name { get; set; }
public string ProviderName { get; set; }
public bool IsGlobal => false;
}
public class SqliteDbSettings : ILinqToDBSettings
{
public IEnumerable
Our code is using this DbSettings code in the NorthwindDbFixture.
DbNorthwindStartup.Init(new SqliteDbSettings("Data Source=:memory:;"));
Next we are going to need something that builds up our database with tables and data so let's create a Setup for that!
See that ChangeTableNames() method at the end. We need that because Linq2Db basically ignores the schema name when using Sqlite.
using System;
using System.IO;
using System.Linq;
using LinqToDB.Data;
using LinqToDB.Mapping;
namespace Northwind.Tests
{
public class NorthwindSetup
{
private readonly string[] _nameSpaces;
private bool _namesChanged;
private readonly string[] _buildCommands;
public NorthwindSetup(string buildFile, params string[] nameSpaces)
{
if (buildFile == null) throw new ArgumentNullException(nameof(buildFile));
if (!File.Exists(buildFile)) throw new FileNotFoundException(
string.Format("Unable to find '{0}'", buildFile));
_nameSpaces = nameSpaces ?? throw new ArgumentNullException(nameof(nameSpaces));
using (var sr = new StreamReader(buildFile))
{
var s = sr.ReadToEnd();
_buildCommands = s.Split(';');
}
}
public DbNorthwind GetDbNorthwind()
{
var db = new DbNorthwind();
//Have to change our table names because Linq2Db for Sqlite doesn't use the schema name
ChangeTableNames(db, _nameSpaces);
//build up the database from our script
foreach (var cmd in _buildCommands)
{
db.Execute(cmd);
}
return db;
}
private void ChangeTableNames(DataConnection db, string[] nameSpaces)
{
//What this does?
//Given a namespace look for classes we have registered with Linq2Db
//and change the table name to schema_tablename.
//Only do it once
if (_namesChanged) return;
_namesChanged = true;
//get the classes in the namespaces
foreach (var item in AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(t => t.GetTypes())
.Where(t => t.IsClass && nameSpaces.Contains(t.Namespace)))
{
var descriptor = db.MappingSchema.GetEntityDescriptor(item);
if (descriptor.SchemaName != null)
{
//The reflection is here to deal with the generic methods.
var builder = db.MappingSchema.GetFluentMappingBuilder();
var method = typeof(FluentMappingBuilder).GetMethod("Entity");
var genericMethod = method.MakeGenericMethod(item);
var entityBuilder = genericMethod.Invoke(builder, new object[] { null });
var tblMethod = entityBuilder.GetType().GetMethod("HasTableName");
tblMethod.Invoke(entityBuilder, new object[] {
string.Format("{0}_{1}", descriptor.SchemaName, descriptor.TableName)});
}
}
}
}
}
Linq2Db with SqlLite when executing db.Product.Where(o => o.UnitPrice < 20) is going to execute select * from products where.... Remember our table is called NORTHWIND_PRODUCTS to accomodate the idea of schemas.
So we are changing the table names in the mappings to follow our format of [SCHEMANAME]_[TABLENAME]. The lines of reflection are needed because of the generic functions. This way we execute select * from northwind_products where... when we execute our queries.
We want it to work for all the db mapped classes so we don't have to specify them. So any class in a namespace we specified that is mapped with Linq2Db, it will change the table names for us automatically. It discovers them and changes them for us.
If you wanted to you could specify them by hand by doing something like:
db.MappingSchema.GetFluentMappingBuilder().Entity
Also it is important not to call the ChangeNames more than once. It's kind of expensive that is why this is there.
//Only do it once
if (_namesChanged) return;
_namesChanged = true;
Although we are changing the names once. The database is being recreated for every test! So keep this in mind. The database goes away when the connection is closed or disposed. See the end of that using statement... It goes away at that point.
using (var db = _setup.GetDbNorthwind())
{
var query = db.Product.Where(o => o.UnitPrice < 20).OrderByDescending(o => o.ProductName)
.ToList();
Assert.True(query.Any());
}
//No more database because connection is disposed!
Finally this setup is called in the NorthwindDbFixture.
Setup = new NorthwindSetup("build_northwind.sql", "Northwind.Models");