Using the Fluent NHibernate Persistence Tester and the Ghostbusters Test

Exclusive offer: get 50% off this eBook here
NHibernate 3.0 Cookbook

NHibernate 3.0 Cookbook — Save 50%

Get solutions to common NHibernate problems to develop high-quality performance-critical data access applications

€20.99    €10.50
by Jason Dentler | October 2010 | .NET Cookbooks Open Source

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

 

NHibernate 3.0 Cookbook

NHibernate 3.0 Cookbook

Get solutions to common NHibernate problems to develop high-quality performance-critical data access applications

  • Master the full range of NHibernate features
  • Reduce hours of application development time and get better application architecture and performance
  • Create, maintain, and update your database structure automatically with the help of NHibernate
  • Written and tested for NHibernate 3.0 with input from the development team distilled in to easily accessible concepts and examples
  • Part of Packt's Cookbook series: each recipe is a carefully organized sequence of instructions to complete the task as efficiently as possible
        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...

  1. Add a reference to FluentNHibernate.
  2. In PersistenceTests.cs, add the following using statement:

    using FluentNHibernate.Testing;

  3. 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();
    }

  4. 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:

  1. Create a new instance of the entity (Product, ActorRole, Movie) using the values provided.
  2. Save the entity to the database.
  3. Get the entity from the database.
  4. 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

 

NHibernate 3.0 Cookbook Get solutions to common NHibernate problems to develop high-quality performance-critical data access applications
Published: October 2010
eBook Price: €20.99
Book Price: €34.99
See more
Select your format and quantity:
        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...

  1. 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;
    }

  2. 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);
    }
    }

  3. 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();
    }

  4. 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 Cookbook Get solutions to common NHibernate problems to develop high-quality performance-critical data access applications
Published: October 2010
eBook Price: €20.99
Book Price: €34.99
See more
Select your format and quantity:

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


NHibernate 2 Beginner's Guide
NHibernate 2 Beginner's Guide

WCF 4.0 Multi-tier Services Development with LINQ to Entities
WCF 4.0 Multi-tier Services Development with LINQ to Entities

ASP.NET 3.5 Application Architecture and Design
ASP.NET 3.5 Application Architecture and Design

Microsoft Windows Communication Foundation 4.0 Cookbook for Developing SOA Applications
Microsoft Windows Communication Foundation 4.0 Cookbook for Developing SOA Applications

.NET Compact Framework 3.5 Data Driven Applications
.NET Compact Framework 3.5 Data Driven Applications

Microsoft Azure: Enterprise Application Development
Microsoft Azure: Enterprise Application Development

Applied Architecture Patterns on the Microsoft Platform
Applied Architecture Patterns on the Microsoft Platform

Microsoft Windows Workflow Foundation 4.0 Cookbook
Microsoft Windows Workflow Foundation 4.0 Cookbook


Code Download and Errata
Packt Anytime, Anywhere
Register Books
Print Upgrades
eBook Downloads
Video Support
Contact Us
Awards Voting Nominations Previous Winners
Judges Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software
Resources
Open Source CMS Hall Of Fame CMS Most Promising Open Source Project Open Source E-Commerce Applications Open Source JavaScript Library Open Source Graphics Software