Testing Your CRUD Using Generics

I often use a generic repository pattern as a wrapper around my data access logic. This wrapper generally serves the purpose of encapsulating specific implementation details. Here are the repository interfaces that I current use:

public interface IReadOnlyRepository<TEntity> where TEntity:class
{
    IQueryable<TEntity> All();
}
public interface IRepository<TEntity> : IReadOnlyRepository<TEntity> where TEntity:class
{
    bool Add(TEntity entity);
    bool Add(IEnumerable<TEntity> items);
    bool Update(TEntity entity);
    bool Delete(TEntity entity);
    bool Delete(IEnumerable<TEntity> entities);
}
public interface IKeyedRepository<TKey, TEntity> : IRepository<TEntity>
    where TEntity : class, IKeyed<TKey>
{
    TEntity FindBy(TKey id);
}
public interface IKeyed<TKey>
{
    TKey Id { get; }
}

 

I have a couple of previous posts on the repository pattern.

After creating a specific implementation of the repository, I often want to do some simple tests of the create, read, update and delete (CRUD) functions. This allows a quick test to ensure that my repository is configured correctly (DB configuration, entity mappings…etc). These tests are not meant to be part of my automated test bed. These are simply something that I run locally with a real database to get a quick “go” or “no go” indication. To accomplish these tests usually requires the following steps:

  1. Create an instance of the repository for the entity type.
  2. Get a count of the number of entities in the DB before we start.
  3. Instantiate an entity using random data.
  4. Add the entity to the DB.
  5. Read the entity back out of the DB.
  6. Compare and ensure they are equal.
  7. Modify the original entity.
  8. Update the entity in the DB.
  9. Read the modified entity from the DB.
  10. Compare and ensure the entity was modified.
  11. Delete the entity.
  12. Get a count of the number of entities in the DB after.
  13. Compare the count before and after and ensure they are equal.

This is not a full battery of tests, but hits the all the CRUD methods. I like to do this for each type of entity. This is very repetitive and provides a good opportunity to use Generics to simplify the process.

Create and Update Random Entities

The following bit of code generates an entity with properties that have been initialized with random values.

public static class Random<T> where T:class, new()
{
    private static readonly Random _random = new Random(DateTime.Now.Millisecond);

    public static T Create()
    {
        return Create(null);
    }

    public static T Create(List<string> protectedPropertyNames)
    {
        T result = new T();
        return Update(result, protectedPropertyNames);
    }

    public static T Update(T obj)
    {
        return Update(obj, null);
    }

    public static T Update(T obj, List<string> protectedPropertyNames)
    {
        Type type = typeof (T);
        foreach (PropertyInfo propertyInfo in type.GetProperties())
        {
            PropertyInfo info = propertyInfo;
            bool isFound = protectedPropertyNames == null ? false : protectedPropertyNames.Any(name => name.ToLower() == info.Name.ToLower());

            if (propertyInfo.CanWrite && !isFound)
            {
                if (propertyInfo.PropertyType == typeof(DateTime))
                {
                    propertyInfo.SetValue(obj, DateTime.Now, null);
                }
                else if (propertyInfo.PropertyType == typeof(int))
                {
                    propertyInfo.SetValue(obj, _random.Next(), null);
                }
                else if (propertyInfo.PropertyType == typeof(bool))
                {
                    propertyInfo.SetValue(obj, _random.Next(0, 2) == 0 ? false : true, null);
                }
                else if (propertyInfo.PropertyType == typeof(string))
                {
                    propertyInfo.SetValue(obj, Guid.NewGuid().ToString(), null);
                }
                else if (propertyInfo.PropertyType == typeof(double))
                {
                    propertyInfo.SetValue(obj, _random.NextDouble(), null);
                }
                else if (propertyInfo.PropertyType == typeof(byte))
                {
                    propertyInfo.SetValue(obj, byte.Parse(_random.Next(byte.MinValue, byte.MaxValue).ToString()), null);
                }
                else if(propertyInfo.PropertyType == typeof(Guid))
                {
                    propertyInfo.SetValue(obj, Guid.NewGuid(), null);
                }
            }
        }

        return obj;
    }
}

 

The code above serves two functions: create a new entity and update an existing entity. The create is really an update on a new entity and falls through to the same update code. The methods take an optional parameter that allows a list of properties to be provided that should not be updated. This allows you to set IDs that refer to existing objects without the reference being broken. The list of “if-else if” statements could be extended to include additional types.

A Generic CRUD Tester

Now that we can create and update entities with properties having random values. We need a generic test bench for the CRUD methods.

public static class CrudTester<T> where T: class, IKeyed<int>, new()
{
    public static void Test(ISessionFactory sessionFactory)
    {
        Test(sessionFactory, null);
    }

    public static void Test(ISessionFactory sessionFactory, List<string> protectedPropertyNames)
    {
        T entity1 = Random<T>.Create(protectedPropertyNames);
        Test(sessionFactory, protectedPropertyNames, entity1);
    }

    public static void Test(ISessionFactory sessionFactory, List<string> protectedPropertyNames, T seed)
    {
        int countBefore;

        using (UnitOfWork unitOfWork = new UnitOfWork(sessionFactory))
        {
            IKeyedRepository<int, T> repo = new Repository<int, T>(unitOfWork.Session);

            countBefore = repo.All().Count();

            // create
            repo.Add(seed);
            Assert.IsTrue(seed.Id > 0);

            unitOfWork.Commit();
        }

        T entity2;
        using (UnitOfWork unitOfWork = new UnitOfWork(sessionFactory))
        {
            IKeyedRepository<int, T> repo = new Repository<int, T>(unitOfWork.Session);

            // read
            entity2 = repo.FindBy(seed.Id);
            Assert.IsTrue(seed.Equals(entity2));

            unitOfWork.Commit();
        }

        using (UnitOfWork unitOfWork = new UnitOfWork(sessionFactory))
        {
            IKeyedRepository<int, T> repo = new Repository<int, T>(unitOfWork.Session);

            // update
            seed = Random<T>.Update(seed, protectedPropertyNames);
            repo.Update(seed);
            T entity3 = repo.FindBy(seed.Id);
            Assert.IsFalse(entity2.Equals(seed));
            Assert.IsTrue(entity3.Equals(seed));

            unitOfWork.Commit();
        }

        using (UnitOfWork unitOfWork = new UnitOfWork(sessionFactory))
        {
            IKeyedRepository<int, T> repo = new Repository<int, T>(unitOfWork.Session);

            // delete
            repo.Delete(seed);
            int countAfter = repo.All().Count();
            Assert.IsTrue(countAfter == countBefore);

            unitOfWork.Commit();
        }
    }
}

 

This above code provides three entry points with various levels of control over the test. The following parameters are either specified externally or internally generated:

  • ISessionFactory – An NHibernate session factory (creates instances of ISession). This would need to be modified to work with other ORMs.
  • Protected Property Names – This provides a list of property names that should not be modified.
  • Seed – This provides a seed entity for testing allowing you to hook up your foreign keys and such.

All three entry points fall into the final ‘Test’ method. This method simply runs the CRUD tests that we previously outlined.

Summary

The above code provides an easy framework to create tests that look like the following:

public Crud_Test_User_Entity()
{
    NHibernateHelper helper = new NHibernateHelper(_connectionString);
    CrudTester<User>.Test(helper.SessionFactory);
}

public Crud_Test_Address_Entity()
{
    Address address = Random<Address>.Create();
    address.UserId = 10;  // assume this user exists in DB

    NHibernateHelper helper = new NHibernateHelper(_connectionString);
    CrudTester<Address>.Test(helper.SessionFactory, new List<string>{"UserId"}, address);
}

This allows the basic CRUD methods to quickly be tested to ensure the ORM is configured correctly.

Leave a Reply

Your email address will not be published.

*