如何使用Moq模拟扩展方法?

时间:2009-02-18 17:48:01

标签: c# mocking moq extension-methods

我正在编写一个依赖于扩展方法结果的测试,但我不希望该扩展方法的未来失败会破坏此测试。模拟该结果似乎是显而易见的选择,但 Moq似乎没有提供覆盖静态方法的方法(扩展方法的要求)。与Moq.Protected和Moq.Stub有类似的想法,但它们似乎没有为这种情况提供任何东西。我错过了什么,或者我应该以不同的方式解决这个问题?

这是一个简单的示例,通常 “对非覆盖成员的期望无效” 失败。这是一个需要模拟扩展方法的坏例子,但应该这样做。

public class SomeType {
    int Id { get; set; }
}

var ListMock = new Mock<List<SomeType>>();
ListMock.Expect(l => l.FirstOrDefault(st => st.Id == 5))
        .Returns(new SomeType { Id = 5 });

至于任何可能暗示我使用Isolator的TypeMock迷们:我很欣赏这项努力,因为看起来TypeMock可能会蒙上眼睛并醉酒,但我们的预算不会很快增加。

7 个答案:

答案 0 :(得分:58)

扩展方法只是伪装的静态方法。像Moq或Rhinomocks这样的模拟框架只能创建对象的模拟实例,这意味着无法模拟静态方法。

答案 1 :(得分:18)

如果您可以更改扩展方法代码,那么您可以像这样编码以便能够测试:

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

public static class MyExtensions
{
    public static IMyImplementation Implementation = new MyImplementation();

    public static string MyMethod(this object obj)
    {
        return Implementation.MyMethod(obj);
    }
}

public interface IMyImplementation
{
    string MyMethod(object obj);
}

public class MyImplementation : IMyImplementation
{
    public string MyMethod(object obj)
    {
        return "Hello World!";
    }
}

因此,扩展方法只是实现接口的包装。

(你可以只使用没有扩展方法的实现类,这些方法是一种语法糖。)

您可以模拟实现接口并将其设置为扩展类的实现。

public class MyClassUsingExtensions
{
    public string ReturnStringForObject(object obj)
    {
        return obj.MyMethod();
    }
}

[TestClass]
public class MyTests
{
    [TestMethod]
    public void MyTest()
    {
        // Given:
        //-------
        var mockMyImplementation = new Mock<IMyImplementation>();

        MyExtensions.Implementation = mockMyImplementation.Object;

        var myObject = new Object();
        var myClassUsingExtensions = new MyClassUsingExtensions();

        // When:
        //-------
        myClassUsingExtensions.ReturnStringForObject(myObject);

        //Then:
        //-------
        // This would fail because you cannot test for the extension method
        //mockMyImplementation.Verify(m => m.MyMethod());

        // This is success because you test for the mocked implementation interface
        mockMyImplementation.Verify(m => m.MyMethod(myObject));
    }
}

答案 2 :(得分:15)

我知道这个问题大约一年没有激活,但微软发布了一个框架来处理这个名为Moles的问题。

以下是一些教程:

  • DimeCasts.net
  • Nikolai Tillman's Tutorial

  • 答案 3 :(得分:12)

    我为我需要模拟的扩展方法创建了一个包装类。

    public static class MyExtensions
    {
        public static string MyExtension<T>(this T obj)
        {
            return "Hello World!";
        }
    }
    
    public interface IExtensionMethodsWrapper
    {
        string MyExtension<T>(T myObj);
    }
    
    public class ExtensionMethodsWrapper : IExtensionMethodsWrapper
    {
        public string MyExtension<T>(T myObj)
        {
            return myObj.MyExtension();
        }
    }
    

    然后,您可以使用IOC容器模拟测试中的包装器方法和代码。

    答案 4 :(得分:4)

    对于扩展方法,我通常使用以下方法:

    public static class MyExtensions
    {
        public static Func<int,int, int> _doSumm = (x, y) => x + y;
    
        public static int Summ(this int x, int y)
        {
            return _doSumm(x, y);
        }
    }
    

    它允许相当容易地注入_doSumm。

    答案 5 :(得分:0)

    最好的办法是为具有扩展方法的类型提供自定义实现,例如:

    [Fact]
    public class Tests
    {
        public void ShouldRunOk()
        {
            var service = new MyService(new FakeWebHostEnvironment());
    
            // Service.DoStuff() internally calls the SomeExtensionFunction() on IWebHostEnvironment
            // Here it works just fine as we provide a custom implementation of that interface
            service.DoStuff().Should().NotBeNull();
        }
    }
    
    public class FakeWebHostEnvironment : IWebHostEnvironment
    {
        /* IWebHostEnvironment implementation */
    
        public bool SomeExtensionFunction()
        {
            return false;
        }
    }
    

    答案 6 :(得分:-2)

    模拟数据,而不是扩展方法

    我没有嘲笑扩展方法,但是您可以“模拟”数据。因此,当您在IEnumerable中使用FirstOrDefaultSingle或任何Linq扩展名时,可以使用以下方法:

    using Microsoft.VisualStudio.TestTools.UnitTesting;
    using Moq;
    using System.Collections.Generic;
    using System.Linq;
    
    namespace Tests
    {
        public class SomeType
        {
            public int Id { get; set; }
        }
    
        [TestClass()]
        public class Class1
        {
            [TestMethod]
            public void Test()
            {
                //Prepare a fake or mock data.
                var myMockData = new List<SomeType>
                {
                    new SomeType
                    {
                        Id=3
                    },
                    new SomeType
                    {
                        Id=4
                    },
                    new SomeType
                    {
                        Id=5
                    }
                };
    
                var ListMock = new Mock<List<SomeType>>();
    
                //His old code
                //ListMock.Setup(l => l.FirstOrDefault(st => st.Id == 5)).Returns(new SomeType { Id = 5 });
    
                //I'll setup it to when GetEnumerator it return the myMockData list.
                ListMock.As<IEnumerable<SomeType>>().Setup(m => m.GetEnumerator()).Returns(myMockData.GetEnumerator());
    
                //So now I can use ListMock.Object and use FirstOrDefault normally.
                var result = ListMock.Object.FirstOrDefault(st => st.Id == 5);
    
                //MS Test Assert
                Assert.AreEqual(expected: 5, actual: result.Id);
    
                //OR
    
                //Nunit way
                //Assert.That(result, Is.EqualTo(5));
            }
        }
    }
    

    Expect已过时,所以我改用Setup