Yes, I’m now officially an open source contributor, and the project’s even got it’s logo, so you know it’s gotta be good.
So what we have attempted to do is to make a lightweight, easy to use, no set up tool to be used when testing code that sits on top of a database. With this tool, DataWings, you can set up the database so that your tests are accessing known data and assert that the database is in the expected state after the tests have executed. And all of this is done directly in the test code.
Here’s a more detailed description of what the tool does, and how it works:
First, a word of caution
DataWings is designed to be a tool to be used at design time and during testing. No attention has been paid to security issues, and we definitely do not recommend using this code in production.
Configuring the connection string
The first thing you need to do when using DataWings is to configure the connection string(s) to be used. The functionality for doing this is purposefully designed with two goals in mind: a) making it easy to set up the connection string in code in order to "get going" as fast as possible, and b) making is easy to maintain the connection string outside of code thus helping to ensure that the test will remain operative in the future.
The connection string is set by decorating either the class or method with an attribute. There are several different kinds of attributes thata can be used, but here we'll focus on ConnectionStringFromConfigFile. As the name implies, this attribute is used when the connection string is registered in the standard <connectionStrings> section of the configuration file. A typical usage of this attribute might look like this:
[ConnectionFromConfigFile(SqlVendor.Oracle, Key = "MyConnection", AlternativeConnection = "TheConnectionString")]
Here, the sql vendor (input to the constructor) dictates which ADO.NET provider that will be used behind the scenes, the Key property specifies the name of the connection string in the configuration file to use, and the AlternativeConnection property is set with the specified connection string is not found in the configuration file or if any other problem is detected while trying to look up this string.
How the attribute is resolved
As mentioned, the connection string attribute can be used to decorate both methods and classes. The process of resolving which attribute to be used is carried out by walking the stack looking for an appropriate attribute. The algorithm first looks at the executing method of the stack frame, and if this method does not have an appropriate decoration, the class of the executing method is examined. This process continues for each stack frame until a decoration is found. If no such decoration can be located, an exception is raised.
Named connection string
The ConnectionFromConfigFile attribute also has a property called Name. This property is useful in situations where the tests touch more than one database. All the static gateway classes into the DataWings functionality (DataBoy, DbAssert and Adversary - see below) have a ForConnection() method, through which the named connection attribute to be used can be specified.
Here's a sample of such a named decoration:
[ConnectionFromConfigFile(SqlVendor.Oracle, Name="Default", Key = "MyConnection", AlternativeConnection = "TheConnectionString")]
DataBoy
Standard usage
DataBoy provides functionality for keeping the data in the database in a consistent state so that the tests are running against known data . This sample shows the standard usage of DataBoy.
DataBoy
.ForTable("Person")
.Row("IdPerson", 1).Data("Surname", "Obama").DeleteFirst()
.Row("IdPerson", 2).Data("Surname", "Bush").DeleteFirst()
.ForTable("Address")
.Row("IdAddress", 100).Data("Street", "Main street").DeleteFirst()
.Commit();
Internally DataBoy keeps track of changes in a session and this session resides only in memory until the Commit() method is invoked. The table to insert data into is specified with the ForTable() method, and this table will be the one that is used for all subsequent row specifications until another call to ForTable() is encountered.
Each row to be inserted is marked by the Row() method, and this method takes the column name and the value the (presumably) uniquely identifies the row as input. The row can receive the DeleteFirst() method, and if this method has been invoked, DataBoy will delete the row from the database before it is inserted.
Order of execution
The rows of the session are traversed twice, first from back to front and then from front to back. In the first parse (backwards), any deletions are performed (i.e. rows marked with DeleteFirst()) while the insert are performed in the second traversal. In this way it should largely be possible to order the statements so that any errors due to foreign key constraint violations are avoided.
Updating instead of deleting
By invoking the ForUpdate() method an update statement (instead of an insert) will be generated and invoked the row in question. Example:
DataBoy.ForTable("Person").Row("IdPerson", 1).Data("Surname", "Obama").ForUpdate().Commit();
Just deleting
Rows can be deleted through the usage of the ForDelete() method. Example:
DataBoy.ForTable("Person").Row("IdPerson", 1).ForDelete().Commit();
Executing custom queries
The ExecuteNonQuery() method supplies a way to invoke custom queries directly against the database:
string sqlQuery = "INSERT INTO Address (IdAddress, Street) VALUES (99, 'Some Street')";
DataBoy.ExecuteNonQuery(sqlQuery).Commit();
DbAssert
The static class DbAssert is for asserting that data in the database is in the expected state. This class is quit similar to the familiar Assert class of many unit test frameworks.
In order to set up an assertion you first need to specify which table you are testing against. As with DataBoy, this done through the ForTable() method. When the table has been specified we need to tell the framework which row we are interested in, and this is accomplished with the method WithColumnValuePair(). This method takes a column name and corresponding value as input, and generally this will be the column name of primary key and the primary key for the row of interest. If more than one row exist for this column value pair, the first row encountered (randomly) will be used.
When the assertion has been set up, the actual assertion can be specified:
Exists(), NotExists()
Determines whether the row in question exists at all. Example:
string columnName = "Surname";
string columnValue = "Obama";
DbAssert
.ForTable("Person")
.WithColumnValuePair(columnName, columnValue)
.Exists();
AreEqual()
Determines whether the value in the specified row equals the specified value. Example:
DbAssert.ForTable("Person")
.WithColumnValuePair("IdPerson", 1)
.AreEqual("FirstName", "Barack");
Evaluate()
This method returns the entire row, and you can perform arbitrarily complex tests on the values of this row by using a lambda expression. Example:
DbAssert.ForTable("Person")
.WithColumnValuePair("IdPerson", 1)
.Evaluate(row =>
row.GetResult("FirstName") == "Barack" &&
.row.GetResult("IsPresident") == true);
Adversary
Adversary is a static gateway class providing functionality for provoking conflicts in optimistic concurrency scenarios. This code is still in a very early phase, and hopefully will mature in the future
Sql provider provisioning and built in providers
DataWings natively support SQL Server and Oracle database engines by using the System.Data.SqlClient and System.Data.OracleClient of the .NET framework. Additionally, DataWings supports SQLite through the separate assembly DataWings.SQLite. This support for SQLite has been realized by use of the built-in provider provisioning infrastructure. This is a model where you can develop support for your favorite database engine by implementing two simple interfaces. Hopefully, I'll be able to get into more details about this at a later stage.