Your message has been sent.
This article has been saved to your account.
Go to my account
This article has been emailed to your Kindle.
Send this article
Complete the form below to send this article, Using the Fluent NHibernate Persistence Tester and the Ghostbusters Test, to a friend (or to yourself). We will never share your details (or your friend's) with anyone. For more information, read our Privacy Policy.
Testing is a critical step in the development of any application. This article by Jason Dentler, author of NHibernate 3.0 Cookbook, introduces some techniques you can apply to quickly test your NHibernate applications. The recipes in this article are designed to ease the testing process and expose common issues.
In this article, we will cover the following topics:
- Using the Fluent NHibernate persistence tester
- Using the Ghostbusters test
| Read more about this book |
(For more resources on NHibernate, see here.)
The reader would benefit from reading the previous article on Testing Using NHibernate Profiler and SQLite.
Using the Fluent NHibernate Persistence Tester
Mappings are a critical part of any NHibernate application. In this recipe, I'll show you how to test those mappings using Fluent NHibernate's Persistence tester.
Getting ready
Complete the Fast testing with SQLite in-Memory database recipe mentioned in the previous article.
How to do it...
- Add a reference to FluentNHibernate.
- In PersistenceTests.cs, add the following using statement:
using FluentNHibernate.Testing;
- Add the following three tests to the PersistenceTests fixture:
[Test]
public void Product_persistence_test()
{
new PersistenceSpecification<Product>(Session)
.CheckProperty(p => p.Name, "Product Name")
.CheckProperty(p => p.Description, "Product Description")
.CheckProperty(p => p.UnitPrice, 300.85M)
.VerifyTheMappings();
}
[Test]
public void ActorRole_persistence_test()
{
new PersistenceSpecification<ActorRole>(Session)
.CheckProperty(p => p.Actor, "Actor Name")
.CheckProperty(p => p.Role, "Role")
.VerifyTheMappings();
}
[Test]
public void Movie_persistence_test()
{
new PersistenceSpecification<Movie>(Session)
.CheckProperty(p => p.Name, "Movie Name")
.CheckProperty(p => p.Description, "Movie Description")
.CheckProperty(p => p.UnitPrice, 25M)
.CheckProperty(p => p.Director, "Director Name")
.CheckList(p => p.Actors, new List<ActorRole>()
{
new ActorRole() { Actor = "Actor Name", Role = "Role" }
})
.VerifyTheMappings();
} - Run these tests with NUnit.
How it works...
The Persistence tester in Fluent NHibernate can be used with any mapping method. It performs the following four steps:
- Create a new instance of the entity (Product, ActorRole, Movie) using the values provided.
- Save the entity to the database.
- Get the entity from the database.
- Verify that the fetched instance matches the original.
At a minimum, each entity type should have a simple Persistence test, such as the one shown previously. More information about the Fluent NHibernate Persistence tester can be found on their wiki at http://wiki.fluentnhibernate.org/Persistence_specification_testing
See also
- Testing with the SQLite in-memory database
- Using the Ghostbusters test
| Read more about this book |
(For more resources on NHibernate, see here.)
Using the Ghostbusters test
As part of automatic dirty checking, NHibernate compares the original state of an entity to its current state. An otherwise unchanged entity may be updated unnecessarily because a type conversion caused this comparison to fail. In this recipe, I will show you how to detect these ghost update issues with the Ghostbusters test
Getting ready
Complete the recipe Fast testing with SQLite in-memory database.
How to do it...
- Add a new class named Ghostbusters using the following code:
private static readonly ILog log =
LogManager.GetLogger(typeof(Ghostbusters));
private readonly Configuration _configuration;
private readonly ISessionFactory _sessionFactory;
private readonly Action<string> _failCallback;
private readonly Action<string> _inconclusiveCallback;
public Ghostbusters(Configuration configuration,
ISessionFactory sessionFactory,
Action<string> failCallback,
Action<string> inconclusiveCallback)
{
_configuration = configuration;
_sessionFactory = sessionFactory;
_failCallback = failCallback;
_inconclusiveCallback = inconclusiveCallback;
}
public void Test()
{
var mappedEntityNames = _configuration.ClassMappings
.Select(mapping => mapping.EntityName);
foreach (string entityName in mappedEntityNames)
Test(entityName);
}
public void Test<TEntity>()
{
Test(typeof(TEntity).FullName);
}
public void Test(string entityName)
{
object id = FindEntityId(entityName);
if (id == null)
{
var msg = string.Format(
"No instances of {0} in database.",
entityName);
_inconclusiveCallback.Invoke(msg);
return;
}
log.DebugFormat("Testing entity {0} with id {1}",
entityName, id);
Test(entityName, id);
}
public void Test(string entityName, object id)
{
var ghosts = new List<String>();
var interceptor = new GhostInterceptor(ghosts);
using (var session = _sessionFactory.OpenSession(interceptor))
using (var tx = session.BeginTransaction())
{
session.Get(entityName, id);
session.Flush();
tx.Rollback();
}
if (ghosts.Any())
_failCallback.Invoke(string.Join("\n", ghosts.ToArray()));
}
private object FindEntityId(string entityName)
{
object id;
using (var session = _sessionFactory.OpenSession())
{
var idQueryString = string.Format(
"SELECT e.id FROM {0} e",
entityName);
var idQuery = session.CreateQuery(idQueryString)
.SetMaxResults(1);
using (var tx = session.BeginTransaction())
{
id = idQuery.UniqueResult();
tx.Commit();
}
}
return id;
} - Add another class named GhostInterceptor using the following code:
private static readonly ILog log =
LogManager.GetLogger(typeof(GhostInterceptor));
private readonly IList<string> _ghosts;
private ISession _session;
public GhostInterceptor(IList<string> ghosts)
{
_ghosts = ghosts;
}
public override void SetSession(ISession session)
{
_session = session;
}
public override bool OnFlushDirty(
object entity, object id, object[] currentState,
object[] previousState, string[] propertyNames, IType[] types)
{
var msg = string.Format("Flush Dirty {0}",
entity.GetType().FullName);
log.Error(msg);
_ghosts.Add(msg);
ListDirtyProperties(entity);
return false;
}
public override bool OnSave(
object entity, object id, object[] state,
string[] propertyNames, IType[] types)
{
var msg = string.Format("Save {0}",
entity.GetType().FullName);
log.Error(msg);
_ghosts.Add(msg);
return false;
}
public override void OnDelete(
object entity, object id, object[] state,
string[] propertyNames, IType[] types)
{
var msg = string.Format("Delete {0}",
entity.GetType().FullName);
log.Error(msg);
_ghosts.Add(msg);
}
private void ListDirtyProperties(object entity)
{
string className =
NHibernateProxyHelper.GuessClass(entity).FullName;
var sessionImpl = _session.GetSessionImplementation();
var persister =
sessionImpl.Factory.GetEntityPersister(className);
var oldEntry =
sessionImpl.PersistenceContext.GetEntry(entity);
if ((oldEntry == null) && (entity is INHibernateProxy))
{
var proxy = entity as INHibernateProxy;
object obj =
sessionImpl.PersistenceContext.Unproxy(proxy);
oldEntry = sessionImpl.PersistenceContext.GetEntry(obj);
}
object[] oldState = oldEntry.LoadedState;
object[] currentState = persister.GetPropertyValues(entity,
sessionImpl.EntityMode);
int[] dirtyProperties = persister.FindDirty(currentState,
oldState, entity, sessionImpl);
foreach (int index in dirtyProperties)
{
var msg = string.Format(
"Dirty property {0}.{1} was {2}, is {3}.",
className,
persister.PropertyNames[index],
oldState[index] ?? "null",
currentState[index] ?? "null");
log.Error(msg);
_ghosts.Add(msg);
}
} - Add the following test to the PersistenceTests fixture:
[Test]
public void GhostbustersTest()
{
using (var tx = Session.BeginTransaction())
{
Session.Save(new Movie()
{
Name = "Ghostbusters",
Description = "Science Fiction Comedy",
Director = "Ivan Reitman",
UnitPrice = 7.97M,
Actors = new List<ActorRole>()
{
new ActorRole()
{
Actor = "Bill Murray",
Role = "Dr. Peter Venkman"
}
}
});
Session.Save(new Book()
{
Name = "Who You Gonna Call?",
Description = "The Real Ghostbusters comic series",
UnitPrice = 30.00M,
Author = "Dan Abnett",
ISBN = "1-84576-141-3"
});
tx.Commit();
}
new Ghostbusters(
NHConfigurator.Configuration,
NHConfigurator.SessionFactory,
new Action<string>(msg => Assert.Fail(msg)),
new Action<string>(msg => Assert.Inconclusive(msg))
).Test();
} - Run the tests with NUnit.
How it works...
The Ghostbusters test finds issues where a session's automatic dirty checking determines that an entity is dirty (has unsaved changes) when, in fact, no changes were made. This can happen for a few reasons, but it commonly occurs when a database field that allows nulls is mapped to a non-nullable property such as integer or DateTime, or when an enum property is mapped with type=int. For example, when a null value is loaded in to an integer property, the value is automatically converted to integer's default value, zero. When the session is flushed, automatic dirty checking will see that the value is no longer null and update the database value to zero. This is referred to as a ghost update.
At the heart of our Ghostbusters test, we have the GhostInterceptor. An interceptor allows an application to intercept session events before any database action occurs. This interceptor can be set globally on the NHibernate configuration or passed as a parameter to sessionFactory.OpenSession as we've done in this recipe.
When we flush a session containing a dirty entity, the interceptor's OnFlushDirty method is called.GhostInterceptor compares the current values of the dirty entity's properties to their original values and reports these back to our Ghostbusters class. Similarly, we also intercept Save and Delete events, though these are much less common.
Our Ghostbusters class coordinates the testing. For example, we can call Test(entityName,id) to test using a particular instance of an entity. If we strip this test down to its core, we end up with this:
session.Get(entityName, id);
session.Flush();
tx.Rollback();
Notice that we simply get an entity from the database and immediately flush the session. This runs automatic dirty checking on a single unchanged entity. Any database changes resulting from this Flush() are ghosts.
If we call Test(entityName) or Test<Entity>(), Ghostbusters will first query the database for an ID for the entity, then run the test. For a test on our Movie entity, this ID query would be:
SELECT e.id FROM Eg.Core.Movie e
This lowercase id property has special meaning in HQL. In HQL, lowercase id always refers to the entity's POID. In our model, it happens to be named Id, but we could have just as easily named it <Bob>.
Finally, if we simply call the Test() method, Ghostbusters will test one instance of each mapped entity. This is the method we use in our tests.
This Ghostbusters test has somewhat limited value in automated tests as we've done here. It really shines when testing migrated or updated production data.
See also
- Using the Hibernate Query Language
Summary
In this article we covered:
- Using the Fluent NHibernate persistence tester
- Using the Ghostbusters test
Further resources on this subject:
- NHibernate 3.0: Working with the Data Access Layer
- NHibernate 3.0: Using Named Queries in the Data Access Layer
- NHibernate 3.0: Using ICriteria and Paged Queries in the Data Access Layer
- NHibernate 3.0: Using LINQ Specifications in the data access layer
- NHibernate 3.0: Testing Using NHibernate Profiler and SQLite
- Creating a NHibernate session to access database within ASP.NET
- NHibernate 2: Mapping relationships and Fluent Mapping
About the Author :
Jason Dentler
Jason Dentler grew up in the small Texas town of Mission Valley. He started tinkering with computers as a kid in the late 1980s, and all these years later, he hasn't stopped. He's worked in a few different industries. Currently, he builds really awesome software in higher education. He's an Eagle Scout and a graduate of the University of Houston – Victoria.
Books From Packt
|
|
|




Post new comment