构建可测试的业务层逻辑

我正在使用分层架构在.net / c#/ Entity Framework中构建应用程序。 与外界的应用程序接口是WCF服务层。 在这个图层下面,我有BL,共享库和DAL。

现在,为了使我的应用程序中的业务逻辑可测试,我试图引入关注点分离,松耦合和高内聚,以便能够在测试时注入依赖关系。

我需要一些指示,如果下面描述的方法是否足够好,或者我是否应该进一步解耦代码。

以下代码段用于使用动态linq查询数据库。 我需要使用动态linq,因为我不知道表的名称或要查询到运行时的字段。 代码首先将json参数解析为类型对象,然后使用这些参数构建查询,最后执行查询并返回结果

以下是在下面的测试中使用的GetData函数

IQueryHelper helper = new QueryHelper(Context.DatabaseContext); //1. Prepare query LinqQueryData queryData = helper.PrepareQueryData(filter); //2. Build query IQueryable query = helper.BuildQuery(queryData); //3. Execute query List dalEntities = helper.ExecuteQuery(query); 

以下是DAL及其接口中查询助手类的高级定义

 public interface IQueryHelper { LinqQueryData PrepareQueryData(IDataQueryFilter filter); IQueryable BuildQuery(LinqQueryData queryData); List ExecuteQuery(IQueryable query); } public class QueryHelper : IQueryHelper { .. .. } 

这是使用上述逻辑的测试。 测试构造函数将模拟的db注入Context.DatabaseContext

 [TestMethod] public void Verify_GetBudgetData() { Shared.Poco.User dummyUser = new Shared.Poco.User(); dummyUser.UserName = "dummy"; string groupingsJSON = "[\"1\",\"44\",\"89\"]"; string valueTypeFilterJSON = "{1:1}"; string dimensionFilter = "{2:[\"200\",\"300\"],1:[\"3001\"],44:[\"1\",\"2\"]}"; DataQueryFilter filter = DataFilterHelper.GetDataQueryFilterByJSONData( new FilterDataJSON() { DimensionFilter = dimensionFilter, IsReference = false, Groupings = groupingsJSON, ValueType = valueTypeFilterJSON }, dummyUser); FlatBudgetData data = DataAggregation.GetData(dummyUser, filter); Assert.AreEqual(2, data.Data.Count); //min value for january and february Assert.AreEqual(50, Convert.ToDecimal(data.Data.Count > 0 ? data.Data[0].AggregatedValue : -1)); } 

对我的问题

  1. 这个业务层逻辑是否“足够好”或者可以做些什么来实现松耦合,高内聚和可测试代码?
  2. 我应该在构造函数中将数据上下文注入查询吗? 请注意,QueryHelper定义位于DAL中。 使用它的代码位于BL中

如果我要发布额外的代码以便清楚,请告诉我。 如果界面IQueryHelper足够,我最感兴趣..

我通常使用IServices,Services和MockServices。

  • IServices提供所有业务逻辑必须调用方法的可用操作。
  • 服务是我的代码隐藏注入视图模型(即实际数据库)的数据访问层。
  • MockServices是我的unit testing注入到视图模型(即模拟数据)的数据访问层。

IServices:

 public interface IServices { IEnumerable LoadSupply(Lookup lookup); IEnumerable LoadDemand(IEnumerable stockCodes, int daysFilter, Lookup lookup); IEnumerable LoadParts(int daysFilter); Narration LoadNarration(string stockCode); IEnumerable LoadPurchaseHistory(string stockCode); IEnumerable LoadAlternativeStockCodes(); AdditionalInfo GetSupplier(string stockCode); } 

MockServices:

 public class MockServices : IServices { #region Constants const int DEFAULT_TIMELINE = 30; #endregion #region Singleton static MockServices _mockServices = null; private MockServices() { } public static MockServices Instance { get { if (_mockServices == null) { _mockServices = new MockServices(); } return _mockServices; } } #endregion #region Members IEnumerable _supply = null; IEnumerable _demand = null; IEnumerable _stockAlternatives = null; IConfirmationInteraction _refreshConfirmationDialog = null; IConfirmationInteraction _extendedTimelineConfirmationDialog = null; #endregion #region Boot public MockServices(IEnumerable supply, IEnumerable demand, IEnumerable stockAlternatives, IConfirmationInteraction refreshConfirmationDialog, IConfirmationInteraction extendedTimelineConfirmationDialog) { _supply = supply; _demand = demand; _stockAlternatives = stockAlternatives; _refreshConfirmationDialog = refreshConfirmationDialog; _extendedTimelineConfirmationDialog = extendedTimelineConfirmationDialog; } public IEnumerable LoadAlternativeStockCodes() { return _stockAlternatives; } public IEnumerable LoadSupply(Lookup lookup) { return _supply; } public IEnumerable LoadDemand(IEnumerable stockCodes, int daysFilter, Syspro.Business.Lookup lookup) { return _demand; } public IEnumerable LoadParts(int daysFilter) { var job1 = new Job() { Id = Globals.jobId1, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode100 }; var job2 = new Job() { Id = Globals.jobId2, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode200 }; var job3 = new Job() { Id = Globals.jobId3, AssembledRequiredDate = DateTime.Now, StockCode = Globals.stockCode300 }; return new HashSet() { new Inventory() { StockCode = Globals.stockCode100, UnitQTYRequired = 1, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job1} }, new Inventory() { StockCode = Globals.stockCode200, UnitQTYRequired = 2, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job2} }, new Inventory() { StockCode = Globals.stockCode300, UnitQTYRequired = 3, Category = "Category_1", Details = new PartDetails() { Warehouse = Globals.Instance.warehouse1, Job = job3} }, }; } #endregion #region Selection public Narration LoadNarration(string stockCode) { return new Narration() { Text = "Some description" }; } public IEnumerable LoadPurchaseHistory(string stockCode) { return new List(); } public AdditionalInfo GetSupplier(string stockCode) { return new AdditionalInfo() { SupplierName = "Some supplier name" }; } #endregion #region Creation public Inject Dependencies(IEnumerable supply, IEnumerable demand, IEnumerable stockAlternatives, IConfirmationInteraction refreshConfirmation = null, IConfirmationInteraction extendedTimelineConfirmation = null) { return new Inject() { Services = new MockServices(supply, demand, stockAlternatives, refreshConfirmation, extendedTimelineConfirmation), Lookup = new Lookup() { PartKeyToCachedParts = new Dictionary(), PartkeyToStockcode = new Dictionary(), DaysRemainingToCompletedJobs = new Dictionary>(), . . . }, DaysFilterDefault = DEFAULT_TIMELINE, FilterOnShortage = true, PartCache = null }; } public List Alternatives() { var stockAlternatives = new List() { new StockAlternative() { StockCode = Globals.stockCode100, AlternativeStockcode = Globals.stockCode100Alt1 } }; return stockAlternatives; } public List Demand() { var demand = new List() { new Demand(){ Job = new Job{ Id = Globals.jobId1, StockCode = Globals.stockCode100, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode100, RequiredQTY = 1}, new Demand(){ Job = new Job{ Id = Globals.jobId2, StockCode = Globals.stockCode200, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode200, RequiredQTY = 2}, }; return demand; } public List Supply() { var supply = new List() { Globals.Instance.warehouse1, Globals.Instance.warehouse2, Globals.Instance.warehouse3, }; return supply; } #endregion } 

服务:

 public class Services : IServices { #region Singleton static Services services = null; private Services() { } public static Services Instance { get { if (services == null) { services = new Services(); } return services; } } #endregion public IEnumerable LoadParts(int daysFilter) { return InventoryRepository.Instance.Get(daysFilter); } public IEnumerable LoadSupply(Lookup lookup) { return SupplyRepository.Instance.Get(lookup); } public IEnumerable LoadAlternativeStockCodes() { return InventoryRepository.Instance.GetAlternatives(); } public IEnumerable LoadDemand(IEnumerable stockCodes, int daysFilter, Lookup lookup) { return DemandRepository.Instance.Get(stockCodes, daysFilter, lookup); } . . . 

unit testing:

  [TestMethod] public void shortage_exists() { // Setup var supply = new List() { Globals.Instance.warehouse1, Globals.Instance.warehouse2, Globals.Instance.warehouse3 }; Globals.Instance.warehouse1.TotalQty = 1; Globals.Instance.warehouse2.TotalQty = 2; Globals.Instance.warehouse3.TotalQty = 3; var demand = new List() { new Demand(){ Job = new Job{ Id = Globals.jobId1, StockCode = Globals.stockCode100, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode100, RequiredQTY = 1}, new Demand(){ Job = new Job{ Id = Globals.jobId2, StockCode = Globals.stockCode200, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode200, RequiredQTY = 3}, new Demand(){ Job = new Job{ Id = Globals.jobId3, StockCode = Globals.stockCode300, AssembledRequiredDate = DateTime.Now}, StockCode = Globals.stockCode300, RequiredQTY = 4}, }; var alternatives = _mock.Alternatives(); var dependencies = _mock.Dependencies(supply, demand, alternatives); var viewModel = new MainViewModel(); viewModel.Register(dependencies); // Test viewModel.Load(); AwaitCompletion(viewModel); // Verify var part100IsNotShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode100) && (!p.HasShortage)).Single() != null; var part200IsShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode200) && (p.HasShortage)).Single() != null; var part300IsShort = dependencies.PartCache.Where(p => (p.StockCode == Globals.stockCode300) && (p.HasShortage)).Single() != null; Assert.AreEqual(true, part100IsNotShort && part200IsShort && part300IsShort); } 

CodeBehnd:

  public MainWindow() { InitializeComponent(); this.Loaded += (s, e) => { this.viewModel = this.DataContext as MainViewModel; var dependencies = GetDependencies(); this.viewModel.Register(dependencies); . . . 

视图模型:

  public MyViewModel() { . . . public void Register(Inject dependencies) { try { this.Injected = dependencies; this.Injected.RefreshConfirmation.RequestConfirmation += (message, caption) => { var result = MessageBox.Show(message, caption, MessageBoxButton.YesNo, MessageBoxImage.Question); return result; }; this.Injected.ExtendTimelineConfirmation.RequestConfirmation += (message, caption) => { var result = MessageBox.Show(message, caption, MessageBoxButton.YesNo, MessageBoxImage.Question); return result; }; . . . } catch (Exception ex) { Debug.WriteLine(ex.GetBaseException().Message); } }