Saturday, May 01, 2010

How to write unit tests for your FluentNHibernate class maps

I've been building a spike to learn FluentNHibernate, with the constraint that each step I take, I start with a test. Since FluentNHibernate uses code rather than XML to specify the class mapping (using ClassMap), one of my goals has been to figure out how to test these code-based mappings.

The FluentNHibernate (source on github) includes extensive unit tests, one of which, PersistenceSpecificationTester, performs detailed tests using PersistenceSpecification<T> to validate class maps.

For the spike, I've tried to simplify this down as much as possible, working with a single entity, a base class (DomainEntity, to handle equality) and a static utility method for building an ISessionSource containing any entity class.

Use the following to build your class mapping test:

1) Create a domain model with two entities:
  • Product
  • DomainEntity, a common base class for handling identity/equality

Product.cs

public class Product : DomainEntity
{
    // Location is an immutable value object containing Aisle and Shelf properties;
    // included to demonstrate FluentNHibernate's mapping of an entity component
    public Location Location { get; set; }
    public decimal Price { get; set; }
    public string Name { get; set; }
}


DomainEntity.cs

public class DomainEntity
{
    public int Id { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as DomainEntity;

        return other != null 
            && other.Id > 0
            && other.Id.Equals(this.Id);
    }

    public override int GetHashCode()
    {
        return this.Id.GetHashCode();
    }
}


2) Create a mapping file using FluentNHibernate's ClassMap<T>.

ProductMap.cs

public class ProductMap : ClassMap<Product>
{
    public ProductMap()
    {
        Id(x => x.Id);
        Map(x => x.Name);
        Map(x => x.Price);

        Component(x => x.Location, m =>
        {
            m.Map(x => x.Aisle);
            m.Map(x => x.Shelf);
        });
    }
}


3) Create a static class/method that accepts any domain entity class (i.e. which extends DomainEntity) and returns a mock in-memory NHibernate session containing an instance of the entity (which we will use next to test the mappings).

FluentNHibernateMappingTester.cs

using FluentNHibernate;
using Gaddzeit.Spike.Domain.Entities;
using NHibernate;
using Rhino.Mocks;

namespace Gaddzeit.Spike.Tests.Unit
{
    public static class FluentNHibernateMappingTester
    {
        public static ISessionSource GetNHibernateSessionWithWrappedEntity<T>(T tMappedEntityWithinSession) where T : DomainEntity
        {
            var transaction = MockRepository.GenerateStub();
            var session = MockRepository.GenerateStub();
            session.Stub(s => s.BeginTransaction()).Return(transaction);
            session.Stub(s => s.Get<T>(null)).IgnoreArguments().Return(tMappedEntityWithinSession);
            session.Stub(s => s.GetIdentifier(tMappedEntityWithinSession)).Return(tMappedEntityWithinSession.Id);

            var sessionSource = MockRepository.GenerateStub();
            sessionSource.Stub(ss => ss.CreateSession()).Return(session);
            return sessionSource;
        }
    }
}


4. To test the entity mapping, you need a comparer class that implements IEqualityComparer, and can test both the equality of the entity itself, as well as the equality of any of its properties.

ProductComparer.cs

public class ProductComparer : IEqualityComparer
{
    public bool Equals(object x, object y)
    {
        if (x is Product && y is Product)
            return ((Product)x).Id == ((Product)y).Id;
        if (x is string && y is string)
            return x.ToString().Equals(y.ToString());
        if (x is decimal && y is decimal)
            return Convert.ToDecimal(x).Equals(Convert.ToDecimal(y));
        throw new EqualityComparerUnhandledComparisonException();
    }

    public int GetHashCode(object obj)
    {
        if (obj is Product) 
            return ((Product) obj).GetHashCode();
        if (obj is string) 
            return obj.ToString().GetHashCode();
        if (obj is decimal)
            return Convert.ToDecimal(obj).GetHashCode();
        throw new EqualityComparerUnhandledComparisonException();
    }
}


5. Finally: create a test for the mapping of Product, which calls to the static method to setup the Product instance in the mocked in-memory NHibernate Session (returned as ISessionSource), and then passes ISessionSource and the above ProductMapper into PersistenceSpecification in order to perform validation on any mapping specified. In the test below, we test only one of the property mappings, Product.Name by calling the PersistenceSpecification.CheckProperty() method.

ProductMappingTests.cs

using FluentNHibernate.Testing;
using Gaddzeit.Spike.Domain.DomainServices;
using Gaddzeit.Spike.Domain.Entities;
using NUnit.Framework;

namespace Gaddzeit.Spike.Tests.Unit
{
    [TestFixture]
    public class ProductMappingTests
    {
        [Test]
        public void NameProperty_IsMapped_Correctly()
        {
            var product = new Product
            {
                Id = 35,
                Location = new Location(35, 27),
                Name = "Hammer",
                Price = 35.99M
            };

            var identicalProduct = new Product
            {
                Id = 35,
                Location = new Location(35, 27),
                Name = "Hammer",
                Price = 35.99M
            };
            var sessionSource = FluentNHibernateMappingTester.GetNHibernateSessionWithWrappedEntity(identicalProduct);
            var spec = new PersistenceSpecification(sessionSource, new ProductComparer());
            spec.CheckProperty(p => p.Name, product.Name).VerifyTheMappings();
        }

    }
}


That's it! Your test should pass, and you have verified that your entity is mapped correctly, without setting up a real connection to a database.

No comments: