More Base4 mocking...

More mocking...

The title for this entry should probably be "more IRepository mocking" - as that's what I'm mocking out... but at any rate, let's get down and dirty... In this entry I will cover mocking out a "complex" method, incidentally I think the size of this test suggests that our test isn't fine grained enough (and that individual features within the data loader should be exposed so we can test them better) - but that's my problem, not yours ;o)

Background

So, as some background, this is a test for a class called "DataLoader" - a most unimaginative name for a class that loads data... it's used for loading an XML file which has a bunch of info for different entities... if your recall the last entry, I was talking about simplistic music information, well this class can be used to load musical data into the store, including something not mentioned so far, which is the "test suite" - the test suite defines tests which can be run against the music in the store, the details aren't really up for discussion, but it's for executing customer acceptance tests, not unit tests.

The test below checks one of the primary paths, where by no pre-existing data is available, so it must all be created by the data loader first, before creating the customer acceptance test, which references the track, release etc. data (the definition for customer acceptance test suites are also stored in the store, as are the results from run the customer acceptance tests).

Diving In...

So here is the test for loading a "test suite" for a single track, the song "Dirty Harry" by the "Gorillaz", we are testing the behavior of loading the file when the track, release and artist do not already exist in the store - most of this is basic RhinoMock's usage (though I'm no expert, I'm probably misusing dome of the features...).

[Test]
public void LoadDataFreshForSingleTrack()
{
/**** RECORD ****/

Environment environment = new Environment();
SourceDevice sourceDevice = new SourceDevice();
RecordingDevice recordingDevice = new RecordingDevice();
Track track = new Track();

ObjectPath artistPath = (Artist.Fields.Name == "Gorillaz");
Expect.Call(_artistRepository.FindOne(artistPath)).Constraints(Base4Query.PathEqual(artistPath)).Return(null);

_artistRepository.Save(null);
LastCall.Constraints(Property.Value("Name", "Gorillaz"));

ObjectPath releasePath = (Release.Fields.Name == "Demon Days");
Expect.Call(_releaseRepository.FindOne(releasePath)).Constraints(Base4Query.PathEqual(releasePath)).Return(null);

_releaseRepository.Save(null);
LastCall.Constraints(Property.Value("Name", "Demon Days") &&
Property.ValueConstraint("Artist", Property.Value("Name", "Gorillaz")));

_trackRepository.Save(null);
LastCall.Constraints(Property.Value("Name", "Dirty Harry") &&
Property.ValueConstraint("Release", Property.Value("Name", "Demon Days")));

ObjectPath encodingPath = (TrackContentEncoding.Fields.Name == "MP3");
Expect.Call(_encodingRepository.FindOne(encodingPath)).Constraints(Base4Query.PathEqual(encodingPath)).
Return(null);

_trackContentRepository.Save(null);
LastCall.Constraints(Property.ValueConstraint("Track", Property.Value("Name", "Dirty Harry")));

ObjectPath trackReferencePath = (Track.Fields.Name == "Dirty Harry" &&
Track.Fields.Release.Name == "Demon Days" &&
Track.Fields.Release.Artist.Name == "Gorillaz");

Expect.Call(_trackRepository.FindOne(trackReferencePath)).Constraints(
Base4Query.PathEqual(trackReferencePath)).Return(track);

Expect.Call(_trackRepository.FindOne(trackReferencePath)).Constraints(
Base4Query.PathEqual(trackReferencePath)).Return(track);

ObjectPath environmentPath = (Environment.Fields.Name == "Indoors");
Expect.Call(_environmentRepository.FindOne(environmentPath)).Constraints(
Base4Query.PathEqual(environmentPath)).Return(environment);

ObjectPath sourceDevicePath = (SourceDevice.Fields.Name == "Loopback");
Expect.Call(_sourceDeviceRepository.FindOne(sourceDevicePath)).Constraints(
Base4Query.PathEqual(sourceDevicePath)).Return(sourceDevice);

ObjectPath recordingDevicePath = (SourceDevice.Fields.Name == "Loopback");
Expect.Call(_recordingDeviceRepository.FindOne(recordingDevicePath)).Constraints(
Base4Query.PathEqual(recordingDevicePath)).Return(recordingDevice);

_testSuiteRepository.Save(null);
LastCall.Constraints(Property.Value("Name", "One track, one loopback sample") &&
Property.Value("Description",
"Testing with 30 second snippets pulled from the source files with audacity (Loopback)") &&
Property.ValueConstraint("Tracks",
ItemListOf.NumberOfItems(1) && ItemListOf.IndexedItemConstraint(0, Is.Same(track))));

/***** REPLAY *****/

_mockRepository.ReplayAll();

DataLoader loader = CreateLoader();

loader.LoadData(OneTrackFile, ContentPath);

_mockRepository.VerifyAll();
}

First you'll probably see that the callback and anonymous delegates from the last post are absent, they've been replaced with constraints... which has made the syntax a lot more concise - so far the rhino mocks support is pretty basic, we have two static classes being used for creating the constraints:

  • Base4Query - for constraints on ObjectQuery and ObjectPath arguments/properties.
  • ItemListOf - for constraints on IItemList arguments/properties.

The constraints method in RhinoMocks expects the same number of arguments as the associated method call on the mock object, constraints work on the arguments value, however you can apply constraints to the value of single property of an argument, and those constraints can be logically AND or OR'd together - for instance here:

_testSuiteRepository.Save(null);
LastCall.Constraints(Property.Value("Name", "One track, one loopback sample") &&
Property.Value("Description",
"Testing with 30 second snippets pulled from the source files with audacity (Loopback)") &&
Property.ValueConstraint("Tracks",
ItemListOf.NumberOfItems(1) && ItemListOf.IndexedItemConstraint(0, Is.Same(track))));

We are applying these constraints:

  • The first argument should:
    • Have a property called "Name", and it's value should be "One track, one loopback sample".
    • And have a property called "Description", and it's value should be "Testing...." etc.
    • And have a property called "Tracks", which is of type IItemList which:
      • Contains a total of 1 items
      • And the item at index 0 should:
        • Be the same as the instance returned from an earlier save call.







Notice that we have to call the method we want to apply constraints to first, so that we can address it with LastCall - a necessary evil when the method has a return type of void... otherwise you can use the more pleasant "Expect.Call(...)" convention.

Also, I'm declaring instances of the expected Base4 object paths, and then applying them in the constraints... I could in-line the object paths, but I then need to disambiguate the overloaded call to the FindOne method, so we have my preference:

ObjectPath trackReferencePath = (Track.Fields.Name == "Dirty Harry" &&
Track.Fields.Release.Name == "Demon Days" &&
Track.Fields.Release.Artist.Name == "Gorillaz");

Expect.Call(_trackRepository.FindOne(trackReferencePath)).Constraints(
Base4Query.PathEqual(trackReferencePath)).Return(track);

or this:

Expect.Call(_trackRepository.FindOne((ObjectPath)null)).Constraints(
Base4Query.PathEqual(Track.Fields.Name == "Dirty Harry" &&
Track.Fields.Release.Name == "Demon Days" &&
Track.Fields.Release.Artist.Name == "Gorillaz")).Return(track);

The second involves less code, but I think it's easy to get confused between the compile time query language's logical operators and those of the RhinoMock's constraints... each have merits of course.

Conclusion...

I'm still working through some of the finer points of the implementation (and so won't publish any code just yet...) but this should give you some ideas about how you can test base4 interactions in your application with RhinoMocks, instead of hitting the base4 server and testing the results... and I think it's a lot more intuitive when attempting to do TDD with base4...

I would also like to make a special mention that this code won't actually work with the latest published release of Base4... there is an issue preventing the addition of items to an IItemList in some situations when you have not set a valid default context - there is a fix (Which works well) in the Base4 trunk, and I assume Alex James will release this at some point... and thanks for resolving that issue so quickly Alex :) much appreciated.

Written on November 14, 2006