【DDD】学习笔记-薪资管理系统的测试驱动开发

回顾薪资管理系统的设计建模

在 3-15 课,我们通过场景驱动设计完成了薪资管理系统的领域设计建模。既然场景驱动设计可以很好地与测试驱动开发融合在一起,因此根据场景驱动设计的成果来开展测试驱动开发,就是一个水到渠成的过程。让我们先来看看针对薪资管理系统“支付薪资”领域场景分解的任务:

  • 确定是否支付日期
    • 确定是否为周五
    • 确定是否为月末工作日
      • 获取当月的假期信息
      • 确定当月的最后一个工作日
    • 确定是否为间隔一周周五
      • 获取上一次销售人员的支付日期
      • 确定是否间隔了一周
  • 计算雇员薪资
    • 计算钟点工薪资
      • 获取钟点工雇员与工作时间卡
      • 根据雇员日薪计算薪资
    • 计算月薪雇员薪资
      • 获取月薪雇员与考勤记录
      • 对月薪雇员计算月薪
    • 计算销售人员薪资
      • 获取销售雇员与销售凭条
      • 根据酬金规则计算薪资
  • 支付
    • 向满足条件的雇员账户发起转账
    • 生成支付凭条

根据任务分解驱动出来的时序图完整脚本则如下所示:

PaymentAppService.pay() {PaymentService.pay() {PayDayService.isPayday(today) {Calendar.isFriday(today);WorkdayService.isLastWorkday(today) {HolidayRepository.ofMonth(month);Calendar.isLastWorkday(holidays);}        WorkdayService.isIntervalFriday(today) {PaymentRepository.lastPayday(today);Calendar.isFriday(today);}}PayrollCalculator.calculate(employees) {HourlyEmployeePayrollCalculator.calculate() {HourlyEmployeeRepository.all();while (employee -> List<HourlyEmployee>) {employee.payroll(PayPeriod);}}SalariedEmployeePayrollCalculator.calculate() {SalariedEmployeeRepository.all();while (employee -> List<SalariedEmployee>) {employee.payroll();}}CommissionedEmployeePayrollCalculator.calculate() {CommissionedEmployeeRepository.all();while (employee -> List<CommissionedEmployee>) {employee.payroll(payPeriod);}}}PayingPayrollService.execute(employees) {TransferClient.transfer(account);PaymentRepository.add(payment);}}
}

测试驱动的方向

有了分解的任务,也有了履行职责的各个角色构造型,现在是万事俱备只欠东风。让我们严格按照测试驱动开发的红绿黄节奏以及三定律开展领域实现建模。首先,我们要选择需要添加测试的新功能。场景驱动设计在分解任务时,是从外部代表业务价值的领域场景逐步向内推进和拆分的,这是一个从外向内的驱动设计方向;测试驱动开发则不同,为了尽可能避免编写需要模拟的单元测试,应该从内部代表业务实现的原子任务开始,先完成细粒度的自给自足的领域行为逻辑单元,然后逐步往外推进,直到完成满足完整领域场景的所有任务,这是一个从内向外的驱动开发方向:

71557437.png

这就意味着在开始测试驱动开发之前,我们需要选择合适的任务。需要考虑的因素包括:

  • 任务的依赖性
  • 任务的重要性

从依赖的角度看,并不一定需要优先选择前序任务,因为我们可以使用模拟的方式驱动出当前任务需要依赖的接口,而无需考虑实现。不过,基于场景驱动开发分解的任务层次,为其编写测试用例时,也应优先挑选无需访问外部资源的原子任务,即为聚合编写单元测试,因为它无需任何模拟行为。至于任务的重要性,主要是判断任务是否整个系统或模块的核心功能。在确定了领域场景的前提下,一个判断标准是确定任务是主要流程还是异常流程。通常而言,应优先考虑任务的主流程。

显然,支付薪资领域场景的核心功能是支付与薪资计算。由于支付由外部服务完成,剩下要实现的核心功能就是薪资计算。如果从原子任务开始挑选,应首先从内部的原子任务开始挑选,例如选择“根据雇员日薪计算薪资”原子任务:

  • 计算雇员薪资
    • 计算钟点工薪资
      • 获取钟点工雇员与工作时间卡
      • 根据雇员日薪计算薪资
    • 计算月薪雇员薪资
      • 获取月薪雇员与考勤记录
      • 对月薪雇员计算月薪
    • 计算销售人员薪资
      • 获取销售雇员与销售凭条
      • 根据酬金规则计算薪资

测试驱动开发的过程

编写失败的测试

现在需要为该子任务编写测试用例。根据钟点工薪资的计算规则,可以分为两个不同的测试用例:正常工作时长和加班工作时长。由于场景驱动设计已经确定了履行该原子任务职责的是 HourlyEmployee,遵循测试驱动开发的定律一“一次只写一个刚好失败的测试,作为新加功能的描述”,编写一个刚好失败的测试:

public class HourlyEmployeeTest {@Testpublic void should_calculate_payroll_by_work_hours_in_a_week() {}
}

按照 Given-When-Then 模式来编写该测试方法。首先考虑 HourlyEmployee 聚合的创建。由于钟点工每天都要提交工作时间卡,薪资按周结算,因此在创建 HourlyEmployee 聚合根的实例时,需要传入工作时间卡的列表。计算薪资的方法为 payroll(),返回结果为薪资模型对象 Payroll。验证时,需确保薪资的结算周期与薪资总额是正确的。故而编写的测试方法为:

    @Testpublic void should_calculate_payroll_by_work_hours_in_a_week() {//givenTimeCard timeCard1 = new TimeCard(LocalDate.of(2019, 9, 2), 8);TimeCard timeCard2 = new TimeCard(LocalDate.of(2019, 9, 3), 8);TimeCard timeCard3 = new TimeCard(LocalDate.of(2019, 9, 4), 8);TimeCard timeCard4 = new TimeCard(LocalDate.of(2019, 9, 5), 8);TimeCard timeCard5 = new TimeCard(LocalDate.of(2019, 9, 6), 8);List<TimeCard> timeCards = new ArrayList<>();timeCards.add(timeCard1);timeCards.add(timeCard2);timeCards.add(timeCard3);timeCards.add(timeCard4);timeCards.add(timeCard5);HourlyEmployee hourlyEmployee = new HourlyEmployee(timeCards, Money.of(10000, Currency.RMB));//whenPayroll payroll = hourlyEmployee.payroll();//thenassertThat(payroll).isNotNull();assertThat(payroll.beginDate()).isEqualTo(LocalDate.of(2019, 9, 2));assertThat(payroll.endDate()).isEqualTo(LocalDate.of(2019, 9, 6));assertThat(payroll.amount()).isEqualTo(Money.of(400000, Currency.RMB));}

运行测试,失败:

56636479.png

让失败的测试刚好通过

在实现测试时,遵循测试驱动开发定律二“不写任何产品代码,除非它刚好能让失败的测试通过”,在实现 payroll() 方法时,仅提供满足当前测试用例预期的实现。什么是“刚好能让失败的测试通过”?以当前测试方法为例。要计算钟点工的薪资,除了它提供的工作时间卡之外,还需要钟点工的时薪,至于 HourlyEmployee 的其他属性,暂时可不用考虑;当前测试方法没有要求验证工作时间卡的有效性,在实现时,亦不必验证传入的工作时间卡是否符合要求,只需确保为测试方法准备的数据是正确的即可;当前测试方法是针对正常工作时长计算薪资,实现时就无需考虑加班的情况。实现代码为:

public class HourlyEmployee {private List<TimeCard> timeCards;private Money salaryOfHour;public HourlyEmployee(List<TimeCard> timeCards, Money salaryOfHour) {this.timeCards = timeCards;this.salaryOfHour = salaryOfHour;}public Payroll payroll() {int totalHours = timeCards.stream().map(tc -> tc.workHours()).reduce(0, (hours, total) -> hours + total);Collections.sort(timeCards);return new Payroll(timeCards.get(0).workDay(), timeCards.get(timeCards.size() - 1).workDay(), salaryOfHour.multiply(totalHours));}
}

在编写让失败测试通过的代码时,要把握好分寸,既不要过度地实现测试没有覆盖的内容,也无需死板地拘泥于编写所谓“简单”的实现代码。简单并非简陋,既然你的编码技能与设计水平已经足以一次编写出优良的代码,就不必一定要拖到最后,多此一举地等待重构来改进。例如,在上述实现代码中,需要将工作总小时数乘以 Money 类型的时薪,你当然可以实现为如下代码:

new Money(salaryOfHour.value() * totalHours, salaryOfHour.currency())

然而,如果你已经熟悉迪米特法则,且认识到以数据提供者形式进行对象协作的弊病,就会自然地想到应该在 Money 中定义 multiply() 方法,而非通过公开 value 和 currency 的 get 访问器让调用者完成乘法计算。这时就可直接实现如下代码,而不必等着以后再来进行重构:

public class Money {private final long value;private final Currency currency;public static Money of(long value, Currency currency) {return new Money(value, currency);}private Money(long value, Currency currency) {this.value = value;this.currency = currency;}public Money multiply(int factor) {return new Money(value * factor, currency);}@Overridepublic boolean equals(Object o) {if (this == o) return true;if (o == null || getClass() != o.getClass()) return false;Money money = (Money) o;return value == money.value &&currency == money.currency;}@Overridepublic int hashCode() {return Objects.hash(value, currency);}
}

简单说来,在不会导致过度设计的前提下,若能直接编写出整洁的代码,又何乐而不为呢?只需要做到实现的代码仅仅能让测试刚好通过,不去过度设计即可。为了让测试方法通过,我们定义并实现了 HourlyEmployee、TimeCard 与 Payroll 等领域模型对象。它们的定义都非常简单,即使你知道 HourlyEmployee 一定还有 Id 和 name 等基本的核心字段,也不必在现在就给出这些字段的定义。利用测试驱动开发来实现领域模型,重要的一点就是要用测试来驱动出这些模型对象的定义。只要不会遗漏领域场景,就一定会有测试去覆盖这些领域逻辑。一次只做好一件事情即可。

现在测试变绿了:

82470824.png

在测试通过的情况下,先不要考虑是重构还是编写新的测试,而应提交代码。持续集成强调七步提交法,其基础就是进行频繁的原子提交。这样就能保证尽快将你的最新变更反馈到团队共享的代码库上,降低代码冲突的风险,同时也能为重构设定一个安全的回滚版本。

重构产品代码和测试代码

提交代码后,根据简单设计原则,我们需要检查已有实现与测试代码是否存在重复,是否清晰地表达了设计者意图。

先看产品代码,目前的实现并没有重复代码,但是 payroll() 方法中的代码 Collections.sort(timeCards); 会让人产生困惑:为什么需要对工作时间卡排序?显然,这里缺乏对业务含义的封装,直接将实现暴露出来了。排序仅仅是手段,我们的目标是获得结算薪资的开始日期和结束日期。由于返回的是两个值,且这两个值代表了一个内聚的概念,故而可以定义一个内部概念 Peroid。重构的过程是首先提取 beginDate 和 endDate 变量,然后定义 Period 内部类:

    public Payroll payroll() {int totalHours = timeCards.stream().map(tc -> tc.workHours()).reduce(0, (hours, total) -> hours + total);Collections.sort(timeCards);LocalDate beginDate = timeCards.get(0).workDay();LocalDate endDate = timeCards.get(timeCards.size() - 1).workDay();Period settlementPeriod = new Period(beginDate, endDate);return new Payroll(settlementPeriod.beginDate, settlementPeriod.endDate, salaryOfHour.multiply(totalHours));}private class Period {private LocalDate beginDate;private LocalDate endDate;Period(LocalDate beginDate, LocalDate endDate) {this.beginDate = beginDate;this.endDate = endDate;}}

然后,再提取方法 settlementPeriod()。该方法名直接体现其业务目标,并将包括排序在内的实现细节封装起来:

    public Payroll payroll() {int totalHours = timeCards.stream().map(tc -> tc.workHours()).reduce(0, (hours, total) -> hours + total);return new Payroll(settlementPeriod().beginDate,settlementPeriod().endDate,salaryOfHour.multiply(totalHours));}private Period settlementPeriod() {Collections.sort(timeCards);LocalDate beginDate = timeCards.get(0).workDay();LocalDate endDate = timeCards.get(timeCards.size() - 1).workDay();return new Period(beginDate, endDate);}

接下来,不要忘了对测试代码的重构。毫无疑问,创建 List 的逻辑可以封装为一个方法,不至于让测试的 Given 部分充斥太多不必要的细节:

 public class HourlyEmployeeTest {@Testpublic void should_calculate_payroll_by_work_hours_in_a_week() {//givenList<TimeCard> timeCards = createTimeCards();Money salaryOfHour = Money.of(10000, Currency.RMB);HourlyEmployee hourlyEmployee = new HourlyEmployee(timeCards, salaryOfHour);//whenPayroll payroll = hourlyEmployee.payroll();//thenassertThat(payroll).isNotNull();assertThat(payroll.beginDate()).isEqualTo(LocalDate.of(2019, 9, 2));assertThat(payroll.endDate()).isEqualTo(LocalDate.of(2019, 9, 6));assertThat(payroll.amount()).isEqualTo(Money.of(400000, Currency.RMB));}private List<TimeCard> createTimeCards() {TimeCard timeCard1 = new TimeCard(LocalDate.of(2019, 9, 2), 8);TimeCard timeCard2 = new TimeCard(LocalDate.of(2019, 9, 3), 8);TimeCard timeCard3 = new TimeCard(LocalDate.of(2019, 9, 4), 8);TimeCard timeCard4 = new TimeCard(LocalDate.of(2019, 9, 5), 8);TimeCard timeCard5 = new TimeCard(LocalDate.of(2019, 9, 6), 8);List<TimeCard> timeCards = new ArrayList<>();timeCards.add(timeCard1);timeCards.add(timeCard2);timeCards.add(timeCard3);timeCards.add(timeCard4);timeCards.add(timeCard5);return timeCards;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/699021.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

List集合之UML、特点、遍历方式、迭代器原理、泛型、装拆箱及ArrayList、LinkedList和Vector的区别

目录 ​编辑 一、什么是UML 二、集合框架 三、List集合 1.特点 2.遍历方式 3.删除 4.优化 四、迭代器原理 五、泛型 六、装拆箱 七、ArrayList、LinkedList和Vector的区别 ArrayList和Vector的区别 LinkedList和Vector的区别 一、什么是UML UML&#xff08;Unif…

前端项目打包体积分析与优化

一、安装依赖分析工具 npm install webpack-bundle-analyz 二、修改webpack.config.js文件 1、导入上面下载的包 2、在plugins里创建实例 三、启动打包命令 npm run build 会弹出如下界面&#xff1a; 四、优化 1、通过CDN导入react-dom文件 修改webpack.config.js文件里…

Windows 远程控制 Mac 电脑怎么操作

要从 Windows 远程控制 Mac 电脑&#xff0c;您可以使用内置 macOS 功能或第三方软件解决方案。以下是一些方法&#xff1a; 一、使用内置 macOS 功能&#xff08;屏幕共享&#xff09; 1、在 macOS 上启用屏幕共享 转至系统偏好设置 > 共享&#xff1b;选中“屏幕共享”…

八、线性代数二 ,矩阵的秩

目录 1、矩阵子式的定义与子式个数的计算&#xff1a; 2、矩阵秩的定义&#xff1a; 3、矩阵秩的计算方法&#xff1a; 4、矩阵秩的 性质&#xff1a; 线性代数四——几个重要的矩阵点积_线性代数 矩阵点积-CSDN博客 1、矩阵子式的定义与子式个数的计算&#xff1a; 概念&…

C# OpenCvSharp 利用白平衡技术进行图像修复

目录 效果 灰度世界(GrayworldWB)-白平衡算法 完美反射(SimpleWB)-白平衡算法 基于学习的(LearningBasedWB)-白平衡算法 代码 下载 C# OpenCvSharp 利用白平衡技术进行图像修复 OpenCV xphoto模块中提供了三种不同的白平衡算法&#xff0c;分别是&#xff1a;灰度世界(G…

华清远见嵌入式学习——驱动开发——day9

目录 作业要求&#xff1a; 作业答案&#xff1a; 代码效果&#xff1a; ​编辑 Platform总线驱动代码&#xff1a; 应用程序代码&#xff1a; 设备树配置&#xff1a; 作业要求&#xff1a; 通过platform总线驱动框架编写LED灯的驱动&#xff0c;编写应用程序测试&…

危险!Wyze 摄像头安全漏洞致1.3万用户隐私遭窥探

最近&#xff0c;一则关于 Wyze 摄像头再次出现安全漏洞的新闻引起了人们的广泛关注。据报道&#xff0c;该安全漏洞导致约1.3万用户的摄像头受到了未经授权的访问&#xff0c;使得这些用户的隐私信息遭到了窥视。这一事件再次引发了人们对网络安全的关注和讨论。 网络安全不仅…

啊丢的刷题记录手册

1.洛谷题P1923 求第k小的数 题目描述 输入 n&#xff08;1≤n<5000000 且 n 为奇数&#xff09;个数字ai​&#xff08;1≤ai​<109&#xff09;&#xff0c;输出这些数字的第 k 小的数。最小的数是第 0 小。 请尽量不要使用 nth_element 来写本题&#xff0c;因为本题…

module ‘json‘ has no attribute ‘dumps‘

如果在使用Python的json模块时遇到AttributeError: module json has no attribute dumps错误&#xff0c;通常是因为在Python环境中json模块不支持dumps方法。这种情况可能是因为Python的json模块被重命名或修改过导致的。 解决方法可以尝试以下几种&#xff1a; 1.检查Pytho…

基于DPU和HADOS-RACE加速Spark 3.x

背景简介 Apache Spark&#xff08;下文简称Spark&#xff09;是一种开源集群计算引擎&#xff0c;支持批/流计算、SQL分析、机器学习、图计算等计算范式&#xff0c;以其强大的容错能力、可扩展性、函数式API、多语言支持&#xff08;SQL、Python、Java、Scala、R&#xff09…

springsecurity+vue前后端分离适配cas认证的跨域问题

0. cas服务搭建参考:CAS 5.3服务器搭建_cas-overlay-CSDN博客 1. 参照springsecurity适配cas的方式, 一直失败, 无奈关闭springssecurity认证 2. 后端服务适配cas: 参考前后端分离项目(springbootvue)接入单点登录cas_前后端分离做cas单点登录-CSDN博客 1) 引入maven依赖 …

鸿蒙LiteOS-M 内核初始化

目录 一、LiteOS-M 初始化内核二、LOS_KernelInit代码分析三、LOS_Start代码解析坚持就有收获 一、LiteOS-M 初始化内核 在LiteOS-M应用程序中&#xff0c;系统初始化如下&#xff1a; /*** brief This is the ohos entry, and you could call this in your main funciton af…

2024如何恢复旧版的Chrome的主题样式

起因 chrome 更新版本之后的主题样式变成了浅紫色的页签卡样式&#xff0c;感觉很不习惯&#xff0c;也很不喜欢 如何换回旧版主题 通过主题商店&#xff0c;安装旧版本的主题 主题商店搜索下面&#xff0c;或着直接访问下面的地址 Chrome Original White Theme https://…

vue手写卡片切换,并且点击获取到卡片信息

需求&#xff1a;做一个卡片样式的列表&#xff0c;之后有一些基本信息&#xff0c;之后卡片选中后样式不一样&#xff0c;默认选中第一个卡片&#xff0c;点击卡片后可以获取到卡片的信息 一、效果 二、关键代码 index默认重0开始,activeTable默认为0,0-0等于0&#xff0c;但…

Facebook的数字社交使命:连接世界的下一步

在数字化时代&#xff0c;社交媒体已成为人们生活的重要组成部分&#xff0c;而Facebook作为其中最具影响力的平台之一&#xff0c;一直以来都在努力履行着自己的使命——连接世界。然而&#xff0c;随着时代的变迁和技术的发展&#xff0c;Facebook正在不断探索着连接世界的下…

Kafka:kafka的技术架构? ①

一、Kafka的优势 Apache Kafka是一个开放源代码的分布式事件流平台&#xff0c;成千上万的公司使用它来实现高性 能数据管道&#xff0c;流分析&#xff0c;数据集成和关键任务等相关的应用程序。 二、技术架构 0&#xff09;partition分区可以设置备份数&#xff0c;也可以设…

Go 如何按行读取(大)文件?尝试 bufio 包提供的几种方式

嗨&#xff0c;大家好&#xff01;我是波罗学。本文是系列文章 Go 技巧第十七篇&#xff0c;系列文章查看&#xff1a;Go 语言技巧。 本文将介绍 Go 如何按行读取文件&#xff0c;基于此会逐步延伸到如何按块读取文件。 引言 我们将要介绍的按行读取文件的方式其实是非常适合…

C# 1.消息队列MQ使用场景--图文解析

为什么使用消息队列MQ&#xff08;Message Queue&#xff09;&#xff1f; 消息队列有什么优点和缺点&#xff1f; Kafka(大数据日志采集)、ActiveMQ(最早的MQ--目前使用较少)、RabbitMQ(开源&#xff0c;中小型企业使用足够)、RocketMQ(阿里开发&#xff0c;大型企业适用) 都…

125 Linux C++ 系统编程4 Linux 静态库制作,动态库制作,静态库和动态库对比。静态库运行时找不到库的bug fix

一 静态库 和动态库 对比 静态库的原理&#xff1a;假设我们有一个 静态库&#xff0c;大小为500M&#xff0c;这个静态库实现了一些打牌的逻辑算法&#xff0c;提供了一堆API&#xff0c;让开发者 可以轻松的实现 54张扑克牌的随机发牌&#xff0c;指定发牌等功能。 我们写了…

Python入门学习——基础语法

一、Python解释器 1. Python解释器的作用是&#xff1a; 将Python代码翻译成计算机认识的O和1并提交计算机执行在解释器环境内可以一行行的执行我们输入的代码也可以使用解释器程序&#xff0c;去执行".py"代码文件 2. Python解释器程序在&#xff1a; <Python…