我正在编写一个依赖于扩展方法结果的测试,但我不希望该扩展方法的未来失败会破坏此测试。模拟该结果似乎是显而易见的选择,但 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可能会蒙上眼睛并醉酒,但我们的预算不会很快增加。
答案 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)
答案 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中使用FirstOrDefault
,Single
或任何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
。