Sunday, July 8, 2012

Wrapping repositories, MongoDB, and stubbing are all touched on in this blog posting.

UserAccount foo = new UserAccount("tomjaeschke@tomjaeschke.com", "foo");
UserAccount bar = new UserAccount("fevercheese@gmail.com", "bar");
IUserAccountRepository repository =
      ObjectFactory.GetInstance<IUserAccountRepository>();
if (repository.GetCountOfExistingUserAccounts() == 0) foo.Status =
      UserStatus.Administrative;
repository.SaveNewUserAccount(foo);
repository.SaveNewUserAccount(bar);

 
 

I was able to replace the six lines above (which YES sit in the user interface layer of a Palermo Onion which YES is also the bootstrapper for Inversion of Control via Jeremy Miller's StructureMap) with the five lines below.

UserAccount foo = new UserAccount("tomjaeschke@tomjaeschke.com", "foo");
UserAccount bar = new UserAccount("fevercheese@gmail.com", "bar");
UserAccountRepositoryWrapper repository = new
      UserAccountRepositoryWrapper(
      ObjectFactory.GetInstance<IUserAccountRepository>());
repository.SaveNewUserAccount(foo);
repository.SaveNewUserAccount(bar);

 
 

That is all of the user interface code I am going to show in this blog posting! I was able to push the decision making as to whether or not to make a UserAccount administrative down into the Core by creating a repository wrapper like so:

using MongoLogin.Core.Interfaces.DataIntegration;
namespace MongoLogin.Core.InfrastructureWrappers.DataIntegration
{
   public class UserAccountRepositoryWrapper
   {
      public IUserAccountRepository Repository { get; set; }
      
      public UserAccountRepositoryWrapper(IUserAccountRepository repository)
      {
         Repository = repository;
      }
      
      public void SaveNewUserAccount(UserAccount userAccount)
      {
         int numberOfUsers = Repository.GetCountOfExistingUserAccounts();
         if (numberOfUsers == 0) userAccount.Status = UserStatus.Administrative;
         Repository.SaveNewUserAccount(userAccount);
      }
   }
}

 
 

The decision making may then be TESTED as part of the Core logic. It is not something dirty and random in the UI.

using Microsoft.VisualStudio.TestTools.UnitTesting;
using MongoLogin.Core.InfrastructureWrappers.DataIntegration;
using MongoLogin.Core.Tests.InterfaceTestingHelpers.DataIntegration;
namespace MongoLogin.Core.Tests.InfrastructureWrapperTests.DataIntegration
{
   [TestClass]
   public class UserAccountRepositoryWrapperTests
   {
      [TestMethod]
      public void SaveNewUserAccountUpdatesUserStatusWhenAppropriateTest()
      {
         UserAccount userAccount = new UserAccount("tomjaeschke@tomjaeschke.com",
               "foo");
         StubbedUserAccountRepository stub = new StubbedUserAccountRepository();
         stub.ValueToReturnForGetCountOfExistingUserAccounts = 0;
         UserAccountRepositoryWrapper repository = new
               UserAccountRepositoryWrapper(stub);
         repository.SaveNewUserAccount(userAccount);
         Assert.AreEqual(stub.UserAccountHandedIntoSaveNewUserAccount.Status,
               UserStatus.Administrative);
         
         userAccount = new UserAccount("fevercheese@gmail.com", "bar");
         stub = new StubbedUserAccountRepository();
         stub.ValueToReturnForGetCountOfExistingUserAccounts = 1;
         repository = new UserAccountRepositoryWrapper(stub);
         repository.SaveNewUserAccount(userAccount);
         Assert.AreEqual(stub.UserAccountHandedIntoSaveNewUserAccount.Status,
               UserStatus.Awaiting);
      }
   }
}

 
 

There should be no need to mock methods in the Core assuming the Core is clean from the "contamination" of external dependencies. Instead, the repository that inherits from IUserAccountRepository may be faked by a stub that in turn has no external dependencies.

using MongoLogin.Core.Interfaces.DataIntegration;
namespace MongoLogin.Core.Tests.InterfaceTestingHelpers.DataIntegration
{
   public class StubbedUserAccountRepository : IUserAccountRepository
   {
      public int ValueToReturnForGetCountOfExistingUserAccounts { get; set; }
      public UserAccount UserAccountHandedIntoSaveNewUserAccount { get; set; }
      
      public int GetCountOfExistingUserAccounts()
      {
         return ValueToReturnForGetCountOfExistingUserAccounts;
      }
      
      public void SaveNewUserAccount(UserAccount userAccount)
      {
         UserAccountHandedIntoSaveNewUserAccount = userAccount;
      }
   }
}

 
 

The real repository in the Infrastructure project looks like so:

using MongoDB.Driver;
using MongoLogin.Core;
using MongoLogin.Core.Interfaces.DataIntegration;
using MongoDB.Driver.Builders;
namespace MongoLogin.Infrastructure.DataIntegration
{
   public class UserAccountRepository : IUserAccountRepository
   {
      private string connectionString = "mongodb://localhost/?safe=true";
      private string regularExpressionForStatusMatching = "/^([A].*)$/";
      private string databaseName = "test";
      private string collectionName = "entities";
      
      public int GetCountOfExistingUserAccounts()
      {
         var server = MongoServer.Create(connectionString);
         var database = server.GetDatabase(databaseName);
         var allRecords = database.GetCollection<UserAccountDataTransferObject>
               (collectionName);
         var query = Query.Matches(CommonMagicStrings.Status(),
               regularExpressionForStatusMatching);
         var legitimateResults = allRecords.Find(query);
         return (int)legitimateResults.Count();
      }
      
      public void SaveNewUserAccount(UserAccount userAccount)
      {
         var server = MongoServer.Create(connectionString);
         var database = server.GetDatabase(databaseName);
         var allRecords = database.GetCollection<UserAccountDataTransferObject>
               (collectionName);
         var dto = new UserAccountDataTransferObject
                     {
                        Email = userAccount.Email,
                        Password = userAccount.Password,
                        Status = userAccount.Status.ToString()
                     };
         allRecords.Insert(dto);
         allRecords.Save(dto);
      }
   }
}

 
 

The interface that the real and fake repositories inherit from...

namespace MongoLogin.Core.Interfaces.DataIntegration
{
   public interface IUserAccountRepository
   {
      int GetCountOfExistingUserAccounts();
      void SaveNewUserAccount(UserAccount userAccount);
   }
}

 
 

Alright. Whew! Let's slow down for a minute. What am I trying to do? I thought I'd start teaching myself how to interface with MongoDB from C# this weekend and to that end I thought I'd write a super simple login control that lets users create accounts. Administrative users can make awaiting users active. I thought it would be nice if the first user put into the system was conveniently set up to be administrative, but clearly I would not want to do this for most users. I needed to make a repository check to see if any users existed yet to determine if I should save a new user in administrative status. This means I needed to tie the counting of existing users to the saving of users. The dilemma of how to get the logic driving the conditional administrative state into the Core and not the Infrastructure nor the UI in the name of making the testable Core as fat as possible is addressed with my repository wrapper solution. That said, I have a few other things to say. I left this out of the Core and in the Infrastructure layer:

using MongoDB.Bson;
namespace MongoLogin.Infrastructure.DataIntegration
{
   public class UserAccountDataTransferObject
   {
      public ObjectId Id { get; set; }
      public string Email { get; set; }
      public string Password { get; set; }
      public string Status { get; set; }
   }
}

 
 

It turns out that objects put into Mongo have to have an Id of type ObjectId which comes from the MongoDB.Bson namespace which has no business bleeding into the Core and thus all Mongo-hydratable objects cannot live in the Core. Yuck!

this sort of honesty is unnerving! on Twitpic

That means my Mongo POCO has to have sister in the Core:

namespace MongoLogin.Core
{
   public class UserAccount
   {
      public string Email { get; set; }
      public string Password { get; set; }
      public UserStatus Status { get; set; }
      
      public UserAccount(string email, string password)
      {
         PrepareUserAccount(email, password, null);
      }
      
      public UserAccount(string email, string password, string status)
      {
         PrepareUserAccount(email, password, status);
      }
      
      public UserAccount(string email, string password, UserStatus status)
      {
         PrepareUserAccount(email, password, status.ToString());
      }
      
      public void PrepareUserAccount(string email, string password, string status)
      {
         Email = email;
         Password = password;
         Status = UserStatus.Awaiting;
         if (status == UserStatus.Aborted.ToString()) Status = UserStatus.Aborted;
         if (status == UserStatus.Active.ToString()) Status = UserStatus.Active;
         if (status == UserStatus.Administrative.ToString()) Status =
               UserStatus.Administrative;
      }
   }
}

 
 

Also, I'm going to have a bunch of magic strings to manage in working with Mongo it seems. I wrote a kludgy class to start wrangling such.

namespace MongoLogin.Core
{
   public static class CommonMagicStrings
   {
      public static string Email()
      {
         return "Email";
      }
      
      public static string Metadata()
      {
         return "Metadata";
      }
      
      public static string Password()
      {
         return "Password";
      }
      
      public static string Status()
      {
         return "Status";
      }
   }
}

 
 

I end with the enum.

namespace MongoLogin.Core
{
   public enum UserStatus
   {
      Aborted,
      Active,
      Administrative,
      Awaiting
   }
}

No comments:

Post a Comment