如何用NSubstitute伪造当前类的方法?

时间:2014-09-19 21:52:26

标签: c# unit-testing nunit nsubstitute

我是NSubstitue的新手(对于.NET中的单元测试来说还是个新手)。我想测试我的类是否将所有数据保存在不同文件中,例如每个条目StringDictionary。

说我有我的班级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?
        }

    }
}

或者我应该以不同的方式做到这一点?

1 个答案:

答案 0 :(得分:3)

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

使用其他依赖项作为模拟

它将封装数据存储,您将检查您的断言。我建议你阅读假,存根和模拟之间的区别。

  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>());
    }
    
  4. 这里最重要的是你不必测试私有方法调用。这是一个糟糕的做法!单元测试的全部内容都是测试公共合同,而不是私有方法,这些方法在时间上更容易改变。 (对不起,我想念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. 另一个解决方案是测试另一个工作单元,其中包含描述的工作单元(可能看起来像我的第一个解决方案)。
    4. UPD:阅读http://nsubstitute.github.io/help/partial-subs/以更好地了解NSubstitute。