2008-10-08

Implementing complex unit testing with IoC

I have a method that looks something like this

public void DoSomething(SomeClass someInstance, User user)
{
  var persistence = someInstance.AsIObject.GetEcoService<IPersistenceService>();
  persistence.Unload(someInstance.AsIObject());
  
  if (someInstance.CurrentUser != null)
    throw new ..........;
  someInstance.CurrentUser = user;

  persistence.UpdateDatabase(someInstance);
}


This unloads the local cache before ensuring someInstance.CurrentUser == null, it then sets someInstance.CurrentUser and updates the DB. The unit test I wanted would check what happens when two users try to perform this at the same time. What I wanted was

User A: Unload
User B: Unload
User A: Check == null, it is
User B: Check == null, it is
User A: Change + update DB
User B: Change + update DB + experience a lock exception

What I didn't want was

User A: Unload
User B: Unload
User A: Check == null, it is
User A: Change + update DB
User B: Check == null, it isn't

To achieve two things running at once I need to either

A: Execute a line of code at a time for each user within the unit test instead of executing the method.
B: Execute the same method from two different threads.

Option A is easy to follow but is rubbish because it involves copying the method source out into the test method, no thanks! Option B is good but harder because I need to ensure that the two threads execute the lines of code in sync. What I really could do with is sync code inside the method, but there is no way I want to add additional sync logic because it is only needed for testing! However, there is already an opportunity to inject some code into the method; take a look here

  var persistence = someInstance.AsIObject.GetEcoService<IPersistenceService>();
  ...
  persistence.UpdateDatabase(someInstance);


Because we are using Inversion of Control it means that the method will call UpdateDatabase() on the reference we pass rather than a hard-coded reference. This means that we could quite easily replace the IPersistenceService with our own implementor and intercept the call to UpdateDatabase.

  public class PersistenceServiceWithEvents : IPersistenceService
  {
    //An event to call back
    public event EventHandler BeforeUpdateDatabase;

    //A reference to the original persistence service
    private readonly IPersistenceService PersistenceService;

    //Constructor
    public PersistenceServiceWithEvents(IEcoServiceProvider serviceProvider)
    {
      PersistenceService =
        serviceProvider.GetEcoService<IPersistenceService>();
    }

    //Interface implementation
    public void UpdateDatabase(IObjectProvider obj)
    {
      EventHandler handler = BeforeUpdateDatabase;
      if (handler != null)
        handler(this, EventArgs.Empty);
      
    }

    //Other methods omitted
  }


Now I have the opportunity to call back some code just before the real UpdateDatabase is executed, which gives me the chance to insert some thread sync' code:

  [TestMethod]
  public void HandlesConcurrency()
  {
    //Create a shared object to work with
    var ecoSpace = TestHelper.EcoSpace.Create();
    var someInstance = new SomeClass(ecoSpace);
    ecoSpace.UpdateDatabase();
    //Get it's external ID
    string someInstanceID = ecoSpace.ExternalIds.IdForObject(someInstance);

    //Create a thread sync object which both threads will wait for before
    //passing on their UpdateDatabase call to the original persistence servce
    var startUpdateDB = new ManualResetEvent(false);

    //Create a thread sync object which tells the test when this thread has called
    //PersistenceService.UpdateDatabase
    var user1ReadyToUpdateDB = new AutoResetEvent(false);

    bool user1Conflict = false;
    var user1ThreadStart = new ThreadStart(
        delegate()
        {
          HandlesConcurrency_UserEmulation(
            ecoSpace.PersistenceMapper,
            someInstanceID,
            user1ReadyToUpdateDB,
            startUpdateDB,
            out user1Conflict);
        }
      );


    //Create a thread sync object which tells the test when this thread has called
    //PersistenceService.UpdateDatabase
    var user2ReadyToUpdateDB = new AutoResetEvent(false);

    bool user2Conflict = false;
    var user2StartUpdateDB = new AutoResetEvent(false);
    var user2ThreadStart = new ThreadStart(
        delegate()
        {
          HandlesConcurrency_UserEmulation(
            ecoSpace.PersistenceMapper,
            someInstanceID,
            user2ReadyToUpdateDB,
            startUpdateDB,
            out user2Conflict);
        }
      );

    //Create and start both threads
    var user1Thread = new Thread(user1ThreadStart);
    var user2Thread = new Thread(user2ThreadStart);
    user1Thread.Start();
    user2Thread.Start();

    //Wait until they have both signalled that the call to UpdateDatabase has been reached
    user1ReadyToUpdateDB.WaitOne();
    user2ReadyToUpdateDB.WaitOne();

    //Both threads have now executed DoSomething() as far as the call to
    //persistenceService.UpdateDatabase and are waiting for me to tell them
    //to continue.
    startUpdateDB.Set();

    //Both threads will now wake up and call the original PersistenceService.UpdateDatabase.
    //Wait for both threads to finish so that the test does not end prematurely.
    user1Thread.Join();
    user2Thread.Join();

    //Check at least one experienced a conflict
    int lockCount = 0;
    if (user1Conflict)
      lockCount++;
    if (user2Conflict)
      lockCount++;
    Assert.AreEqual(1, lockCount, "One should experience a lock conflict");
  }

  private void HandlesConcurrency_UserActionEmulation(
    PersistenceMapper persistenceMapper,
    string someInstanceID,
    AutoResetEvent readyToUpdateDB,
    WaitHandle startUpdateDB,
    out bool lockConflict)
  {
    //SETUP

    //Create secondary ecospaces with the same persistence mapper as the
    //original. This is because my test EcoSpace uses a memory persistence mapper
    var ecoSpace = TestHelper.EcoSpace.CreateWithSharedMapper(persistenceMapper);

    //Create the persistence service with the BeforeUpdateDatabase event
    var callbackPersistenceService = new TestHelper.PersistenceServiceWithEvents(ecoSpace);
    //Register it
    ecoSpace.RegisterEcoService<IPersistenceService>(callbackPersistenceService);

    //Ensure update happens in sync, this occurs when we try to lock the object
    callbackPersistenceService.BeforeDatabaseUpdate += (sender, args) =>
      {
        //Notify we are ready to update
        readyToUpdateDB.Set();
        //Now wait to be told to complete the update
        startUpdateDB.WaitOne();
      };

    //ACT

    //Now test our code
    var someServiceToTest = new SomeService(ecoSpace);
    var someInstance = ecoSpace.ExternalIds.ObjectForId(someInstanceID).GetValue<SomeClass>();
    try
    {
      lockConflict = false;
      someServiceToTest.DoSomething(someInstance, new User(ecoSpace));
    }
    catch (OptimisticLockException)
    {
      lockConflict = true;
    }
  }


So there you have it. An example of how Inversion of Control and the Service Provider pattern can enable you to redirect calls in parts of your code to enable complex testing scenarios without having to change your implementation!

No comments: