【问题标题】:How can I fake call to current class' method with NSubstitute?如何使用 NSubstitute 伪造对当前类方法的调用?
【发布时间】:2014-11-14 13:52:42
【问题描述】:

我是 NSubstitue 的新手(对 .NET 中的单元测试也很陌生)。我想测试我的班级是否为每个条目将所有数据保存在不同的文件中,例如字符串字典。

假设我有我的课DataManipulation.cs

using System;
using System.Collections;
using System.Collections.Specialized;

namespace ApplicationName
{
    // interface for NSubstitute
    public interface IManipulator
    {
        void saveAllData();
        void saveEntry(string entryKey, string entryValue);
    }

    public class DataManipulator : IManipulator
    {
        protected StringDictionary _data {get; private set;}

        public DataManipulator()
        {
            _data = new StringDictionary();
        }

        public void addData(string name, string data)
        {
            this._data.Add(name, data);
        }

        public void saveAllData()
        {
            // potential implementation - I want to test this
            foreach (DictionaryEntry entry in this._data)
            {
                this.saveEntry(entry.Key.ToString(), entry.Value.ToString());
            }
        }

        public void saveEntry(string entryKey, string entryValue)
        {
            // interact with filesystem, save each entry in its own file
        }
    }
}

我要测试的内容:当我调用DataManipulator.saveAllData() 时,它会将每个_data 条目保存在一个单独的文件中——这意味着它运行saveEntry 的次数等于_data.Count。 NSubstitute 可以吗?

每次我尝试将 DataManipulation 用作测试对象并单独用作模拟时 - 当我运行 Received() 时,我得到的信息是没有进行任何调用。

我要使用的 NUnit 测试模板:

using System;
using System.Collections.Generic;

using NUnit.Framework;
using NSubstitute;

namespace ApplicationName.UnitTests
{
    [TestFixture]
    class DataManipulatorTests
    {
        [Test]
        public void saveAllData_CallsSaveEntry_ForEachData()
        {

            DataManipulator dm = new DataManipulator();
            dm.addData("abc", "abc");
            dm.addData("def", "def");
            dm.addData("ghi", "ghi");

            dm.saveAllData();

            // how to assert if it called DataManipulator.saveEntry() three times?
        }

    }
}

或者我应该以不同的方式来做吗?

【问题讨论】:

    标签: c# unit-testing nunit nsubstitute


    【解决方案1】:

    根据一些 OOP 原则和测试需要,您必须引入依赖项或一些构造来创建适合测试的“接缝”。

    使用另一个依赖项作为模拟

    它将封装数据存储,您将根据它检查您的断言。我建议您阅读 fake、stub 和 mock 之间的区别。

    1. 添加新的存储接口和实现。

      public interface IDataStorage
      {
          void Store(string key, string value);
      }
      
      public class DataStorage : IDataStorage
      {
          public void Store(string key, string value)
          {
              //some usefull logic
          }
      }
      
    2. 在您的 Manipulator 实现中将其用作依赖项(并通过构造函数注入)

      public class DataManipulator : IManipulator
      {
          protected IDataStorage _storage { get; private set; }
          protected StringDictionary _data { get; private set; }
      
          public DataManipulator(IDataStorage storage)
          {
              _storage = storage;
              _data = new StringDictionary();
          }
      
          public void addData(string name, string data)
          {
              this._data.Add(name, data);
          }
      
          public void saveAllData()
          {                
              // potential implementation - I want to test this
              foreach (DictionaryEntry entry in this._data)
              {
                  this.saveEntry(entry.Key.ToString(), entry.Value.ToString());
              }
          }
      
          public void saveEntry(string entryKey, string entryValue)
          {
              _storage.Store(entryKey, entryValue);
          }
      }
      
    3. 测试一下

      [Test]
      public void saveAllData_CallsSaveEntry_ForEachData()
      {
      
          var dataStorageMock = Substitute.For<IDataStorage>();
          DataManipulator dm = new DataManipulator(dataStorageMock);
          dm.addData("abc", "abc");
          dm.addData("def", "def");
          dm.addData("ghi", "ghi");
      
          dm.saveAllData();
      
          dataStorageMock.Received().Store("abc", "abc");
          dataStorageMock.Received().Store("def", "def");
          dataStorageMock.Received().Store("ghi", "ghi");
          //or 
          dataStorageMock.Received(3).Store(Arg.Any<string>(), Arg.Any<string>());
      }
      

    这里最重要的是您不必测试私有方法调用。这是一个不好的做法!单元测试是关于公共合约的测试,而不是私有方法的测试,私有方法在时间上更容易改变。 (抱歉,我错过了 saveEntry(..) 是公开的)

    将 DataManipulator 用作模拟

    我认为这不是一个好主意,但是...使用 NSubstitute 做到这一点的唯一方法是使方法 saveEntry 虚拟:

    public virtual void saveEntry(string entryKey, string entryValue)
    {
    //something useful
    }
    

    并测试它:

    [Test]
    public void saveAllData_CallsSaveEntry_ForEachData()
    {
    
        var dm = Substitute.For<DataManipulator>();
        dm.addData("abc", "abc");
        dm.addData("def", "def");
        dm.addData("ghi", "ghi");
    
        dm.saveAllData();
    
        dm.Received(3).saveEntry(Arg.Any<string>(), Arg.Any<string>());
    }
    

    仅仅为了测试需要做一些虚拟方法的需要可能不是很有吸引力,但是..

    1. 只要您的测试也是业务逻辑的客户端,就可以接受它。
    2. 在这种情况下,可以使用一些“繁重”的测试框架,例如 MS Fakes,但这似乎有点过头了。
    3. 另一种解决方案是测试另一个工作单元,其中涵盖了描述的一个(可能看起来像我的第一个解决方案)。

    UPD:阅读 http://nsubstitute.github.io/help/partial-subs/ 以更好地了解 NSubstitute。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-16
      • 1970-01-01
      • 2016-09-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-07-17
      相关资源
      最近更新 更多