Logo

NHibernate

The object-relational mapper for .NET

The best way to solve NHibernate bugs? Submit good unit test

There's no better way to explain some code-related issue than providing a test for them, and that is what any NH team member is going to ask you no matter how clearly you described it; that said.. why not being smart enough to provide it since the beginning?

For those who doesn't know what a unit test is, or why could possibly be useful, a unit test is nothing more than a method with some code in it to test if a feature works as expected or, in your case, to reproduce a bug. What makes them so useful is the ability to automatically execute them all; if you hypothetically had a set of test for every feature you coded into the software you're designing, after every change you could test if everything is still working or something got broken. If that triggered your attention, you can read further information on Unit Tests and Test Driven Development here and here, while here you can download and get some info on NUnit, which is the testing framework NHibernate team is currently using; obviously you can google around a bit for more info on this topic, as I'm going to focus on how testing applies to NHibernate bug fixing process.

Ok, back on topic then. If you dared to download NHibernate sources from SourceForge or GitHub, you'd find a well established test project with some base classes you should use to implement the test. BTW, for the sake of simplicity, I created a C# project extracting only the few classes you need to build a test, so you don't need to use the actual NH sources anymore. You can download it here.

The project has the following structure, which is very similar to the one you'd find in the official sources:

image

I've mantained classes and namespaces naming in order to let your test compile in the actual NH test project without (hopefully) changing anything on it. Next steps will be

  • Building a small domain model along with its mappings
  • Setting up the test class
  • Create the test itself
  • Run the test to verify that it reproduces the bug
  • If it does, open a GitHub issue with the test (or even better, a PR on its NHibernate.Test project)

Please note that all the code should be located in a NHSpecificTest folder's subfolder named like the GitHub issue or PR you submitted (for example, GH1234). So, once you've created the issue or PR, you should do a little refactoring work to modify your test's folder and namespaces.

Domain Model and mappings

It would be hard to test an ORM without a domain model, so a simple one is mandatory, along with its mappings. My advice here is to keep things as simple as possible: your main aim should be trying to isolate the bug without introducing unnecessary complexity.

For example, if you find out that NHibernate isn't working fine retrieving a byte[] property when using a Sql Server 2005 RDBMS (it isn't true, NHibernate can deal quite well with such kind of data), you should create a domain entity not so different from the following:

   1: namespace NHibernate.Test.NHSpecificTest.NH1234
   2: {
   3:     public class DomainClass
   4:     {
   5:         private byte[] byteData;
   6:         private int id;
   7:  
   8:         public int Id
   9:         {
  10:             get { return id; }
  11:             set { id = value; }
  12:         }
  13:  
  14:         public byte[] ByteData
  15:         {
  16:             get { return byteData; }
  17:             set { byteData = value; }
  18:         }
  19:     }
  20: }

Mappings of such a simple domain model should be quite easy and small sized; the standard way to proceed is creating a single mapping file, named Mappings.hbm.xml, containing mapping definitions of all your domain model.

   1: <hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernate.Test"
   2:                    namespace="NHibernate.Test.NHSpecificTest.NH1234" default-access="field.camelcase"
   3:                    default-lazy="false">
   4:   <class name="DomainClass">
   5:     <id name="Id">
   6:       <generator class="assigned" />
   7:     </id>
   8:     <property name="ByteData" />
   9:   </class>
  10: </hibernate-mapping>

As we're going to see shortly, test base class, in his default behavior, looks for such a file in test assembly's resources and automatically adds it to NHibernate configuration.

Setting up the test class

A test class is nothing more than a class decorated with the TestFixture attribute and which inherits (in our test environment) from BugTestCase base class. If you remember, we are going to test a fake bug about NHibernate incorrectly retrieving a byte[] property from the database. That means that first of all we're going to need a database with a suitable schema and then an entity already stored in it.

The default connection string points to a localhost server containing a test database called NHibernate; if your environment fits well with that, you have nothing to change in the app.config file. The test base class takes care of creating all the tables, keys and constraints that your test needs to play fine, taking into account what you wrote on the mapping file(s).

What about the entity we should already have into a table to test the retrieving process? The right way to inject custom code before the execution of a test is overriding the OnSetup method:

   1: protected override void OnSetUp()
   2: {
   3:     base.OnSetUp();
   4:     using (ISession session = this.OpenSession())
   5:     {
   6:         DomainClass entity = new DomainClass();
   7:         entity.Id = 1;
   8:         entity.ByteData = new byte[] {1, 2, 3};
   9:         session.Save(entity);
  10:         session.Flush();
  11:     }
  12: }

That code is invoked once per test, just before its execution; the infrastructure provides a OnTearDown virtual method as well, useful to wipe away your data from the tables and present a clean environment to the next test being executed:

   1: protected override void OnTearDown()
   2: {
   3:     base.OnTearDown();
   4:     using (ISession session = this.OpenSession())
   5:     {
   6:         string hql = "from System.Object";
   7:         session.Delete(hql);
   8:         session.Flush();
   9:     }
  10: }

The test class is almost setted up, we only need one last element: our imaginary bug happens only when dealing with a Sql Server 2005 database, things seem to be fine with other RDBMS. That means that the corresponding test only makes sense when that dialect has been selected, otherwise it should be ignored; another virtual method, called AppliesTo, serves for this purpose and can be overridden to specify a particular dialect for which the test makes sense:

   1: protected override bool AppliesTo(NHibernate.Dialect.Dialect dialect)
   2: {
   3:     return dialect as MsSql2005Dialect != null;
   4: }

Coding the test

The test method is were you'll describe how NHibernate should behave although it actually doesn't. It's a traditional C# method, that usually ends with one or more Asserts, by which you verify whether things went as you expected. Our "fake" bug was "In Sql Server 2005, NHibernate can't correctly load a byte array property of an entity"; a good test for that could be something like

   1: [Test]
   2: public void BytePropertyShouldBeRetrievedCorrectly()
   3: {
   4:     using (ISession session = this.OpenSession())
   5:     {
   6:         DomainClass entity = session.Get<DomainClass>(1);
   7:  
   8:         Assert.AreEqual(3, entity.ByteData.Length);
   9:         Assert.AreEqual(1, entity.ByteData[0]);
  10:         Assert.AreEqual(2, entity.ByteData[1]);
  11:         Assert.AreEqual(3, entity.ByteData[2]);
  12:     }
  13: }

Your test class may contain as many test methods as you need to better show the cases in which you experienced the issue. If it relies on NHibernate 2nd level cache, you can turn it on and off simply overriding the CacheConcurrencyStrategy propery in your test class:

   1: protected override string CacheConcurrencyStrategy
   2: {
   3:     get { return "nonstrict-read-write"; }
   4: }

Please remember that in the simple test project I provided, 2nd level cache is disabled by default. However, NHibernate official test project uses nonstrict-read-write caching strategy for every entity, because every "green" test should pass with caching enabled as well.

Conclusions

When NHibernate doesn't work as expected, the best way to describe the issue is providing a good unit test. NHibernate.LiteTest helps you writing tests that are so similar to the official ones to be directly integrable in the actual NHibernate trunk. So, if you think you've just discovered a bug,

  1. Go to NHibernate GitHub issues and use its search engine to look for a similar bug (perhaps your problem has already been fixed and you only need to wait for a new release)
  2. If you don't find anything, write a unit test to reproduce it
  3. Execute NUnit and test it
  4. if NUnit bar is red (and NHibernate documentation doesn't state that it's a not supported case), go to GitHub NHibernate issues, open a issue, rename your test folder and namespaces with the actual issue number (ex. GH1234) and upload it as an attachment.

Obviously, if you think you're good enough, no one will be offended if you submit a patch, too.


Posted Fri, 03 October 2008 09:04:00 PM by Crad
Filed under: HowTo

comments powered by Disqus
© NHibernate Community 2024