【问题标题】:Moq How do you(Can you?) add two different (incompatible) interfaces to the same context?Moq 你如何(你能吗?)将两个不同的(不兼容的)接口添加到同一个上下文中?
【发布时间】:2019-03-01 20:50:48
【问题描述】:

我的上下文是我的数据模型的模拟 我在另一个名为“Email”的类中有一个方法“Send” 我的服务类使用模拟数据模型。 我的服务类“SendEmailForAlarm”中的一个方法从 Mock 数据模型访问数据,然后调用电子邮件类中的“Send”方法。 问题:如何让我的电子邮件类中的“发送”方法包含在我的模拟中?

相关代码:

//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 方法查询 EntityModel.Alarms DBSet 并从检索到的对象中提取信息。然后调用 Email 类中的 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 方法上放置一个拦截器,这样我就可以计算它被执行了多少次,或者让模拟验证验证它被调用了 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());
}   

当我运行此命令时,“Calls”= 0 且 Times.Once 也为零,Assert 失败。 我相信我的问题是由于“服务”是使用访问警报数据所需的“mockContext”(IEntityModel)创建的,但 Send 方法不是 mockContext 的一部分。如果我模拟“mockEmail”并添加我的回调,我无法弄清楚如何将它添加到 mockContext。

我怎样才能将 Mock IEmail 和 Mock IEntityModel 添加到相同的上下文中,否则我会错了吗?

【问题讨论】:

  • SendEmailForAlarm 你有new Email() 这意味着你不能对SendEmailForAlarm 进行单元测试。您需要将IEmail 传递到服务中,以便您可以传递真实的或模拟的。

标签: c# unit-testing moq


【解决方案1】:

澄清一下:您正在创建一个 Mock,但据我所见,没有没有可以告诉代码使用这个 mock 做任何事情。您不会在任何地方使用该模拟 - 只需创建它然后稍后对其进行测试;毫不奇怪,它没有被调用。

我认为您遇到的问题是您的 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&lt;IEmail&gt; 注入您的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) 使其在您的测试项目中可见(尽管这对于其他类仍然可见)包含服务本身的项目)..

【讨论】:

  • 感谢 GPW,我决定采用您在重载构造函数中提供 IEmail 的建议。这将适用于我的预期目的。
  • 您不必每秒使用 IoC 容器和 DI。您可以将其提供给构造函数(手动注入)。 +1
  • @CodingYoshi 是的,但它确实在许多方面让生活变得更轻松......必须手动构建东西,这使得以后很难在不更改大量代码区域的情况下更改东西的组合。
猜你喜欢
  • 2022-07-01
  • 2017-05-15
  • 2023-01-01
  • 2011-11-25
  • 2015-12-02
  • 2014-02-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多