构建可测试的业务层逻辑 [英] Construct testable business layer logic

查看:186
本文介绍了构建可测试的业务层逻辑的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我建立在使用分层架构的.NET / C#/实体框架的应用程序。这些应用程序接口与外部世界是一个WCF服务层。下面这一层我有BL,共享库和DAL。

I am building an applications in .net/c#/Entity Framework that uses a layered architecture. The applications interface to the outside world is a WCF service Layer. Underneath this layer I have the BL, Shared Library and the DAL.

现在,为了使业务逻辑在我的应用程序可检验的,我想介绍的关注,松散耦合和高内聚分离,以便能够注入的依赖,同时测​​试。

Now, in order to make the business logic in my application testable, I am trying to introduce separation of concerns, loose coupling and high cohesion in order to be able to inject dependencies while testing.

我需要一些指针仿佛下面介绍我的做法是不够好,或者我应该进一步去耦code。

I need some pointers as to if my approach described below is good enough or if I should be decoupling the code even further.

以下code代码段用于查询使用动态LINQ的数据库。我需要的,因为我不知道表或字段的名称来查询,直到运行时使用动态LINQ。在code首先分析JSON参数代入的对象类型,然后使用这些参数建立查询,最后查询执行并返回结果。

The following code snippet is used to query a database using dynamic linq. I need to use dynamic linq since I dont know the name of the table or the fields to query until runtime. The code first parses json parameters into types objects, then builds the query using these parameters, and finally the query is execute and the result is returned

下面是在试验中使用以下

Here is the GetData function that is used in the test below

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的查询助手类的高层次definion及其接口

Here is the high level definion of the query helper class in DAL and its interface

public interface IQueryHelper
{
   LinqQueryData PrepareQueryData(IDataQueryFilter filter);
   IQueryable BuildQuery(LinqQueryData queryData);
   List<dynamic> ExecuteQuery(IQueryable query);
}

public class QueryHelper : IQueryHelper
{  
  ..
  ..
}

下面是使用逻辑与上述试验。测试构造注入嘲笑成分贝Context.DatabaseContext

Here is the test that uses the logic as described above. The test constructor injects the mocked db into 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. 这是业务层逻辑足够好或者还有什么可以做,以实现松耦合,高内聚,可测试code?

  2. 我应该注入的数据上下文在构造查询?请注意,QueryHelper定义位于DAL。在code
    使用它位于BL

请让我知道如果我应该张贴额外的code的清晰度。我如果接口IQueryHelper足以最感兴趣..

Please let me know if I should post additional code for clarity. I'm mostly interested if the interface IQueryHelper is sufficient..

推荐答案

我一般用IServices,服务和MockServices。

I generally use IServices, Services, and MockServices.


  • IServices提供可用的操作,所有的业务逻辑必须调用的方法。

  • 服务是我的code-背后注入到视图模型(即实际的数据库)中的数据访问层。

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

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

这篇关于构建可测试的业务层逻辑的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆