Рефакторинг кода C # для насмешек с целью устранения зависимостей базы данных

#c# #unit-testing #tdd

#c# #модульное тестирование #tdd

Вопрос:

У меня есть метод тестирования, который выглядит примерно так:

 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;
        }

        
    }
}
  

Комментарии:

1. Почему бы вам просто не создать класс, который принимает объект конструктора IGetMap interfacje, а затем просто вызвать BuildMyNode или GetMyIdMap, которые используют этот объект вместо передачи его как param ?

2. Мне интересно, что методы, которые вы показали, ничего не делают, кроме делегирования другому методу, единственный метод, в котором происходят «вещи» GetMyMapAsync , — это загрузка данных из базы данных. Если это так, то я бы протестировал его с реальной базой данных, чтобы быть на 100% уверенным, что код работает. Если это не так, и есть какая-то логика, которую вы не показали, я бы извлек эту логику в выделенный класс и реструктурировал другие классы, чтобы моя важная логика вообще не зависела от базы данных при проектировании и выполнении. Примерно структура будет выглядеть Load data -> Process data and return result -> Complete .

Ответ №1:

Ваш подход кажется правильным. Вы хотели бы четко разделить свои зависимости. Это означает:

  • 1 класс работает только с базой данных SQL, вы можете вызвать это SomethingRepository (SqlConnection и т. Д.)
  • 1 класс использует этот репозиторий для извлечения / отправки данных, вы можете назвать это SomethingService (карта, словарь, узел и т. Д.)

Конечно, вам не обязательно называть их так, это просто соглашение, которое используют некоторые люди.

Служба получит интерфейс для репозитория в своем конструкторе. У вас также должен быть контейнер (или что-то в этом роде), который контролирует, какой экземпляр репозитория вводится. В вашем приложении это будет реальное хранилище базы данных, в тесте это может быть макет. В интеграционном тестировании вы можете передать реальный экземпляр репозитория, но настроить его настройки подключения.

Кроме того, вы могли бы поместить весь материал async / await в стек (то есть не в репозиторий, а где-нибудь в сервисах или даже в вашем webapi или что у вас есть на самом верху), поэтому большая часть вашего кода должна быть синхронизирована, только вызывающий вызывающий вызывает метод синхронизации с помощьюасинхронный / ожидающий. Просто экономит немного кода и делает его немного легче для чтения, но не стесняйтесь не соглашаться.