模拟方法而不先模拟类

本文关键字:模拟类 方法 模拟 | 更新日期: 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, TestPropertytestlist并没有真正的帮助——读者不知道这些东西是如何相互联系的。它几乎和该死的Foo, Bar和Baz一样令人困惑。用猫、狗、人等具体而有意义的名字来代替。

    那么,假设您想对RemoveAndNotify方法进行单元测试。但是你不能在不添加一个项目的情况下删除一个项目!这是否意味着我必须模拟内部_list来插入虚拟数据?不!
  1. 另外,我如何验证该项目确实已从内部列表中删除?我是否必须以某种方式检索内部_list并检查其内容(通过注入,反射等)?不!

我认为你的问题的关键是:单元测试不一定测试一个方法!单元测试测试行为的最小可能单元

行为单元可能涉及调用一个或多个方法。下面是我如何单元测试移除项目相关的行为。我们想测试两种行为

  1. 我们想测试的是,收集不再有一个项目后,我们已经删除了。
  2. 我们想测试当一个项目被删除时接收者是否得到通知。


[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, RemoveGetEnumerator),以测试一个行为单元

底线:忘记"测试中的方法",而考虑"测试中的行为"。你是在测试行为,而不是方法。

两件事:

  1. 我不建议你模仿所有的。在集合的情况下,使用真实的(不是模拟的)对象是完全可以的。当你嘲笑这个列表的时候,有什么好处呢?通过检查testlist属性,您可以非常轻松地测试所有内容。

  2. (这是一个边注,但也解决了你的第一个评论。)尽量使你的类型不可变。您可以找到许多关于这个主题的文章。搜索一下就知道了。这样做的好处是,你不必处理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
}