Reputation: 3221
I have a Test Method that looks something like:
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;
}
So my test has a dependency on my database, which isn't good. If I build an interface from GetMyMapAsync and then instantiate some mock data in TestConversion() that implements the interface I'd solve the problem. However, it seems like I'd have to pass it down as a parameter to BuildMyNode, GetMyIdMap, and then to GetMyMapAsync.
So my middle two methods would looks something like:
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;
}
Is there a better way to do this?
I've tried refactoring, adding a delegate with the signature of my GetMyMapAsync method. I was thinking I could pass through a mock version of the delegate in a different constructor. However, working backward through the call stack it's much complicated than I first realized. Using a factory pattern and multiple abstract classes. This is a revised version of my code:
//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;
}
}
}
Upvotes: 1
Views: 78
Reputation: 35905
Your approach seems alright. You'd want to separate your dependencies cleanly. This means:
SomethingRepository
(SqlConnection etc)SomethingService
(map, dictionary, node, etc)Of course, you don't have to call them that, it's just a convention some people use.
A service would receive an interface for the repository in its constructor. You would also have a container (or something like this) that controls which instance of repo gets injected. In your app this will be a real database repo, in a test, this may be a mock. In an integration test, you may want to pass a real repo instance but tweak its connection settings.
Also, you could push all the async/await stuff up the stack (so not in the repo, but somewhere in services or even your webapi or what have you at the very top), so most of your code should be sync, it's only the caller who is invoking a sync method using async/await. Just saves a bit of code and makes it a little easier to read, but feel free to disagree.
Upvotes: 2