Monday, 3 February 2014

Reusing common mocking functionality using Moq

Last month I wrote how to mock a repository in a generic way using mocking framework Moq. The example I used to illustrate the scenario was a pure generic repository, let's see it again:
public interface IIdentityKey<Tkey>
{
    TKey Id { get; set; }
}

public interface IRepository<Tkey,TEntity> 
    where TEntity : IIdentityKey<Tkey>
{
    IQueryable<TEntity> All();
 
    TEntity FindBy(TKey id);
 
    TEntity Add(TEntity entity);
 
    void Update(TEntity entity);
 
    void Delete(TEntity entity);
}
However, in real life, this is not always achievable, sometimes we need specific repositories per entity, let's make a change to that example, for instance, adding a new field named GroupId to Person entity.
public class Person :  IIdentityKey<long>
{
    public long Id { get; set; }

    public string Name { get; set; }

    public string Email { get; set; }

    public long GroupId { get; set; }
}
Thus, the new repository would need a new method named GetByGroup, but only for this repository, hence the new repository interface looks like this:
public interface IPersonRepository : IRepository<long, Person>
{
    IQueryable<Person> GetByBroup(long groupId); 
}
When trying to mock the behaviour it's more or less the same as in previous example except for the fact that we have an additional method GetByGroup, so it's not fair having to implement or even worst, copy and paste the boilerplate previously implemented. We need some way to set up the common behaviour to any inheritor of IRepository<TK,TE>. However, an object of type Mock<IPersonRepository> can not be assigned to an object of type Mock<IRepository<TK,TE>>. What to do?

Well, the class Mock<T> inherits from Mock without generics, we can create a method that accepts a Mock as parameter, but how do we treat that parameter as concrete typed object? Fortunately, the Mock class contains a method As<T> which returns the mock as a typed object specified in generic argument.

Finally, the rest is only to move the common set up to that new method, here is my sample:
public static void SetupRepositoryMock<TK, TE>(Mock mockRepo, List<TE> data)
    where TE : class, IIdentityKey<TK>
{
    var mock = mockRepo.As<IRepository<TK, TE>>();

    // setup All method
    mock.Setup(x => x.All()).Returns(data.AsQueryable());

    // setup Add method
    mock.Setup(x => x.Add(It.IsAny<TE>()))
        .Returns(new Func<TE, TE>(x =>
        {
            dynamic lastId = data.Last().Id;
            dynamic nextId = lastId + 1;
            x.Id = nextId;
            data.Add(x);
            return data.Last();
        }));

    // setup Update method
    mock.Setup(x => x.Update(It.IsAny<TE>()))
        .Callback(new Action<TE>(x =>
        {
            var i = data.FindIndex(q => q.Id.Equals(x.Id));
            data[i] = x;
        }));

    // setup FindBy (id)
    mock.Setup(x => x.FindBy(It.IsAny<TK>()))
        .Returns(new Func<TK, TE>(
            x => data.Find(q => q.Id.Equals(x))
        ));

    // setup Delete
    mock.Setup(x => x.Delete(It.IsAny<TE>()))
        .Callback(new Action<TE>(x =>
        {
            var i = data.FindIndex(q => q.Id.Equals(x.Id));
            data.RemoveAt(i);
        }));
}
Now, our method CreateController would look like this.
private PersonController CreateController()
{
    // create the mock to specific repository
    var mock = new Mock<IPersonRepository>();

    // set up the common methods 
    SetupRepositoryMock<long, Person>(mock, _fakedata);

    // start mocking specific methods
    mock.Setup(x => x.GetByBroup(It.IsAny<long>()))
        .Returns(new Func<long, IQueryable<Person>>(
            gid => _fakedata.Where(x => x.GroupId == gid).AsQueryable())
        );

    PersonController target = new PersonController(mock.Object);
    return target;
}
As the goal was only to implement the mock for the specific method to the repository interface, it was achieved by invoking SetupRepositoryMock<TK,TE> for common base methods and followed by the implementation of specific method. The full sample source code can be downloaded here.

No comments:

Post a Comment