Tuesday, October 20, 2009

DataWings: Convention over Configuration

I've just released some new functionality for the increasingly popular (?) framework for data driven integration testing: DataWings. This new functionality aims at letting you register conventions as to how your domain entities map to the database tables, and in this way you can declare your assertions in a much more concise and elegant manner. The purpose is to reduce the amount of ceremony needed to execute the assertions.

Get the bits here.

An example:

The Old Way:

   1:  [Test]


   2:  public void CreatePersonInTransaction_ScopeCompleted_PersonExistsInDatabase()


   3:  {


   4:      Person person;


   5:      using (var scope = new TransactionScope())


   6:      {


   7:          person = new Person{ Id = Guid.NewGuid() };


   8:          IoC.GetInstance<IDomainObjectProvider>().Save(person);


   9:          scope.Complete();


  10:      }


  11:      // Assertion using DataWings


  12:      // The old fashioned way


  13:      DbAssert.ForTable("PERSON")


  14:          .WithColumnValuePair("ID", person.Id)


  15:          .Exists();


  16:  }






The New Way (using conventions):





   1:  [Test]


   2:  [DbTableNameConvention(DbTableNameConventionType.ClassNameEqualsTableName)]


   3:  [DbIdConvention("ID")]


   4:  public void CreatePersonInTransaction_ScopeCompleted_PersonExistsInDatabase()


   5:  {


   6:      Person person;


   7:      using (var scope = new TransactionScope())


   8:      {


   9:          person = new Person{ Id = Guid.NewGuid() };


  10:          IoC.GetInstance<IDomainObjectProvider>().Save(person);


  11:          scope.Complete();


  12:      }


  13:      // Assertion using DataWings


  14:      // The new way with conventions


  15:      DbAssert.Exists(person);


  16:  }









All conventional assertions (such as the one on line 15) are available as extension methods, so that the code on line 15 can be replaced with this code:



person.AssertExistsInDatabase();


Attribute based configuration



Notice how the assertion on lines 13 to 15 in the “old way” is replaced by just a single line (number 15) in the sample using conventions. In order to use this concise notation, DataWings will have to know how to map the object to its corresponding table, i.e. what the conventions are. These conventions are specified with the help of attributes, and in the sample above you can see how the two attributes DbTableNameConvention and DbIdConvention set up such conventions.



Such attributes may be put on methods or classes, the functionality walks the stack looking for an attribute to use. This is done by examining the method and class of each stack frame until a suitable attribute decoration is located. If no suitable attribute can be found, the conventional convention (see below) will be used.



 



The conventional conventions



You can start using the conventions functionality immediately, if you accept the conventional conventions, that is. And these conventions are:




  • Table name equals name of class


  • Name of unique key column in table is on the format Id[ClassName]



Registering conventions for mapping class to table



The convention for how the class maps to a database table is specified through the usage of DbTableNameConvention. The following example show how to use this attribute:



Class name and table name match exactly



[DbTableNameConvention(DbTableNameConventionType.ClassNameEqualsTableName)]


Class name as part of the table name



[DbTableNameConvention(DbTableNameConventionType.Custom, Convention = "TBL_{0}")]


Overriding for specific classes



The attribute also has a property EntityType, and this is used in cases where the conventions for a specific class do not match the conventions of the other entities that are in play in the test.



[DbTableNameConvention(DbTableNameConventionType.Custom, Convention = "TBL_{0}", EntityType = typeof(Address))]

[DbTableNameConvention(DbTableNameConventionType.ClassNameEqualsTableName, EntityType = typeof(Relation))]



Registering conventions for mapping primary key



The convention for how the primary key of the table is  mapped to a property of the class is specified by using DbIdConvention. Some examples:



Name of primary key column is identical for all tables



[DbIdConvention("ROWID")]



Class name is part of primary key column name



[DbIdConvention("Id{0}")]