Integration testing demonstrated (a data access testing with NHibernate)

 

In this post, I'll talk about and demonstrate integration testing. If you are just starting out with integration testing, you want to test small before you test big. For instance, full-system tests are good, but if they fail, they don't give much of a hint as to where the failure is. Smaller integration tests will help narrow the area where the failure lies.  After designing a vertical slice of the application with my team, I like to test-drive the code (that is micro-test the code) into existence.  Then, each scenario gets a covering integration test to prove that all the pieces fit together.

A few rules to live by

· An integration test must be isolated in setup and teardown. If it requires some data to be in a database, it must put it there. Environmental variables should not cause the test to fail randomly.

· It must also run fast. If it is slow, build time will suffer, and you will run fewer builds - leading to other problems.

· Integration tests should be order-independent. It should not matter the order you run them. They should all pass.

· Feel free to make up rules that objectively result in fewer defects.

Testing a repository class

Below, you'll see an integration test for ConferenceRepository.cs.  This code is in CodeCampServer, so you have full access to the whole system if that helps you understand what's going on.

    [Test]

    public void GetByKey()

    {

      Conference theConference = CreateConference("Frank", "some name");

      Conference conference2 = CreateConference("Frank2", "some name2");

      using (ISession session = getSession())

      {

        session.SaveOrUpdate(theConference);

        session.SaveOrUpdate(conference2);

        session.Flush();

      }

 

      IConferenceRepository repository = new ConferenceRepository(_sessionBuilder);

      Conference conferenceSaved = repository.GetConferenceByKey("Frank");

 

      Assert.That(conferenceSaved, Is.Not.Null);

      Assert.That(conferenceSaved, Is.EqualTo(theConference));

      Assert.That(theConference.Key, Is.EqualTo("Frank"));

      Assert.That(theConference.Name, Is.EqualTo("some name"));

    }

You'll probably want to pull down the entire source tree using TortoiseSVN using the Subversion url:  https://codecampserver.googlecode.com/svn/trunk

Just for context, this test is testing an implementation of the following interface:

  public interface IConferenceRepository

  {

    Conference[] GetAllConferences();

    Conference GetConferenceByKey(string key);

    Conference GetFirstConferenceAfterDate(DateTime date);

    Conference GetMostRecentConference(DateTime date);

    Conference GetById(Guid id);

    void Save(Conference conference);

    bool ConferenceExists(string name, string key);

    bool ConferenceKeyAvailable(string key);

  }

Every test needs to set up its own state, so in this test, we see that the beginning of the test is using the application's data access layer to save two Conference objects to the database.  CodeCampServer using NHibernate, so the test will use the same when setting up the database for the test.  If you examine the source, you will notice that the base class for all the NUnit test fixtures runs a command that clears out every table in the local database.  This is important because each test needs a known starting point, and the easiest starting point is an empty database.  Note that we're talking about the local developer's database, which should be created and updated by the local build.

After the database has two records, our class under test runs the GetConferenceByKey() method and returns a Conference.  Our assert statements can then verify the code did the right thing.  This test goes all the way through the data access layer and to the database.  If anything was awry along the way, the test would fail.

 

My hope is that this brief example will fill in some gaps that may exist in your understanding of integration testing.  Even though I'm doing integration testing and not unit testing, I'm keeping the scope of the test small because the larger the scope of the test, the harder it is to pinpoint the cause of any failure.

 

My feed:  http://feeds.jeffreypalermo.com/jeffreypalermo

Comments

Seth Petry-Johnson said on 7.11.2008 at 11:49 AM

Out of curiosity, how do you scale these techniques to much larger and more complicated databases? In my own integration testing experiences, large DBs introduce two major pain points:

1) When Foo contains many foreign key relationships. If you're starting from a blank DB, entries in these supporting tables need to be established before you can create any Foo instances. All of this creation logic can clutter up the test code.

2) When a "blank" database does not represent a "ready state" for the application. For instance, my DB hosts multiple Companies, and setting up a Company isn't trivial. Since Foo instances are tied to a parent Company, this setup has to happen before I can test Foo creation.

What I've done thus far is set up a testing DB in a NON-EMPTY, but well-defined known state. This DB contains a handful of Companies [one for each application area or configuration option that has integration tests] and other entities. The DB is restored from a backup file [in source control] during test initialization. The base test class contains constants for important values like pre-existing Company IDs, user IDs, etc.

The benefit of this approach is that individual tests are spared a ton of repetitive setup code, and I don't have to script out the initial configuration steps [which would be painful].

The drawback is that maintaining this test DB is a huge time sink, and there's a learning curve to understanding the pre-defined state of the DB.

Is there a better way?

jpalermo said on 7.11.2008 at 11:52 AM

@Seth,

I invite you to check out CodeCampServer (codecampserver.org). Pull down all the source code, run the build. You will probably better understand through the build and the test code than a comment on this blog. Each test clears out the database and sets up everything required for a test to run.

There is a class called ZDataLoader that runs and puts enough data in the database to run the application locally. This handles the problem of necessary data required for the application to even run.

Tapio Kulmala said on 7.15.2008 at 4:18 AM

@Seth

Stephen Bohlen has a nice way to initialize the db for integration tests. He uses NDBUnit. Check this screencast

www.summerofnhibernate.com/.../Summer%20of%20N

I'm not sure, how this will scale to larger or more complicated databases. There might be also some performance issues.

Tapio