Faking Data
Clark's DataContextWrapper makes it possible to mock the LINQ to SQL data context. This is a tremendous advantage when developing unit tests that make use of the data context. Andrew's implementation is, however, more of a fake implementation rather than a mock. A fake implementation implements the same interface, but uses a simpler mechanism to accomplish the same tasks. A mock implementation, on the other hand, does not attempt to actually perform the same actions, but simply pretends to. Another difference is that the mock implementation typically tracks calls to its methods so that they can be verified, whereas a fake implementation is typically a simple stand-in for the actual implementation.
I decided early on, though, that I wanted to fake the data in addition to mocking the data context so this was actually ideal for me. Because so much of my application interacts with the database, I wanted to be able to use a DataContextWrapper that acted as much like the real database as possible. My feeling is that using a fake database would make it conceptually easier to write tests as if I were directly interacting with the database without having to consider all of the interactions that would go on under the hood. I suspect that there are many who would disagree with me, but I find that I work better with this model. One advantage is that I can simply reuse the fake data over and over again, yet when necessary I can still mock the DataContextWrapper, for example when I need one of its methods to throw an exception. When faking data I found that the fake implementation of the DataContextWrapper needed a few tweaks to make it really usable.
Inserting Data
The fake DataContextWrapper, as implemented by Stuart, adds the entity in the InsertOnSubmit method. However, the real LINQ To SQL data context doesn't insert the data into the database until SubmitChanges is called. Since I wanted my fake context to work as much like the real context as possible, I decided to implement a mechanism to keep track of the inserted data until SubmitChanges is called rather than add the entity to the fake table implementation during InsertOnSubmit. Likewise, it doesn't make much sense to use a relational database unless your data is related. Nearly all applications have entities that are related to each other. LINQ to SQL implements this using EntityRef (for one-to-one relationships) and EntitySets (for one-to-many relationships). The natural way to add related data in LINQ to SQL is to add it to the EntityRef or EntitySet representing the association.
var context = new DataContextWrapper<MyDataContext>();
var masterEntity = new MasterEntity { ... };
masterEntity.RelatedEntities.Add( new RelatedEntity { ... } );
context.InsertOnSubmit( masterEntity );
context.SubmitChanges();
Without some special consideration, though, a mock DataContextWrapper doesn't support adding new related entities this way. I found myself writing code like this, instead, to pass my tests.
var context = new DataContextWrapper<MyDataContext>();
var masterEntity = new MasterEntity { ... };
var relatedEntity = new RelatedEntity
{
MasterEntity = masterEntity,
...
}
masterEntity.RelatedEntities.Add( relatedEntity );
context.InsertOnSubmit( masterEntity );
context.InsertOnSubmit( relatedEntity );
context.SubmitChanges();
Note the difference between an actual context and the wrapper's implementation of InsertOnSubmit. I highly prefer the wrapper's implementation.
Clearly, to get the code I wanted, I needed to make my fake DataContextWrapper be able to detect when a newly inserted entity has related data and insert it as well. This would enable me to pass my tests without having to write extra code for the fake implementation. In order to do this I search the related entities for each of the entities stored in the fake tables for entities that are not in the appropriate table. These entities get added to the set of entities that need to be inserted into the fake implementation during the insert phase of SubmitChanges.
To do this I need a few helper methods for my FakeDataContextWrapper. These methods will iterate through an object's referenced objects and, if they aren't already in the fake data, schedule them to be added during the insert phase of SubmitChanges.
private void AddReferencedObjects( object entity )
{
foreach (var set in GetEntitySets( entity ))
{
foreach (var item in set)
{
if (!this.mockDatabase.Tables[item.GetType()].Contains( item ))
{
this.Added.Add( item );
}
}
}
foreach (var reference in GetEntityRefs( entity ))
{
if (!this.mockDatabase.Tables[reference.GetType()].Contains( reference ))
{
this.Added.Add( reference );
}
}
}
private IEnumerable<IEnumerable> GetEntitySets( object entity )
{
foreach (var property in entity.GetType().GetProperties())
{
if (property.PropertyType.Name.Contains( "EntitySet" ))
{
var value = property.GetValue( entity, null );
yield return value as IEnumerable;
}
}
}
private IEnumerable<object> GetEntityRefs( object entity )
{
foreach (var property in entity.GetType().GetProperties())
{
if (property.PropertyType.Name.Contains( "EntityRef" ))
{
yield return property.GetValue( entity, null );
}
}
}
Then we add a few lines of code to our SubmitChanges implementation to take care of actually updating the fake data when entities are added/updated/deleted. Notice how we make sure that all of the new objects to be inserted get added before the insert phase. The phases run, in order, insert, delete, update -- though one could probably switch the first two. As of yet, though, we don't have any need to address updates.
Validation
var directlyAdded = new List<object>( this.Added );
foreach (var obj in directlyAdded)
{
AddReferencedObjects( obj );
}
foreach (var list in this.mockDatabase.Tables.Values)
{
foreach (var obj in list)
{
AddReferencedObjects( obj );
}
}
foreach (var obj in this.Added)
{
this.mockDatabase.Tables[obj.GetType()].Add( obj );
}
this.Added.Clear();
foreach (var obj in this.Deleted)
{
this.mockDatabase.Tables[obj.GetType()].Remove( obj );
}
this.Deleted.Clear();
I decided to use Scott Guthrie's validation techniques on LINQ to SQL entities. To this end, I have a IValidatedEntity interface that my entities implement that defines a GetRuleViolations() method where my business rules are validated. In addition, I implement the OnValidate partial method, which calls GetRuleViolations to ensure that my entities are valid prior to saving them to the database. Unfortunately Andrew's mock context doesn't address the validation requirements. I decided to implement validation in the SubmitChanges method so that my fake DataContextWrapper would also perform validation just like the real context.
One issue that I ran into, however, is that the real data context tracks the changes that are made to existing entities so that it knows which ones need to be updated. It only updates those entities that have changed. Rather than add this complexity to the fake implementation, I decided instead to simply validate all entities as if they were being updated during the update phase of SubmitChanges. This incurs a little extra processing overhead for each unit test that touches code that does a SubmitChanges but the advantage is that the fake implementation is simpler.
I use reflection to find the OnValidate method for each entity and invoke it with the proper ChangeAction. Finally, in order to get the actual exception, instead of the exception thrown by the reflection calls, I wrap the entire SubmitChanges code in a try-catch block and throw the InnerException on errors.
public virtual void SubmitChanges( ConflictMode failureMode )
{
try
{
var directlyAdded = new List<object>( this.Added );
foreach (var obj in directlyAdded)
{
AddReferencedObjects( obj );
}
foreach (var list in this.mockDatabase.Tables.Values)
{
foreach (var obj in list)
{
AddReferencedObjects( obj );
}
}
foreach (var obj in this.Added)
{
MethodInfo validator = obj.GetType().GetMethod( "OnValidate",
BindingFlags.Instance | BindingFlags.NonPublic );
if (validator != null)
{
validator.Invoke( obj, new object[] { ChangeAction.Insert } );
}
this.mockDatabase.Tables[obj.GetType()].Add( obj );
}
this.Added.Clear();
foreach (var obj in this.Deleted)
{
MethodInfo validator = obj.GetType().GetMethod( "OnValidate",
BindingFlags.Instance | BindingFlags.NonPublic );
if (validator != null)
{
validator.Invoke( obj, new object[] { ChangeAction.Delete } );
}
this.mockDatabase.Tables[obj.GetType()].Remove( obj );
}
this.Deleted.Clear();
foreach (KeyValuePair<Type, IList> tablePair in this.mockDatabase.Tables)
{
MethodInfo validator = tablePair.Key.GetMethod( "OnValidate",
BindingFlags.Instance | BindingFlags.NonPublic );
if (validator != null)
{
foreach (var obj in tablePair.Value)
{
validator.Invoke( obj, new object[] { ChangeAction.Update } );
}
}
}
}
catch (TargetInvocationException e)
{
throw e.InnerException;
}
}
I've made a few other tweaks to the entire set of classes that support mocking the data context. I'll write about those later when I tackle automating auditing for LINQ to SQL.
No comments :
Post a Comment
Comments are moderated.