【问题标题】:Refactoring C# code for mocking to eliminate database dependencies重构用于模拟的 C# 代码以消除数据库依赖关系
【发布时间】:2020-09-28 19:21:42
【问题描述】:

我有一个看起来像这样的测试方法:

public void TestConversion()
{
   BuildMyNode(inputDocument)
}

public override MyXMLDocumentObject BuildMyNode(XmlDocument inputDocument)
{
   Dictionary<string, long> myIdMap = await GetMyIdMap(inputDocument);
}


public async Task<Dictionary<string, long>> GetMyIdMap(XmlDocument inputDocument)
{
   Dictionary<string, long> myIdMap = await MyDataService.GetMyMapAsync(myIds, _cancellationToken);
   return myIdMap;
}


public async Task<Dictionary<string, long>> GetMyMapAsync(XmlNodeList myIds, CancellationToken cancellationToken)
{
    var idMap = new Dictionary<string, long>();


    using (SqlConnection connection = new SqlConnection())
    {
        //Build sql command
        //Convert DataReader to idMap
    }

    return idMap;
}

所以我的测试依赖于我的数据库,这不好。如果我从 GetMyMapAsync 构建一个接口,然后在实现该接口的 TestConversion() 中实例化一些模拟数据,我将解决问题。但是,似乎我必须将其作为参数传递给 BuildMyNode、GetMyIdMap,然后再传递给 GetMyMapAsync。

所以我中间的两种方法看起来像:

public override MyXMLDocumentObject BuildMyNode(XmlDocument inputDocument, IGetMap getMap)
{
   Dictionary<string, long> myIdMap = await GetMyIdMap(inputDocument);
}


public async Task<Dictionary<string, long>> GetMyIdMap(XmlDocument inputDocument, IGetMap getMap)
{
   Dictionary<string, long> myIdMap = await getMap.GetMyMapAsync(myIds, _cancellationToken);
   return myIdMap;
}

有没有更好的方法来做到这一点?

我尝试重构,添加一个带有我的 GetMyMapAsync 方法签名的委托。我在想我可以在不同的构造函数中通过委托的模拟版本。但是,通过调用堆栈向后工作比我最初意识到的要复杂得多。使用工厂模式和多个抽象类。这是我的代码的修订版:

//1
namespace Namespace.UnitTests
{
    public class MyTests
    {
        [Fact]
        public void TestConversion()
        {
            IDataProcessor _myDataProcessor = MyFactory.GetInstance(Type_2, settings);            

            XmlDocument expectedDocument = new XmlDocument();
            expectedDocument.LoadXml(expectedData);            
            
            XmlDocument myDocument = _dataProcessor.ProcessItem(data);            
            Equal(myDocument.Documents[0].OuterXml, expectedDocument.OuterXml);            
        } 
    }
}

//2
namespace Namespace.Factory
{
    public static class MyFactory
    {

        public static IDataProcessor GetInstance(MyEntityType type, MySettings settings)
        {
            IDataProcessor _processor;
            if (MyEntityType.Type_1 == type)
            {
                _processor = new MyProcessor1(settings);
            }
            else if (MyEntityType.Type_2 == type)
            {
                _processor = new MyProcessor2(settings);
            }
        }
    }
}


//3
namespace Namespace.DataProcessor
{
    public class MyProcessor2 : BaseDataProcessor1
    {
        public MyProcessor2(MySettings settings)
            : base(settings)                
        {
            //Do some stuff here
        }
    }
}

//4
namespace Namespace.Base
{
    public abstract class BaseDataProcessor1 : MyDataProcessor
    {
        public BaseDataProcessor1(MySettings settings)
            : base(settings)                    //instantiate
        {
        }
    }
}


//5
namespace Namespace.Base
{
    public abstract class MyDataProcessor : IDataProcessor
    {
        public MyDataProcessor(MySettings settings)
        {
            this.settings = settings;
            this.myDataService = new MyDataService(Settings);   //instantiate
        }
    }
}

//6
namespace Namespace.Data
{
    
    public  delegate Task<Dictionary<string, long>> GetDictionary(XmlNodeList orgMasterIds, CancellationToken cancellationToken);

    public class MyDataService 
    {
        //Original constructor
        public MyDataService(MySettings settings)
        {
            _settings = settings.NotNull();
            GetDictionaryMethod = GetMyMapAsync;  //assign delegate
        }

        //New constructor for testing purposes
        public MyDataService(MySettings settings, GetDictionary myDictionary )
        {
            _settings = settings.NotNull();
            GetDictionaryMethod = myDictionary;  //assign delegate
        }

        public async Task<Dictionary<string, long>>  GetMyMapAsync (XmlNodeList myIds, CancellationToken cancellationToken)
        {
            var idMap = new Dictionary<string, long>();


            using (SqlConnection connection = new SqlConnection())
            {
                //Build sql command
                //Convert DataReader to idMap
            }

            return idMap;
        }

        
    }
}

【问题讨论】:

  • 你为什么不直接创建类女巫接受 IGetMap 接口的构造函数对象,而不是简单地调用使用该对象的 BuildMyNode 或 GetMyIdMap 而不是将其作为参数传递?
  • 我想知道你展示的方法除了委派给另一个方法什么都不做,唯一发生“事情”的方法是GetMyMapAsync,它从数据库加载数据。如果是这种情况,那么我将使用实际数据库对其进行测试,以 100% 确保代码正常工作。如果不是这种情况,并且您没有显示一些逻辑,那么我会将该逻辑提取到专用类中并重组其他类,因此我的重要逻辑在设计和运行时根本不依赖于数据库。大致的结构看起来是Load data -&gt; Process data and return result -&gt; Complete

标签: c# unit-testing tdd


【解决方案1】:

您的方法似乎没问题。你想干净地分离你的依赖关系。这意味着:

  • 1 类只与 SQL 数据库对话,你可以调用它SomethingRepository(SqlConnection 等)
  • 1 个类使用该存储库来拉取/推送数据,您可以将其称为SomethingService(地图、字典、节点等)

当然,您不必这样称呼它们,这只是某些人使用的约定。

服务将在其构造函数中接收存储库的接口。您还将有一个容器(或类似的东西)来控制注入哪个 repo 实例。在您的应用程序中,这将是一个真正的数据库存储库,在测试中,这可能是一个模拟。在集成测试中,您可能希望传递一个真实的 repo 实例,但要调整其连接设置。

此外,您可以将所有 async/await 内容推送到堆栈中(所以不是在 repo 中,而是在服务中的某个地方,甚至是您的 webapi 或您在最顶层的东西),所以您的大部分代码应该是同步的,只有调用者使用 async/await 调用同步方法。只是节省了一些代码并使其更易于阅读,但可以不同意。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-07-08
    • 2019-02-27
    • 2014-10-10
    • 2011-03-22
    相关资源
    最近更新 更多