Tuesday, November 3, 2009

How To Unittest Entities

So tonight I have been refactoring some of my code again. I’m trying to get a better domainmodel with real entities, ones that have an Id. Now the problem for me was that I handed out the responsibility of generating id´s for entities to NHibernate, like this:

   1: <id name="ObjectIdentity" column="Id" type="System.Guid" unsaved-value="00000000-0000-0000-0000-000000000000">
   2:       <generator class="guid.comb"/>
   3:     </id>


The problem with this is unittesting. How to I make a unittest where I have no repository with a database? Whoops did anyone say persistence ignorance :)


I want to create an instance of a Company I can’t just use it’s contructor as this would give me an invalid entity, one with no Id assigned. After some reading with uncle Google I thought the answer was to let the application generate the id´s and not NHibernate, but further reading told me NOT to. The was some caching/performance issues when NHibernate is not in controle of id assignment. Further I didn’t like the concept, there must be another solution. Suddenly it stroke me, I’m already using Dependency Injection, thats the answer. Well how should I do this? First some code then an explanation :)



   1: public class Company
   2:     {
   3:         private Guid id = Guid.Empty;
   4:  
   5:         private Company()
   6:         {
   7:             //nhibernate uses this one 
   8:         }
   9:  
  10:         internal Company(string name)
  11:         {
  12:             //....store name etc.   
  13:         }
  14:  
  15:         internal void AssignId(Guid id)
  16:         {
  17:             //sanity check
  18:             if(this.id == Guid.Empty || id == Guid.Empty)
  19:                 throw new InvalidOperationException("Entity already has an id and id can not be empty");
  20:             this.id = id;
  21:         }
  22:     }



   1: public class CompanyAggregate
   2:     {
   3:         private readonly IRepository<Company> companies;
   4:  
   5:         public CompanyAggregate(IRepository<Company> companies )
   6:         {
   7:             this.companies = companies;
   8:         }
   9:  
  10:         Company CreateNewCompany(string name)
  11:         {
  12:             Company company = new Company(name);
  13:             companies.Add(company);
  14:             return company;
  15:         }
  16:     }



   1: public interface IRepository<T>
   2:     {
   3:         void Add(T entity);
   4:         T Get(Guid id);
   5:         void Delete(T entity);
   6:         ICollection<T> FindAll(T entity);
   7:     }
   8:  
   9:     public class CompanyRepository : IRepository<Company>
  10:     {
  11:         public void Add(Company entity)
  12:         {
  13:             entity.AssignId(Guid.NewGuid());
  14:         }
  15:  
  16:         public Company Get(Guid id)
  17:         {
  18:             throw new NotImplementedException();
  19:         }
  20:  
  21:         public void Delete(Company entity)
  22:         {
  23:             throw new NotImplementedException();
  24:         }
  25:  
  26:         public ICollection<Company> FindAll(Company entity)
  27:         {
  28:             throw new NotImplementedException();
  29:         }
  30:     }

Okay, the code is just to show the concept, not to be ready for production :)


Well the concept is that a CompanyRepository is created for Test, this could be done using some mocking tool, but the key point is that it is the responsibility of the repository to assign an Id to an entity, same thing as if the id is assigned using nhibernate and some id generator. And when working inside the domain, all new companies should only be created using the aggregate.


It is very important to notice that the visibility of the company entity is private and internal, the private is because of nhibernate, and the internal is for me to be able to make tests. This isolates the id assignment to a very narrow part of the domain. Some might say there is still a chance that this might break as the id is assigned “later”, but I would still give it a Go.

No comments: