user2565762
user2565762

Reputation: 33

Moq How do you(Can you?) add two different (incompatible) interfaces to the same context?

My context is a mock of my data model I have a method "Send" in another class named "Email" My Service class uses the mocked data model. A method in my service class "SendEmailForAlarm" accesses data from the Mock data model and then calls the "Send" method in the Email Class. Question: How can I get the "Send" method in my email class to be included in my mock?

Relevent Code:

//INTERFACES
public interface IEmail
{
  ...
  void Send(string from, string to, string subject, string body, bool isHtml = false);
}
public interface IEntityModel : IDisposable
{
    ...
    DbSet<Alarm> Alarms { get; set; } // Alarms
}

//CLASSES
public class Email : IEmail
{
    ...
    public void Send(string from, string to, string subject, string body, bool isHtml = false)
    {
      ...sends the email
    }
}
public partial class EntityModel : DbContext, IEntityModel
{
    ...
    public virtual DbSet<Alarm> Alarms { get; set; } // Alarms
}
public class ExampleService : ExampleServiceBase
{
    //Constructor
    public ExampleService(IEntityModel model) : base(model) { }

    ...
    public void SendEmailForAlarm(string email, Alarm alarm)
    {
        ...
        new Email().Send(from, to, subject, body, true);
    }
}

//HELPER method
public static Mock<DbSet<T>> GetMockQueryable<T>(Mock<DbSet<T>> mockSet, IQueryable<T> mockedList) where T : BaseEntity
{
    mockSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(mockedList.Expression);
    mockSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(mockedList.ElementType);
    mockSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(mockedList.GetEnumerator());
    mockSet.Setup(m => m.Include(It.IsAny<string>())).Returns(mockSet.Object);

    // for async operations
    mockSet.As<IDbAsyncEnumerable<T>>()
        .Setup(m => m.GetAsyncEnumerator())
        .Returns(new TestDbAsyncEnumerator<T>(mockedList.GetEnumerator()));

    mockSet.As<IQueryable<T>>()
       .Setup(m => m.Provider)
       .Returns(new TestDbAsyncQueryProvider<T>(mockedList.Provider));

    return mockSet;
}

The following unit test passes but is not written to validate email was actually sent (or called) The service.SendEmailForAlarm method queries the EntityModel.Alarms DBSet and extracts information from the retrieved object. It then calls the Send method in the Email Class and returns void.

[TestMethod]
public void CurrentTest()
{
    //Data
    var alarms = new List<Alarm> {CreateAlarmObject()}  //create a list of alarm objects

    //Arrange
    var mockContext = new Mock<IEntityModel>();     
    var mockSetAlarm = new Mock<DbSet<Alarm>>();
    var service = new ExampleService(mockContext.Object);

    mockSetAlarm = GetMockQueryable(mockSetAlarm, alarms.AsQueryable());
    mockContext.Setup(a => a.Alarms).Returns(mockSetAlarm.Object);

    //Action
    service.SendEmailForAlarm("[email protected]", alarms[0]);

    //NOTHING IS ASSERTED, SendEmailForAlarm is void so nothing returned.
}

What I would like to do is put an Interceptor on the Email.Send method so I can count how many times it was executed or have mock verify validate it was called n times (in this example once).

[TestMethod]
public void WhatIWantTest()
{
    //Data
    var alarms = new List<Alarm> {CreateAlarmObject()}  //create a list of alarm objects
    var calls = 0;

    //Arrange
    var mockEmail = new Mock<IEmail>();         //NEW
    var mockContext = new Mock<IEntityModel>();     
    var mockSetAlarm = new Mock<DbSet<Alarm>>();
    var service = new ExampleService(mockContext.Object);

    mockEmail.Setup(u => 
        u.Send(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>(),
               It.IsAny<string>(), It.IsAny<bool>()))
         .Callback(() => calls++);  //NEW add a call back on the Email.Send method 
    mockSetAlarm = GetMockQueryable(mockSetAlarm, alarms.AsQueryable());
    mockContext.Setup(a => a.Alarms).Returns(mockSetAlarm.Object);

    //Action
    service.SendEmailForAlarm("[email protected]", alarms[0]);

    //Assert NEW
    Assert.AreEqual(1, calls); //Check callback count to verify send was executed
    //OR 
    //Get rid of method interceptor line above and just verify mock object.
    mockEmail.Verify(m => m.Send(It.IsAny<string>(), It.IsAny<string>(),
                                 It.IsAny<string>(), It.IsAny<string>(),
                                 It.IsAny<bool>()), Times.Once());
}   

When I run this the Assert fails for "Calls" = 0 and Times.Once is zero as well. I believe my issue is due to the fact that "service" was created using "mockContext" (IEntityModel) which is needed to access alarm data, but the Send method is not part of the mockContext. If I mock up "mockEmail" and add my callback, I cannot figure out how to add it to mockContext.

How can I add both Mock IEmail AND Mock IEntityModel to the SAME Context or am I going about this all wrong?

Upvotes: 0

Views: 177

Answers (1)

GPW
GPW

Reputation: 2626

Just to clarify: you're creating a Mock but there's nothing to tell the code to use this mock for anything, from what I can see. you don't use that mock anywhere - just create it then later test it; no surprise it's not being called.

I think the issue you have is that your ExampleService is instantiating a new Email() class:

new Email().Send(from, to, subject, body, true);

whereas a better approach would be to supply an IEmail in the constructor:

    IEmail _email;
    //Constructor
    public ExampleService(IEntityModel model, IEmail email) : base(model) 
    { 
        _email = email;
    }

then use this later:

    public void SendEmailForAlarm(string email, Alarm alarm)
    {
        _email.Send(from, to, subject, body, true);
    }

you can then inject your Mock<IEmail> into your ExampleService and check that it was called as appropriate.

If it's difficult/impossible to set up DI properly for constructor injection, then you could have a public property which allows you to replace the IEmail in the class with another for Unit testing, but this is a bit messier:

public class ExampleService
{
    private IEmail _email = null;
    public IEmail Emailer
    {
        get
        {
            //if this is the first access of the email, then create a new one...
            if (_email == null)
            {
                _email = new Email();
            }
            return _email;
        }
        set
        {
            _email = value;
        }
    }

    ...
    public void SendEmailForAlarm(string email, Alarm alarm)
    {
        //this will either use a new Email() or use a passed-in IEmail
        //if the property was set prior to this call....
        Emailer.Send(from, to, subject, body, true);
    }
}

Then in your test:

    //Arrange
    var mockEmail = new Mock<IEmail>();         //NEW
    var mockContext = new Mock<IEntityModel>();     
    var mockSetAlarm = new Mock<DbSet<Alarm>>();
    var service = new ExampleService(mockContext.Object);
    //we set the Emailer object to be used so it doesn't create one..
    service.Emailer = mockEmail.Object;

if having a public property like this is a problem you could perhaps make it internal and use internalsvisibleto (google) to make it visible in your test project (though this would still be visible to other classes in the project containing the Service itself)..

Upvotes: 2

Related Questions