Moq Как вы (можете?) Добавить два разных (несовместимых) интерфейса в один и тот же контекст? - PullRequest
0 голосов
/ 01 марта 2019

Мой контекст - это макет моей модели данных. У меня есть метод «Отправить» в другом классе с именем «Электронная почта». Мой класс обслуживания использует макетированную модель данных.Метод в моем классе обслуживания «SendEmailForAlarm» получает доступ к данным из модели данных Mock, а затем вызывает метод «Отправить» в классе электронной почты.Вопрос: Как я могу получить метод «Отправить» в своем классе электронной почты для включения в мою макет?

Код восстановления:

//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;
}

Следующий модульный тест проходит, но не записывается вПодтверждение того, что электронная почта была действительно отправлена ​​(или вызвана) Метод service.SendEmailForAlarm запрашивает DBSet EntityModel.Alarms и извлекает информацию из полученного объекта.Затем он вызывает метод Send в классе электронной почты и возвращает 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("test@domain.com", alarms[0]);

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

Что я хотел бы сделать, так это поместить перехватчик в метод Email.Send, чтобы я мог подсчитать, сколько раз он был выполнен илииметь mock verify validate он вызывался n раз (в этом примере один раз).

[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("test@domain.com", 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());
}   

Когда я запускаю это, Assert завершается неудачно для "Calls" = 0 и Times.Once также равно нулю.Я считаю, что моя проблема связана с тем, что «сервис» был создан с использованием «mockContext» (IEntityModel), который необходим для доступа к данным тревоги, но метод Send не является частью mockContext.Если я создаю макет «mockEmail» и добавляю свой обратный вызов, я не могу понять, как добавить его в mockContext.

Как я могу добавить оба Mock IEmail И Mock IEntityModel в тот же контекст, или я собираюсь об этом всенеправильно?

1 Ответ

0 голосов
/ 02 марта 2019

Просто чтобы уточнить: вы создаете макет, но нет ничего , чтобы сообщить коду использовать этот макет для всего, что я вижу.вы нигде не используете этот макет - просто создайте его, а потом протестируйте;не удивительно, что его не вызывают.

Я думаю, что у вас есть проблема в том, что ваш ExampleService создает новый класс Email():

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

, тогда как лучшим подходом было быукажите IEmail в конструкторе:

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

, а затем используйте это позже:

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

затем вы можете вставить свой Mock<IEmail> в ExampleService и убедиться, что он былвызывается соответствующим образом.

Если трудно / невозможно правильно настроить DI для инжектора конструктора, то у вас может быть открытое свойство, которое позволяет вам заменить IEmailв классе с другим для модульного тестирования, но это немного сложнее:

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);
    }
}

Затем в вашем тесте:

    //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;

, если свойство public, как этоВозможно, вы могли бы сделать это internal и использовать internalsvisibleto (google), чтобы сделать его видимым в вашем тестовом проекте (хотя это все равно будет видно другим классам в проекте, содержащем саму Службу) ..

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...