【问题标题】:How to mock non virtual methods?如何模拟非虚拟方法?
【发布时间】:2012-07-29 02:25:28
【问题描述】:
[TestMethod]
public void TestMethod1()
{
    var mock = new Mock<EmailService>();
    mock.Setup(x => x.SendEmail()).Returns(true);
    var cus = new Customer();
    var result = cus.AddCustomer(mock.Object);
    Assert.IsTrue(result);
}

public class Customer
{
    public bool AddCustomer(EmailService emailService)
    {
        emailService.SendEmail();
        Debug.WriteLine("new customer added");
        return true;
    }
}

public class EmailService
{            
    public virtual bool SendEmail()
    {
        throw  new Exception("send email failed cuz bla bla bla");
    }
}

EmailService.SendEmail 方法必须是虚拟的才能模拟它。有没有办法模拟非虚拟方法?

【问题讨论】:

标签: c# .net moq


【解决方案1】:

Moq 不能模拟类上的非虚拟方法。要么使用其他模拟框架,例如 Type mock Isolator,它实际上将 IL 编织到你的程序集中,要么在 EmailService 上放置一个接口并模拟它。

【讨论】:

  • 我知道这是一篇很老的帖子,但有一些类似的问题。当实际应用程序运行时,将方法转换为虚拟方法有什么缺点吗?
  • 我建议在 SO 中提出一个单独的问题。
【解决方案2】:

模拟非虚拟方法涉及使用低级分析器 API。目前我认为唯一可用的选项是:

两者都是商业的,即使 JustMock 有一个精简版,模拟非虚拟方法也只能在商业版本中使用。 正如 cmets 所指出的,在项目 Pex and Moles

中有一些来自微软研究的东西

【讨论】:

  • 我认为您可以将 Microsoft Moles/Pex/Fakes 添加到该列表中。作为奖励,它是免费的。
  • 如果你有 Visual Studio 的企业版,Microsoft Fakes 是“免费的”。
【解决方案3】:

2020 年 7 月更新: pose已弃用,目前推荐的是: https://github.com/riezebosch/Unmockable 感谢@Customizer 指出。

我还在查看https://github.com/Serg046/AutoFake,这从@Serg046 看来是可行的 —— 使用pose。允许您替换任何方法,包括静态或非虚拟方法。相当新的项目,但完全开源 MIT 许可证。 https://github.com/tonerdo/pose

【讨论】:

【解决方案4】:

必须使用虚拟方法进行模拟的替代方法是使用接口。这样你就可以模拟出整个依赖。

public interface IEmailService
{
    bool SendEmail();
    // etc...
}

public class EmailService : IEmailService
{
    //...
}

现在您可以创建接口IEmailService 的模拟来模拟它的任何方法。当然,您必须在适当的情况下将包含EmailService 对象的变量类型更改为IEmailService

【讨论】:

  • 我认为使用这种技术很好,使用接口肯定有优势。但是,如果您要在两个选项之间评估非虚拟方法是否可模拟:1) 使其成为虚拟方法,2) 将其用作接口的一部分,那么 2 比 1 有什么优势?即使您选择 2,那么您实际上是在使该方法成为虚拟方法,因为它现在是接口的一部分。
【解决方案5】:

我很久以前就看到了这个问题,并意识到我想创建一些开源的东西来解决这个问题。所以它准备好了 - AutoFake。最令人兴奋的是,它不需要任何疯狂的 CLR Profiler API。它只是一个普通的 .NET 包,仅此而已。 Bellow 是您可以使用该库执行的操作的示例:

public class Calendar
{
    public static DateTime Yesterday => DateTime.Now.AddDays(-1);
    internal Task<DateTime> AddSomeMinutesAsync(DateTime date) => Task.Run(() => AddSomeMinutes(date));
    public static DateTime AddSomeMinutes(DateTime date) => date.AddMinutes(new Random().Next(1, 10));
}

[Fact]
public void Yesterday_SomeDay_ThePrevDay()
{
    var fake = new Fake<Calendar>();

    var sut = fake.Rewrite(() => Calendar.Yesterday);
    sut.Replace(() => DateTime.Now).Return(new DateTime(2016, 8, day: 8));

    Assert.Equal(new DateTime(2016, 8, 7), sut.Execute());
}

[Fact]
public async Task AddSomeMinutesAsync_SomeDay_MinutesAdded()
{
    var randomValue = 7;
    var date = new DateTime(2016, 8, 8, hour: 0, minute: 0, second: 0);
    var fake = new Fake<Calendar>();

    var sut = fake.Rewrite(f => f.AddSomeMinutesAsync(date));
    sut.Replace((Random r) => r.Next(1, 10)) // Arg.Is<int>(i => i == 10) is also possible
                           // r.Next(1, 11) fails with "Expected - 11, actual - 10"
        .ExpectedCalls(1) // c => c > 1 fails with "Actual value - 1"
        .Return(randomValue);

    Assert.Equal(date.AddMinutes(randomValue), await sut.Execute());
}

[Fact]
public void AddSomeMinutes_SomeDay_EventsRecorded()
{
    var events = new List<string>();
    var fake = new Fake<Calendar>();

    var sut = fake.Rewrite(() => Calendar.AddSomeMinutes(new DateTime(2016, 8, 8)));

    sut.Prepend(() => events.Add("The first line"));
    sut.Prepend(() => events.Add("The line before AddMinutes(...) call"))
        .Before((DateTime date) => date.AddMinutes(Arg.IsAny<int>()));

    sut.Append(() => events.Add("The line after new Random() call"))
        .After(() => new Random());
    sut.Append(() => events.Add("The last line"));

    sut.Execute();
    Assert.Equal(new[]
        {
            "The first line",
            "The line after new Random() call", // indeed, this call is earlier
            "The line before AddMinutes(...) call",
            "The last line"
        },
        events);
}

【讨论】:

    【解决方案6】:

    正如@aqwert 和@Felice 在使用Typemock Isolator 时所写,可以(而且非常容易)模拟非虚拟方法而无需添加或更改任何代码,例如:

    [TestMethod,Isolated]
        public void TestMethod1()
        {
            var mock = Isolate.Fake.Instance<EmailService>();
            Isolate.WhenCalled(() => mock.SendEmail()).WillReturn(true);
            var cust = new Customer();
            var result = cust.AddCustomer(mock);
            Assert.IsTrue(result);
        }
    

    如您所见,我创建的测试与您尝试创建的测试相似。

    【讨论】:

    • 是商业包
    【解决方案7】:

    模拟非虚拟方法的唯一方法是模拟用于使用非虚拟方法实现该类的接口。下面是例子。

    public interface IEmployee
    {
        DateTime GetDateofJoining(int id);
    }
    
    public class Employee
    {
        public DateTime GetDateofJoining(int id)
        {
            return DateTime.Now;
        }
    }
    
        public class Program
    {
        static void Main(string[] args)
        {
            var employee = new Mock<IEmployee>();
            employee.Setup(x => x.GetDateofJoining(It.IsAny<int>())).Returns((int x) => DateTime.Now);
    
            Console.WriteLine(employee.Object.GetDateofJoining(1));
            Console.ReadLine();
        }
    }
    

    【讨论】:

      【解决方案8】:

      作为一种解决方法,您可以不使用方法本身,而是创建虚拟包装方法

      public class EmailService
      {     
          protected virtual void SendEmailReal(){
              throw  new Exception("send email failed cuz bla bla bla");
          }
              
          public void bool SendEmail()
          {
              return SendEmailReal();
          }
      }
      

      然后在测试类中覆盖它:

      abstract class TestEmailService: EmailService{
      
           public abstract override  bool SendEmailReal();
      }
      

      测试方法本身:

      [TestMethod]
      public void TestMethod1()
      {
          var mock = new Mock<TestEmailService>();
          mock.Setup(x => x.SendEmailReal()).Returns(true);
          var cus = new Customer();
          var result = cus.AddCustomer(mock.Object);
          Assert.IsTrue(result);
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2013-05-01
        • 1970-01-01
        • 1970-01-01
        • 2011-09-25
        • 1970-01-01
        • 2010-11-07
        • 1970-01-01
        相关资源
        最近更新 更多