【问题标题】:Construct testable business layer logic构建可测试的业务层逻辑
【发布时间】:2015-01-08 09:25:55
【问题描述】:

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

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

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

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

这是下面测试中使用的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<dynamic> dalEntities = helper.ExecuteQuery(query);

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

public interface IQueryHelper
{
   LinqQueryData PrepareQueryData(IDataQueryFilter filter);
   IQueryable BuildQuery(LinqQueryData queryData);
   List<dynamic> 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 足够,我最感兴趣..

【问题讨论】:

  • DataAggregation.GetData 是否在直接查找测试数据库?
  • GetData 按照 3 个步骤中的说明运行代码。 Context.DatabaseContext 通过代码注入被模拟数据覆盖。因此,在这种情况下,getdata 函数会转到模拟数据库。通常它使用真实的数据库

标签: c# separation-of-concerns loose-coupling cohesion


【解决方案1】:

我通常使用 IServices、Services 和 MockServices。

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

IServices:

public interface IServices
{
    IEnumerable<Warehouse> LoadSupply(Lookup lookup);
    IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Lookup lookup);

    IEnumerable<Inventory> LoadParts(int daysFilter);
    Narration LoadNarration(string stockCode);
    IEnumerable<PurchaseHistory> LoadPurchaseHistory(string stockCode);

    IEnumerable<StockAlternative> LoadAlternativeStockCodes();
    AdditionalInfo GetSupplier(string stockCode);
}

模拟服务:

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<Warehouse> _supply = null;
    IEnumerable<Demand> _demand = null;
    IEnumerable<StockAlternative> _stockAlternatives = null;
    IConfirmationInteraction _refreshConfirmationDialog = null;
    IConfirmationInteraction _extendedTimelineConfirmationDialog = null;
    #endregion

    #region Boot
    public MockServices(IEnumerable<Warehouse> supply, IEnumerable<Demand> demand, IEnumerable<StockAlternative> stockAlternatives, IConfirmationInteraction refreshConfirmationDialog, IConfirmationInteraction extendedTimelineConfirmationDialog)
    {
        _supply = supply;
        _demand = demand;
        _stockAlternatives = stockAlternatives;
        _refreshConfirmationDialog = refreshConfirmationDialog;
        _extendedTimelineConfirmationDialog = extendedTimelineConfirmationDialog;
    }

    public IEnumerable<StockAlternative> LoadAlternativeStockCodes()
    {
        return _stockAlternatives;
    }

    public IEnumerable<Warehouse> LoadSupply(Lookup lookup)
    {
        return _supply;
    }

    public IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Syspro.Business.Lookup lookup)
    {
        return _demand;
    }

    public IEnumerable<Inventory> 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<Inventory>()
        {
            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<PurchaseHistory> LoadPurchaseHistory(string stockCode)
    {
        return new List<PurchaseHistory>();
    }

    public AdditionalInfo GetSupplier(string stockCode)
    {
        return new AdditionalInfo()
        {
            SupplierName = "Some supplier name"
        };
    }
    #endregion

    #region Creation
    public Inject Dependencies(IEnumerable<Warehouse> supply, IEnumerable<Demand> demand, IEnumerable<StockAlternative> stockAlternatives, IConfirmationInteraction refreshConfirmation = null, IConfirmationInteraction extendedTimelineConfirmation = null)
    {
        return new Inject()
        {
            Services = new MockServices(supply, demand, stockAlternatives, refreshConfirmation, extendedTimelineConfirmation),

            Lookup = new Lookup()
            {
                PartKeyToCachedParts = new Dictionary<string, Inventory>(),
                PartkeyToStockcode = new Dictionary<string, string>(),
                DaysRemainingToCompletedJobs = new Dictionary<int, HashSet<Job>>(),
.
.
.

            },

            DaysFilterDefault = DEFAULT_TIMELINE,
            FilterOnShortage = true,
            PartCache = null
        };
    }

    public List<StockAlternative> Alternatives()
    {
        var stockAlternatives = new List<StockAlternative>() { new StockAlternative() { StockCode = Globals.stockCode100, AlternativeStockcode = Globals.stockCode100Alt1 } };
        return stockAlternatives;
    }

    public List<Demand> Demand()
    {
        var demand = new List<Demand>()
        {
            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<Warehouse> Supply()
    {
        var supply = new List<Warehouse>() 
        { 
            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<Inventory> LoadParts(int daysFilter)
    {
        return InventoryRepository.Instance.Get(daysFilter);
    }

    public IEnumerable<Warehouse> LoadSupply(Lookup lookup)
    {
        return SupplyRepository.Instance.Get(lookup);
    }

    public IEnumerable<StockAlternative> LoadAlternativeStockCodes()
    {
        return InventoryRepository.Instance.GetAlternatives();
    }

    public IEnumerable<Demand> LoadDemand(IEnumerable<string> stockCodes, int daysFilter, Lookup lookup)
    {
        return DemandRepository.Instance.Get(stockCodes, daysFilter, lookup);
    }
.
.
.

单元测试:

    [TestMethod]
    public void shortage_exists()
    {
        // Setup
        var supply = new List<Warehouse>() { 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<Demand>()
        {
            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);
        }
    }

【讨论】:

  • 好的,你有一个例子来说明它的外观吗?
  • 好的,您是为整个业务层使用一个 IService,还是为业务层的不同部分使用许多不同的业务层接口?
  • 我更喜欢使用立面模式。但是,我确实有多个存储库,我的服务将路由到这些存储库。不过这取决于你。
  • 所以你用它的所有方法来模拟整个业务层对象.. 这很漂亮
  • 是的。它增强了我的业务逻辑的可靠性,无论它操作的数据如何(即内存、数据库、Web 服务)。如果这篇文章回答了您的问题,请标记我的回答。
猜你喜欢
  • 2012-02-05
  • 2016-08-12
  • 1970-01-01
  • 2010-12-18
  • 2014-06-01
  • 2010-12-28
  • 2011-12-03
  • 2011-11-26
  • 2017-04-29
相关资源
最近更新 更多