NHibernate Forge
The official new home for the NHibernate for .NET community

How Test your mappings: the Ghostbuster

In NHibernate, when you have the FlushMode configured to AutoFlush, session.Flush() is called when NH detects a dirty entity instance and when a query with an intersected QuerySpace is performed. (The QuerySpace is represented by all tables affected in a query.)

Example:

  <class name="Animal">
<
id name="Id">
<
generator class="hilo"/>
</
id>
<
property name="Description"/>

<
joined-subclass name="Reptile">
<
key column="animalId"/>
<
property name="BodyTemperature"/>
</
joined-subclass>

</
class>

In the above domain, a query on the Reptile class in a opened session with a dirty instance of Animal would cause session.Flush() will be called.

After a session.Get<Animal>(animalId) we can be pretty sure that there is no dirty entities in the session, sure ?

Don’t be so sure! The real answer is: It depends.

For example try this domain:

public enum Sex
{
Unspecified,
Male,
Female
}
public class Person
{
public virtual int Id { get; set; }
public virtual Sex Sex { get; set; }
}

with this mapping:

  <class name="Person">
<
id name="Id">
<
generator class="hilo"/>
</
id>
<
property name="Sex" type="int"/>
</
class>

In the mapping I define the property Sex of type int but in the class the type is Sex; even if you don’t receive an exception, because an int is convertible to Sex and viceversa, your persistence will have a unexpected  behavior. NH will detect a modification, of your entity, “immediately” after session.Get because it having an int in the entity snap-shot (retrieved from DB) and a Sex in the actual state. The example are showing a very simple case of “ghosts” in your application. In a big environment, with a complex domain, find “ghosts” it is not so easy.

The Ghostbusters

[TestFixtureSetUp]
public void TestFixtureSetUp()
{
XmlConfigurator.Configure();
cfg = new Configuration();
cfg.Configure();
new SchemaExport(cfg).Create(false, true);
sessions = (ISessionFactoryImplementor) cfg.BuildSessionFactory();
PopulateDb();
}

Few words about the TestFixtureSetUp:


  • if you are testing your domain persistence you can run the “ghostbuster” in each test.
  • if you are testing yours DAOs and you have an implementation of ObjectMother or TestDataBuilder you can use it in the implementation of PopulateDb() method.
  • If you don’t have tests you can leave the PopulateDb() method empty and configure NH to an existing copy of your DB.
[Test, Explicit]
public void UnexpectedUpdateDeleteOnFetch()
{
PersistingMappings(null);
}

[Test, Explicit]
public void UnexpectedUpdateDeleteOnFetchSpecific()
{
var entitiesFilter = new[]
{
"Person"
};
PersistingMappings(entitiesFilter);
}

In my experience the above two tests are needed. The first sound like “close your eyes and pray” the second allow you to analyze some specific entities.

To avoid breaking the test, on each unexpected DB-hit, I’ll use the power of log4net in the whole fixture.

To intercept unexpected Flush a possible, easy and quickly, way is an implementation of IInterceptor.

private class NoUpdateInterceptor : EmptyInterceptor
{
private readonly IList<string> invalidUpdates;

public NoUpdateInterceptor(IList<string> invalidUpdates)
{
this.invalidUpdates = invalidUpdates;
}

public override bool OnFlushDirty(object entity, object id, object[] currentState, object[] previousState, string[] propertyNames, IType[] types)
{
string msg = " FlushDirty :" + entity.GetType().FullName;
log.Debug(msg);
invalidUpdates.Add(msg);
return false;
}

public override bool OnSave(object entity, object id, object[] state, string[] propertyNames, IType[] types)
{
string msg = " Save :" + entity.GetType().FullName;
log.Debug(msg);
invalidUpdates.Add(msg);
return false;
}

public override void OnDelete(object entity, object id, object[] state, string[] propertyNames, IType[] types)
{
string msg = " Delete :" + entity.GetType().FullName;
log.Debug(msg);
invalidUpdates.Add(msg);
}
}

As you can see I’m interested in : unexpected Flush of dirty instance, unexpected Saves and unexpected Deletes.

The PersistingMappings is my “driver” to test each entity. The responsibility of the method is iterate each persistent class known by the SessionFactory (or the selected in UnexpectedUpdateDeleteOnFetchSpecific methods), run the test of each entity and reports all issues found.

private void PersistingMappings(ICollection<string> entitiesFilter)
{
var invalidUpdates = new List<string>();
var nop = new NoUpdateInterceptor(invalidUpdates);

IEnumerable<string> entitiesToCheck;
if (entitiesFilter == null)
{
entitiesToCheck = cfg.ClassMappings.Select(x => x.EntityName);
}
else
{
entitiesToCheck = from persistentClass in cfg.ClassMappings
where entitiesFilter.Contains(persistentClass.EntityName)
select persistentClass.EntityName;
}

foreach (var entityName in entitiesToCheck)
{
EntityPersistenceTest(invalidUpdates, entityName, nop);
}

if (invalidUpdates.Count > 0)
{
if (logError.IsDebugEnabled)
{
logError.Debug(" ");
logError.Debug("------ INVALID UPDATES -------");
invalidUpdates.ForEach(x => logError.Debug(x));
logError.Debug("------------------------------");
}
}
Assert.AreEqual(0, invalidUpdates.Count, "Has unexpected updates.");
}

To check each persistent entity I’m using the Configuration.ClassMappings collection and extracting the EntityName from the PersistentClass. The use of EntityName don’t mean that I’m using the tag entity-name (as you can see in the mapping above).

The real “ghostbuster” is:

private void EntityPersistenceTest(ICollection<string> invalidUpdates,
string entityName, IInterceptor nop)
{
const string queryTemplate = "select e.{0} from {1} e";
string msg = "s--------" + entityName;
log.Debug(msg);

using (var s = sessions.OpenSession(nop))
using (var tx = s.BeginTransaction())
{
IList entityIds = null;
try
{
string queryString = string.Format(queryTemplate, DefaultIdName, entityName);
entityIds = s.CreateQuery(queryString).SetMaxResults(1).List();
}
catch (Exception e)
{
log.Debug("Possible METEORITE:" + e.Message);
}

if (entityIds != null)
{
if (entityIds.Count == 0 || entityIds[0] == null)
{
log.Debug("No instances");
}
else
{
if (entityIds.Count > 1)
{
msg = ">Has " + entityIds.Count + " subclasses";
log.Debug(msg);
}
object entityId = entityIds[0];
try
{
s.Get(entityName, entityId);
try
{
s.Flush();
}
catch (Exception ex)
{
string emsg = string.Format("EXCEPTION - Flushing entity [#{0}]: {1}", entityId, ex.Message);
log.Debug(emsg);
invalidUpdates.Add(emsg);
}
}
catch (Exception ex)
{
string emsg = string.Format("EXCEPTION - Getting [#{0}]: {1}", entityId, ex.Message);
invalidUpdates.Add(emsg);
log.Debug(emsg);
}
}
tx.Rollback();
}
}
msg = "e--------" + entityName;
log.Debug(msg);
}

The core of the test is:

s.Get(entityName, entityId);
s.Flush();

If I Get an entity, from a clear fresh session, without touch the state what I’m expect is that the follow Flush don’t  make absolutely nothing but… you know… perhaps there is an ugly “ghost”. Each try-catch are checking some special situation.

And now lets go to run the “ghostbuster” in your application. Code available here.


Posted oct 20 2008, 03:48 p.m. by Fabio Maulo
Filed under: , ,

Comments

Stefan Steinegger wrote re: How Test your mappings: the Ghostbuster
on 11-14-2008 9:40

Interesting approach. I also wrote a MappingTester, that works quite different. Probably because our project is different. We generate the tables from the mapping files. We have to test if the mapping matches to the entities.

I write a test for each class graph ("tree"). The tester takes the type of the "root" object. Then it

 - creates an instance of the type, populating all properties using reflection.

 - stores the entity (and some referenced entities that are configured to be not cascading)

 - reloads the entity in a new transaction

 - compares all properties using reflection.

 - Assures that a given list of types has been tested

In most cases, there is little to write, like "ignore this property" and cascading issues.

Example (pseudo-code)

tester = new MappingTester(typeof(Person))

tester.Configure<Person>()

 .NonCascading("Employer")

 .DontGenerate("AnnualWage") // calculated from Salary

 .DontTest("State"); // transient

tester.Configure<Address>()

 .NonCascading("City");

tester.test();

tester.AssertTypesTested(

 typeof(Person),

 typeof(Address),

 typeof(City),

 typeof(Company));

Finds errors like:

 - Mapping-parsing errors

 - create schema errors (rare)

 - When storing: many, many possible mistakes, missing mapping file (simple, but common), type mismatches and so on

 - When comparing: a list of properties that are not equal after persisting

It's pretty easy. The tests are set up within minutes, I write them before I write the mapping file and use the test result as "task-list". When it runs, I'm pretty sure that everything is alright.

Because of the excluding nature of the configuration, new properties in the entities let the test break, until they are either mapped or excluded (using DontTest which needs a good reason).

The configurations could be held on a central place, because they are not test-specific, but entity-specific (If the same entity is reference from another test, you need the same configuration for it)

The most complex part of the tester is the TestDataGenerator, that creates fully populated instances of object graphs, which saves one of writing hundreds of lines of code. It could be used independent from the mapping test.

When I enhance it, I would implement that the same property is tested with two different values, which is important for booleans (until now, only true is tested).

Another enhancment would be testing the deletion (and cascades like with update). But in my experience, a high percentage of common mistakes is already covered.

Fabio Maulo wrote re: How Test your mappings: the Ghostbuster
on 11-14-2008 10:00

ObjectMother, and you can do it in the method PopulateDb.

What we are testing here is something else... to be sure try to change a type of an enum (the same example I'm doing here) in your tests and take a look if some test fail.

Stefan Steinegger wrote re: How Test your mappings: the Ghostbuster
on 11-15-2008 21:32

I know that you are testing something else. I don't question your approach. The "Get-Flush" ghostbuster should probably also be part of my test. (You could easily use a mock as interceptor).

I'm not sure if I understand your ObjectMother remark. You mean, that you write a method that creates objects hardcoded (instead of using a generic library or helper class)? If this is what you mean, you have to admit that you can't create a whole object graph (consisting of many classes) within four lines of code. You'll get hundreds of lines of code in your project, which is hard to maintain.

Fabio Maulo wrote re: How Test your mappings: the Ghostbuster
on 11-16-2008 17:47

Try to follow the link of ObjectMother and/or TestDataBuilder I put in the post.

Giovanni wrote re: How Test your mappings: the Ghostbuster
on 12-03-2008 9:42

Hi Fabio ;)

Tnx a lot for this peace of code!

I have some suggest:

In PersistingMappings method there are 2 problem:

1) the name of persistentClass.EntityName can contain the namespaces ...so u check something like "pippo.Contains(MySpace.Domain.Preferred.Degustibus.Pippo) ...and yep: there is no story :P

My BRUTAL suggestion:

where entitiesFilter.Contains(                  persistentClass.EntityName.Split('.')[persistentClass.EntityName.Split('.').Length - 1])

(brutal but it work!)

2) for the Id key: i don't know why, but i use a "personal" Primary Key Name for every entity. Like IdUser, IdBill etc...

So i change the code (raw mode on): (From Line 80):

if (entitiesFilter == null)

{

var entitiesToCheck = from persistentClass in _config.ClassMappings

 select new { Name = persistentClass.EntityName,

 IdName = persistentClass.IdentifierProperty.Name };

foreach (var entity in entitiesToCheck)

{

EntityPersistenceTest(invalidUpdates, entity.Name, entity.IdName, nop);

}

}

else

{

var entitiesToCheck = from persistentClass in _config.ClassMappings

                 where

                  entitiesFilter.Contains(

                  persistentClass.EntityName.Split('.')[persistentClass.EntityName.Split('.').Length - 1])

                 select new {Name = persistentClass.EntityName, IdName = persistentClass.IdentifierProperty.Name};

foreach (var entity in entitiesToCheck)

{

EntityPersistenceTest(invalidUpdates, entity.Name, entity.IdName, nop);

}

}

And obviusly the firm of EntityPersistenceTest:

private void EntityPersistenceTest(ICollection<string> invalidUpdates,

string entityName, string idName, IInterceptor nop)

And use:

string queryString = string.Format(queryTemplate, idName, entityName);

I hope this help somone ...if u understand what i wrote

Giovanni wrote re: How Test your mappings: the Ghostbuster
on 12-03-2008 10:02

AARGH My post was lost! ARGH.

Ok I retry:

First: thanks a lot to Fabio for another cool peace of code!

Second: I have some suggestions:

1) a problem with filter in PersistingMappings:

i compare something like:

pippoEntity.Contains(MyNameSpace.Domain.Land.Preferred.Walt.Disney.PippoEntity).

Somethins go wrong...

so it's my BRUTAL solution:

where

                  entitiesFilter.Contains(

                  persistentClass.EntityName.Split('.')[persistentClass.EntityName.Split('.').Length - 1])

2) the Primary Key Name problem: for reason that i don't understand, i still use "personalize" id name for my entities. Like IdUser, IdBill, IdDrugs...

so i change the call of EntityPersistenceTest:

if (entitiesFilter == null)

{

var entitiesToCheck = from persistentClass in _config.ClassMappings

 select new { Name = persistentClass.EntityName,

 IdName = persistentClass.IdentifierProperty.Name };

foreach (var entity in entitiesToCheck)

{

EntityPersistenceTest(invalidUpdates, entity.Name, entity.IdName, nop);

}

}

else

{

var entitiesToCheck = from persistentClass in _config.ClassMappings

                 where

                  entitiesFilter.Contains(

                  persistentClass.EntityName.Split('.')[persistentClass.EntityName.Split('.').Length - 1])

                 select new {Name = persistentClass.EntityName, IdName = persistentClass.IdentifierProperty.Name};

foreach (var entity in entitiesToCheck)

{

EntityPersistenceTest(invalidUpdates, entity.Name, entity.IdName, nop);

}

}

and obviusly the EntityPersistenceTest itself with the new firm:

private void EntityPersistenceTest(ICollection<string> invalidUpdates,

string entityName, string idName, IInterceptor nop)

And the line of query builder:

string queryString = string.Format(queryTemplate, idName, entityName);

3) if u change ur mapping files at Class definition with this:

<class name="MyBill" table="Bills" lazy="true" dynamic-update="true">

With Dinamyc-update="true" u can see ONLY the FIELD to adjust!

I hope this help someone :P

Powered by Community Server (Commercial Edition), by Telligent Systems