模拟方法而不先模拟类
本文关键字:模拟类 方法 模拟 | 更新日期: 2023-09-27 18:09:55
我使用moq4来模拟我的UnitTests中的东西。我有一个类说TestClass,其中一个名为TestMethod的方法是他们的,我想测试。所以我的问题是我的TestMethod需要检查testlist,这是在我的TestClass。这样的:-
public Class TestClass
{
public readonly ISomeService _someService;
public bool TestProperty { get; private set; }
public List<string> testlist { get; private set; }
Public TestClass(ISomeService someService)
{
_someService = someService;
}
public async Task<bool> TestMethod(string sd)
{
if(TestProperty) // here how can i control this property in UnitTest.
return false;
testlist.contains(sd); // check in this list but list will be null.
}
public async Task<List<string>> SetTestListAndProperty()
{
testlist.Add("2");
testlist.Add("3");
TestProperty = false;
}
}
现在在testmethodtests中,我模拟了ISomeService并传递给TestClass构造器。
var someservicemock = new Mock<ISomeService>();
var testclassobj = new TestClass(someservicemock.Object);
现在我调用了My TestMethod
result = await testclassobj.TestMethod("id"); // it is throwing the exception that testlist is null.
//我还想在TestProperty为false时测试TestMethod。我怎么设置这个属性呢?它没有公共setter。它是由其他方法(SetTestListAndProperty)设置的。
所以我的问题是我可以模拟TestClass testlist而不模拟TestClass吗?如果这种方式是错误的,请让我知道,或者你知道任何变通,然后也。欢呼声
我认为你的问题归结为:当方法在测试依赖于由其他方法设置的内部状态时该怎么办?
下面是一个例子:
class MyCollection<T> : IEnumerable<T>
{
private List<T> _list = new List<T>();
private Receiver _receiver;
public MyCollection()
{
_receiver = receiver;
}
public void Add(T item)
{
_list.Add(item);
}
public void RemoveAndNotify(T item)
{
_list.Remove(item);
_receiver.Notify(item);
}
public IEnumerator<T> GetEnumerator()
{
return _list.GetEnumerator();
}
}
(顺便说一下,称你的类型为TestClass
, TestProperty
和testlist
并没有真正的帮助——读者不知道这些东西是如何相互联系的。它几乎和该死的Foo, Bar和Baz一样令人困惑。用猫、狗、人等具体而有意义的名字来代替。
- 那么,假设您想对
- 另外,我如何验证该项目确实已从内部列表中删除?我是否必须以某种方式检索内部_list并检查其内容(通过注入,反射等)?不!
RemoveAndNotify
方法进行单元测试。但是你不能在不添加一个项目的情况下删除一个项目!这是否意味着我必须模拟内部_list
来插入虚拟数据?不!我认为你的问题的关键是:单元测试不一定测试一个方法!单元测试测试行为的最小可能单元。
行为单元可能涉及调用一个或多个方法。下面是我如何单元测试移除项目相关的行为。我们想测试两种行为
- 我们想测试的是,收集不再有一个项目后,我们已经删除了。
- 我们想测试当一个项目被删除时接收者是否得到通知。
[Fact]
public void RemoveAndNotify_RemovesItem()
{
//Arrange
var mockReceiver = ...
var collection = new MyCollection<int>(mockReceiver.Object);
collection.Add(5);
//Act
collection.Remove(5);
//Assert
//internally calls GetEnumerator to verify that the collection no longer contains "5"
Assert.AreEqual(Enumerable.Empty<int>(), collection);
}
[Fact]
public void RemoveAndNotify_NotifiesReceiver()
{
//Arrange
var mockReceiver = ...
var collection = new MyCollection<int>(mockReceiver.Object);
collection.Add(5);
//Act
collection.Remove(5);
//Assert
mockReceiver.Verify(rec => rec.Notify(5), Times.Once());
}
如您所见,第一个单元测试调用了类上的三个方法(Add
, Remove
和GetEnumerator
),以测试一个行为单元。底线:忘记"测试中的方法",而考虑"测试中的行为"。你是在测试行为,而不是方法。
两件事:
-
我不建议你模仿所有的。在集合的情况下,使用真实的(不是模拟的)对象是完全可以的。当你嘲笑这个列表的时候,有什么好处呢?通过检查
testlist
属性,您可以非常轻松地测试所有内容。 -
(这是一个边注,但也解决了你的第一个评论。)尽量使你的类型不可变。您可以找到许多关于这个主题的文章。搜索一下就知道了。这样做的好处是,你不必处理
NullReferenceException
和一堆其他场景。对于您的示例,这意味着您应该在构造函数中初始化testlist
。
我将创建另一个构造函数,它将列表作为参数,以便您可以在测试中创建并填充带有测试数据的列表,然后将其提供给被测试的类。
public TestClass(ISomeService service, IList<string> list) {
_someService = service;
testList = list;
}
使用这个构造函数,testList不会为空,除非你传入null。
我知道有些人不喜欢添加只用于测试的方法。如果不想公开仅用于测试的另一个构造函数,可以将新构造函数标记为internal
,并仅向测试程序集公开内部结构。
internal TestClass(ISomeService service, IList<string> list) {
...
}
项目AssemblyInfo.cs
:
[assembly: InternalsVisibleTo("your test assembly");
编辑:用于属性。如果你有不暴露setter的属性,你可以将属性标记为virtual
,并使用Moq的SetupGet
方法来设置属性的返回值。
public class ClassUnderTest
{
public virtual string TestProperty { get; private set; }
}
在你的测试中:
public void SomeTest() {
var mock = new Mock<ClassUnderTest>();
mock.SetupGet(m => m.TestProperty).Returns("hi");
var result = mock.Object.TestProperty;
Assert.That(result, Is.EqualTo("hi"); // should pass
}