#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 или что у вас есть на самом верху), поэтому большая часть вашего кода должна быть синхронизирована, только вызывающий вызывающий вызывает метод синхронизации с помощьюасинхронный / ожидающий. Просто экономит немного кода и делает его немного легче для чтения, но не стесняйтесь не соглашаться.