Agile Entity Framework 4 Repository: Part 6: Mocks & Unit Tests

I did finish this series, honest I did. But not in the blog. I’ve shown this in a number of conferences and even in my book, but I never came back and wrote it all down. In fact, I had the whole solutino written before I began the series, but it has gone through a lot of changes.

Where did I leave off?

  1. Agile Entity Framework 4 Repository: Part 1- Model and POCO Classes
  2. Agile Entity Framework 4 Repository: Part 2- The Repository
  3. Agile EF4 Repository: Part 3 -Fine Tuning the Repository
  4. Agile EF4 Repository: Part 4: Compiled LINQ Queries
  5. Agile Entity Framework 4 Repository Part 5: IObjectSet and Include
  6. Agile Entity Framework 4 Repository: Part 6: Mocks & Unit Tests

So finally, I can create the mocks and the tests.

I’m not going to walk through this in great detail since I’ve already done that in my book and don’t feel like laying the whole thing out again. And because my solution has evolved, this last bit of code might not line up exactly with the prior blog posts. You’ll find

I have a single project for tests. It has references to the model classes, the Repositories, the interfaces (IContext, IObjectSet and IEntityRepository), the model and ObjectContext (for integration tests) and System.Data.Entity.

In order to run unit tests, I have a mock context that implements IContext. My real “hit the database” context, also implements IContext. But the mock contexts will return data by creating some fake data on the fly. Yes I said contexts with the “s” on the end. I have more than one.

Because this is all dependent on using IObjectSet (previous post) rather than the ObjectSet, and you can’t instantiate an interface, you’ll need another implementation of the IObjectSet which you can use in your fake context.

It’s pretty simple. The key is to implement all of the interfaces that IObjectSet implements as well and the logic of those methods is fairly common. The trick is to replace the specific methods of the ObjectSet (e.g. Attach, DeleteObject, etc) with generic List methods. So Attach will become Add, DeleteObject will become Remove, etc.

using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Data.Objects;

namespace POCOEFTests
{
  class MockObjectSet<T>: IObjectSet<T> where T : class
  {
    readonly IList<T> _container=new List<T>();

      public void AddObject(T entity)
    {
      _container.Add(entity);
    }

    public void Attach(T entity)
    {
      _container.Add(entity);
    }

    public void DeleteObject(T entity)
    {
      _container.Remove(entity);
    }

    public void Detach(T entity)
    {
      _container.Remove(entity);
    }

    public IEnumerator<T> GetEnumerator()
    {
      return _container.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
      return _container.GetEnumerator();
    }

    public Type ElementType
    {
      get{return typeof(T);}
    }

    public System.Linq.Expressions.Expression Expression
    {
      get { return _container.AsQueryable<T>().Expression; }
    }

    public IQueryProvider Provider
    {
      get { return _container.AsQueryable<T>().Provider; }
    }
  }
}

The IContext interface requires that any implementor return the same set of IObjectSets (Contact, Customers, etc). Our real context already does this (again, by hitting the database). You’ll see that the IObjectSet properties in the mock call local methods which create fake data on the fly to populate the ObjectSet. The various sets of data are related to each other through their foreign keys.

using System;
using System.Collections.Generic;
using System.Linq;
using BAGA;
using BAGA.Repository.Interfaces;
using System.Data.Objects;

namespace POCOEFTests
{
  class MockContext : IContext
  {

    private IObjectSet<Reservation> _reservations;
    private IObjectSet<Customer> _customers;
    private IObjectSet<Trip> _trips;
    private IObjectSet<Payment> _payments;
    private IObjectSet<Contact> _contacts;

    #region IContext Members

    public IObjectSet<Contact> Contacts
    {

      get
      {
        CreateCustomers();
        return _contacts;
      }

    }

    public IQueryable<Customer> Customers
    {
      get
      {
        CreateCustomers();
        return _customers;
      }
    }

    public IObjectSet<Trip> Trips
    {
      get
      {
        CreateTrips();
        return _trips;
      }
    }

    public IObjectSet<Reservation> Reservations
    {
      get {
        CreateReservations();
        return _reservations;
      }
    }



    public IObjectSet<Payment> Payments
    {
      get
      {
        CreatePayments();
        return _payments;

      }
    }

    public string Save()
    {
      throw new NotImplementedException();
    }

public IEnumerable<T> ManagedEntities<T>()
{
  if (typeof(T) == typeof(Reservation))
  {
    var newRes = new Reservation
                   {
                     ReservationID = 1,
                     ContactID = 20,
                     TripID = 1,
                     ReservationDate = new DateTime(2009, 08, 01)
                   };

    var managedRes = new List<Reservation>{newRes};
    return (IEnumerable<T>) managedRes.AsEnumerable();
  }
  return null;
}
    
  
    public bool PreSavingValidate(out string validationErrors)
    {
      bool isvalid = true;
      validationErrors = "";

      foreach (var res in ManagedEntities<Reservation>())
      {
        string validationError;
        bool isResValid = res.ValidateBeforeSave(out validationError);
        if (!isResValid)
        {
          isvalid = false;
          validationErrors += validationError;
        }
      }

      return isvalid;
    }

    #endregion

    private void CreateReservations()
    {
      if (_reservations == null)
      {
        _reservations = new MockObjectSet<Reservation>();
        //one customer has two reservations, the other only has one
        _reservations.AddObject(new Reservation { ReservationID = 1, TripID = 1, ContactID = 19 });
        _reservations.AddObject(new Reservation { ReservationID = 2, TripID = 2, ContactID = 19 });
        _reservations.AddObject(new Reservation { ReservationID = 3, TripID = 1, ContactID = 23 });
      }
    }
    private void CreateCustomers()
    {
      if (_customers == null)
      {
        _customers = new MockObjectSet<Customer>();
        _customers.AddObject(new Customer { ContactID = 1, FirstName = "Matthieu", LastName = "Mezil" });
        _customers.AddObject(new Customer { ContactID = 19, FirstName = "Kristofer", LastName = "Anderson" });
        _customers.AddObject(new Customer { ContactID = 23, FirstName = "Bobby", LastName = "Johnson" });
      }
    }

    private void CreateContacts()
    {
      if (_contacts==null)
      {
        _contacts = new MockObjectSet<Contact>();
        _contacts.AddObject(new Customer { ContactID = 1, FirstName = "Matthieu", LastName = "Mezil" });
        _contacts.AddObject(new Customer { ContactID = 19, FirstName = "Kristofer", LastName = "Anderson" });
        _contacts.AddObject(new Customer { ContactID = 23, FirstName = "Bobby", LastName = "Johnson" });
      }
    }
    private void CreatePayments()
    {
      if (_payments == null)
      {
        _payments = new MockObjectSet<Payment>();
        //create (not enough) payments for reservation 1 (a $1000 trip)
        _payments.AddObject(new Payment
        {
          PaymentID = 1,
          ReservationID = 1,
          Amount = 500
        });
        //create a full payment for reservation 2
        _payments.AddObject(new Payment
        {
          PaymentID = 2,
          ReservationID = 2,
          Amount = 1200
        });
      }
    }
    private void CreateTrips()
    {
      if (_trips == null)
      {
        _trips = new MockObjectSet<Trip>();
        //one customer has two reservations, the other only has one
        _trips.AddObject(new Trip { TripID = 1, DestinationID = 1, TripCostUSD = 1000 });
        _trips.AddObject(new Trip { TripID = 2, DestinationID = 2, TripCostUSD = 1200 });
      }
    }

  }
}

 

This mock context creates valid data. I also have some other mock contexts that intentionally create data that is bad. I can use those contexts to test that my validation code properly responds to bad data.

Here’s an example of two tests that use two different mock contexts. They test the PreSavingValidate method that I created in an earlier post in this series. Because that method is part of the context, I literally copied it from the “real” context into the mock context so that I can test the same exact code.

    [TestMethod()]
    public void PreSavingValidationsGOODTest()
    {
      IContext context = new MockContext();
      bool valid;
      string validationErrors = "";
      valid = context.PreSavingValidate(out validationErrors);
      Assert.IsTrue(valid);
      Assert.AreEqual(validationErrors, "");
    }

    [TestMethod()]
    public void PreSavingValidationsBADDATATest()
    {
      IContext context = new MockContextBadData();
      bool valid;
      string validationErrors = "";
      valid = context.PreSavingValidate(out validationErrors);
      Assert.IsFalse(valid);
      Assert.AreNotEqual(validationErrors, "");
    }

Here’s a test that tests the GetReservationsForCustomer method in the Reservation repository. That is a method that first validates the incoming ID and then if it’s good executes the query. This method is a really good example of why you want to mock. I can’t separate the ID validation from the query execution so I need  a way to “fake” the query execution when I test the ID validation. I’m instantiating the mock and passing it into the ReservationRepository u sing an overload of the constructor. IF I did not pass in a context, the repository will instantiate a new BAEntities, which is the context implementation that executes the real queries against the database.

[TestMethod()]
public void GetReservationsForCustomerValidId()
{
  var context = new MockContext();
  var target = new ReservationRepository(context);
  int customerId = 20; //valid id
  IList<Reservation> actual = target.GetReservationsForCustomer(customerId);
  Assert.IsNotNull(actual);
}

There are a lot of other tests I ran through as well.

Now that I have posted how to build this whole thing, I know everyone will just ask me to post the code so they can just use it rather than READING the blog posts I made such an effort to write. I’ll have to find the time to clean it up (it’s a big sample from my book) before I post it but I REALLY need to get the BOOK FINISHED! (I Know you want me to). So in the mean time, everything you need is in this series. And the rough draft of the relevant chapter is online as part of the Rough Cuts publication of the 2nd edition of my book. One BIG difference from this series and the solution in my book is that I’ve also implemented a unit of work in the book. Therefore I have tests and production code where follow this pattern instead:

[TestMethod()]
public void GetReservationsForCustomerValidIdUnitOfWork()
{
  var context = new MockContext();
    var uow = new UnitOfWork(context);
  var target = new ReservationRepository(uow);
  int customerId = 20; //valid id
  IList<Reservation> actual = target.GetReservationsForCustomer(customerId);
  Assert.IsNotNull(actual);
}

Enjoy!

  Sign up for my newsletter so you don't miss my conference & Pluralsight course announcements!  

8 thoughts on “Agile Entity Framework 4 Repository: Part 6: Mocks & Unit Tests

  1. thread safety: blogs.msdn.com/…/ef-faq-misc.asp

    EF and websites…different than windows in that in windows you can have a long running context managing entities & tracking changes. Websites can use same patterns as WCF (except not Self-Tracking Entities…at least n ot out of the box). If you are using WebForms and want to use session (with all the usual caveats) you can (though not for long-lived context). Or you can use repository. MVC & repository work great. Your question is why I wrote an 800 page book (twice). 🙂

  2. Thanks for the reply! I am planning on using MVC and the Repository pattern. Does your second book cover setting up UoW to manage the context? (I plan to purchase it regardless =])

  3. yes. I enhanced the repository quite a bit after writing this blog series. UoW owns, instantiates one if necessary, calls SaveChanges etc. You can see a bit of it in the last test in this blog post.

  4. I’m confused by the PreSavingValidate.

    As shown, it’s only validating Reservations. Given it’s in the context, that seems a little specific.

    Should it maybe be moved into the repository instead?

    I’ve been working through the code in the Rough Cut on Safari, and have gotten stuck here.

  5. @Stevi

    I agree 100% and when I refactored this in my book (after learning so much more!!) the validation for the reservations is now the job of the reservation. I"ll have to look at the rough cut version to see if that is more like the blog post or more like the final version.

  6. can we get the complete solution as a download which will help us in understanding it more easily 🙂

    Thanks & Regards,

    Ram

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.