繁体   English   中英

如何模拟非虚拟方法?

[英]How to mock non virtual methods?

[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方法必须是虚拟的才能模拟它。 有没有办法模拟非虚拟方法?

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

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

两者都是商业的,即使 JustMock 有一个精简版,模拟非虚拟方法也仅适用于商业版本。 正如评论中所指出的,微软的研究中有一些东西,在Pex 和 Moles项目中

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

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

必须使用虚拟方法进行模拟的替代方法是使用接口。 通过这种方式,您可以模拟整个依赖项。

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

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

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

正如@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);
    }

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

我很久以前就看到了这个问题,并意识到我想创建一些开源的东西来解决这个问题。 所以它准备好了 - 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);
}

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM