【问题标题】:DDD - ApplicationServices Unit testDDD - 应用服务单元测试
【发布时间】:2016-06-23 16:53:01
【问题描述】:

在阅读了 Uncle Bob 博客和 TSS id DEAD 文章后,我想编写更多测试代码。

我的应用程序正在使用层:ApplicationService -> 模型 -> 支持。

应用服务定义用例。这是一些程序代码(loadX,调用 X.doSomething,saveX)。在某些情况下,appService 类需要大量的外部依赖项。

例如,我在“工作流程”的末尾使用了一个对象“D”。因此,在使用它时,我需要检查与该对象链接的对象“A”/“B”/“C”的一些规则。假设我们有 3 个相关的实体,每个实体都有自己的存储库,在非常特殊的用例中,我需要更深入地检查另一个规则(例如“AA”== 另一个存储库)。

我的 appService 将有 8 到 10 个依赖项(5 个存储库 + 一些业务服务间实体)。测试不是我的“强项”。嘲笑太多服务时我缺乏勇气。如果每个服务有 3 个方法但我的 appService 只需要一个,那么我需要知道要模拟或模拟/存根所有方法的方法(以及模拟/存根 2/3 的无用方法)。

我认为有很多问题。我的第一个是依赖项太多。我只是不想拆分依赖项并将它们隐藏在地毯下。当然,域实体也可能存在问题:边界不好、分离过多等。

阅读article (uncle bob) 后,我在想:我做错了。也许 appService 需要用它需要的东西来表达/声明一个接口。但是如何命名呢?怎么拆分呢?我不确定将不相关的服务放在一起是否是个好主意。编写类似外观的东西,测试会变得更容易(我现在知道我的类与这些外部服务耦合,并且我确切地知道哪些)。

  • 如何测试您的 appServices ? (仅限单元测试或集成测试?)
  • 您认为拆分注入的服务(类似于“外观”或更多业务或其他内容)是一种好方法吗?

谢谢

更新 1 (23.06.2016): 例如,这里对于应用程序服务类 DPAppService 其依赖项列表:

DPRepository dpRepository;
RechercheFournisseurQueryService rechercheFournisseur;
RechercheFactureQueryService rechercheFacture;
DateService dateService;
SFRepository sfRepository;
CommandeRepository commandeRepository;
RepartitionBudgetRepository repartRepo;
GenerateurRepartitionsDPService genRepartDpService;
SaisieRepartitionsSurDpQueryService             saisieRepartitionsSurDpQueryService;
ImputationBudgetaireService imputationBudgetaireService;
NomenclatureService nomenclatureService;
ComptaGeneraleService comptaGeneraleService;
OperationInfoService operationInfoService;
DemandePaiementCalculateurService dpCalculateurService;
CodeMarcheAnnualiseRepository codeMarcheAnnualiseRepository;
DemandePaiementLigneFactory dpLigneFactory;
EcritureRepository ecritureRepository;
BrouillardNonViseRepository brouillardNonViseRepository;

使用构造函数注入非常可怕,非常烦人。有一些存储库(我认为太多了,DP 服务只需要存储库公开的方法的子集),一些服务(目前我的域无法真正拥有的计算)。

我需要稍微解释一下有关域的信息。 这个类负责附加到“DP”概念的所有用例(20 个方法,其中一些方法只是另一种不同的风格)。该概念位于“工作流程”的末尾(此处使用松散的术语):

  1. 首先用户创建一个“EJ”
  2. 然后他创建了一个“SF”
  3. 然后他创建了“Facture”
  4. 最后,他创建了一个 DP,它将预置对象绑定在一起,因此我们需要检查/加载预置对象之一的某些用例。一个 DP 不能真正保存所有的 previsous 对象(数据有点太多)。

想法:

  • 我可以拆分用例(为每个子功能创建嵌套包)以限制依赖项的数量。
  • 我可以重构模型
  • 该类可以提供它需要的接口(在 Bob 叔叔关于 tdd / little architecture 的演讲之后,我正在研究这个想法)

任何优点/缺点/想法?

非常感谢您的时间/回答。

【问题讨论】:

  • 具有如此多依赖关系的应用程序服务可能意味着它提供的服务不是高度内聚的,而且它可能做得太多。您应该至少给出该服务的接口及其所需的依赖项的示例,以便我们可以尝试向您展示正确的方向。另一个减少服务数量的想法是找到新的内聚概念,这些概念将组合一些依赖项并进行一些最初直接在应用程序服务上进行的操作。
  • +1,服务看起来很臃肿。仅仅因为某些活动在您的业务领域中形成了一个高级“工作流”,通常并不意味着所有活动都应该在同一个应用程序服务中。应用服务通常更符合聚合。

标签: unit-testing tdd domain-driven-design


【解决方案1】:

如果每个服务有 3 个方法,但我的 appService 只需要一个,那么我需要知道要模拟或模拟/存根所有方法的方法(以及模拟/存根 2/3 的无用方法)。

是的,这是一种熟悉的代码味道;或者反过来——你有工作的测试,但你必须继续返回它们来存根另一种方法,以保持测试的编译......

读完一篇文章(鲍勃叔叔)后,我在想:我做错了。也许 appService 需要用它需要的东西来表达/声明一个接口。

是的。应用服务声明了某种风格的服务提供者接口,这为它提供了一种与实现无关的方式来描述它需要在其中运行的上下文。

如果有帮助,这类似于在您的域模型中使用存储库或域服务接口。

但是如何命名呢?

给事物起名字很糟糕:

  • 上下文
  • 客户
  • 连接器
  • 网关
  • 接缝

如何拆分?我不确定将不相关的服务放在一起是否是个好主意。

我已经开始像聚合设计一样思考它。我的意思是;我们通常通过想象模型中实体的结构来开始考虑聚合。但是结构是一个非常静态的东西。在领域模型中重要的不是两个状态位通过结构连接,而是对这些状态位的更改通过业务规则连接。

这在应用程序服务级别意味着什么?也就是说,如果应用服务支持多个用例,那么这告诉我们这些用例的共同点是什么?除了“以这种方式很容易实现”之外,还应该有一些动机。我自己在这里没有答案,但我认为这是正确的问题方向。

您如何测试您的 appServices ? (仅限单元测试或集成测试?)

理想情况下两者兼而有之。应用程序服务所在的模块具有测试,这些测试本身充当服务提供者。集成检查将多个模块连接在一起以提供服务,然后针对程序集运行一系列独立的测试。

您认为拆分注入的服务(“门面”之类或更多业务或其他内容)是一种好方法吗?

是的,但这并非没有成本。立面本身非常适合记录实际发生的事情。但是每个外观都需要至少一个实现,可能更多(测试存根等)。您将为此付出代价;模块中更复杂的布线,寻找某些服务的实现实际存在的位置,尝试找到名称类/包/模块以便您可以保持一切正常等等。

【讨论】:

    【解决方案2】:

    我们过去曾多次遇到过这个问题,我们有类似工作流的逻辑,因此我们有很多依赖项。

    我们解决这个问题的方法是使用诸如管道/责任链模式之类的模式。假设有一个工作流有 10 多个依赖项,我们将工作流的每个阶段划分为一个单独的域服务,例如以下服务将存在于检查银行详细信息是否有效的工作流程中:

    initial-validation
    check-name
    check-account
    check-for-fraudulent-behaviour
    event-publication
    result-builder
    save-result
    

    这些域服务中的每一个都将实现以下接口:

    using System;
    
    public interface IService
    {
        bool CanExecute(
            IContext subject);
    
    
        Context Execute(
            IContext subject);
    }
    

    请注意,某些域服务可能会或可能不会执行,例如如果初始验证失败,则无需检查名称。但是在save-result中,无论成功还是失败都会执行

    因此,您尝试测试的工作流不是注入 10 多个依赖项,而是注入域服务的集合(管道)。

    这种模式非常棒,因为您仍然可以将代码链接在一起以创建工作流逻辑,但是代码的可测试性、可扩展性等要好得多。

    在测试方面,每个域服务实现本身都可以很容易地在单元级别进行测试,并且每个都可能有自己的依赖项,例如存储库/外部服务等。工作流本身也很容易测试,就像现在你只是需要模拟您想要的管道行为 - 这相对简单。

    我们经常希望管道构建器之类的人来帮助测试管道,以便可以重用管道构建代码​​。

    最后,我们在每个域服务实现中使用的 Context 对象将所有域服务数据结构链接在一起。每个域服务的每个结果都在 Context 中携带,示例如下:

    public interface IContext
        {
            InitialValidation.IResult InitialValidationResult { get; }
            CheckName.IResult CheckNameResult { get; }
            CheckAccount.IResult CheckAccountResult { get; }
            CheckFraud.IResult CheckFraudResult { get; }
            EventPublish.IResult EventPublishResult { get; }
            ResultBuild.IResult ResultBuildResult { get; }
            Save.IResult SaveResult { get; }
    }
    

    冗长的答案,但很难用几段来描述。简而言之,将工作流划分为有序域服务的集合。这简化了依赖结构,使代码更容易测试。

    【讨论】:

    • 您的域似乎患有贫血症。大多数规则应该封装在聚合中,而不是服务中。
    • 没有,我们的聚合封装了大量的业务规则、不变量和 DBC 检查。我们已经看到,一步一步的工作流逻辑更好地放置在域服务中。工作流将域服务中的逻辑链接在一起。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-02-10
    • 1970-01-01
    • 1970-01-01
    • 2011-02-03
    • 2019-05-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多