DDD - ApplicationServices单元测试

时间:2016-06-21 09:07:35

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

在阅读了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(2016年6月23日): 例如,这里为应用程序服务类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无法真正保留所有预先存在的对象(数据太多)。
  5. 想法:

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

    任何利弊/想法?

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

2 个答案:

答案 0 :(得分:1)

  

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

是的,这是一个熟悉的代码气味;或者反过来 - 你已经完成了工作测试,但你必须继续回到他们那里去寻找另一种方法来保持测试的编译....

  

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

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

如果有帮助,这类似于在域模型中使用Repository或Domain Service接口。

  

但如何命名呢?

命名很糟糕:

  • 上下文
  • 客户端
  • 连接器
  • 网关
  • 煤层
  

怎么分裂呢?我不确定将相关服务放在一起是个好主意。

我开始考虑聚合设计。我的意思是这个;我们通常通过想象模型中实体的结构来开始考虑聚合。但结构是一个非常静态的东西;域模型中最重要的不是两个状态的状态是按结构连接的,而是那些状态位的变化是由业务规则连接的。

这在应用程序服务级别转化为什么?也就是说,如果应用程序服务支持多个用例,那么这些用例的共同点是什么?除了&#34之外应该有一些动机;很容易实现这种方式"。我自己也没有答案,但我认为这是正确问题的方向。

  

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

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

  

您是否认为拆分注入的服务(" facade"喜欢或更多业务或其他什么)是一种好方法?

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

答案 1 :(得分:0)

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

我们解决这个问题的方法是使用管道/责任链模式等模式。因此,如果存在具有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);
}

请注意,某些域服务可能会也可能不会执行,例如如果初始验证失败,则无需检查名称。但是在保存结果中,无论成功还是失败,它都将始终执行

然后,您尝试测试的工作流程,而不是注入10+依赖项,它只是注入域服务的集合(管道)。

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

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

我们经常会喜欢管道构建器来帮助管道测试,以便重用管道构建代码​​。

最后,我们在每个域服务实现中使用的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; }
}

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