关于测试:
单元测试与集成测试
关于Repository与Mapper单元测试与集成测试的权衡,理论上应由集成测试覆盖mapper,单元测试覆盖所有Repository。但实际开发中存在大量的Repository只是单纯的简单调用Mapper,无任何逻辑。对于这样的Repository,如果开发时间紧张且需求变更不频繁,可以直接在Repository层直接编写集成测试来保证测试覆盖。但对于存在业务逻辑的Repository,仍建议使用单元测试进行覆盖。且单元测试执行时间远小于执行集成测试时间,编写大量含有业务逻辑的Repository的集成测试会增加测试执行时间,导致开发人员降低对编写测试的意愿,增加流水线/CI/CD测试执行时间。
以此类推,建议对于简单逻辑的代码,无需花费过多的时间编写重复逻辑的单元测试与集成测试。但对于较为重要的系统组件代码,仍建议进行高粒度和多层次的测试覆盖。
测试对重构的影响
理论上对无测试或测试覆盖度较低的代码进行重构具有较大的风险,很难保证重构前后对外暴露功能与重构前完全一致。但过于细粒度的测试(比如过多重复逻辑的测试:集成测试与单元测试重复,不同层次下集成测试多次覆盖)会大大增加重构时对于测试代码的修改时间。从而导致降低开发人员重构意愿以及增加重构所需时间。尽管不同层次的测试多重覆盖能增加软件的可靠性。但实际开发过程中开发进度与软件可靠性的权衡仍值得项目管理人员考量。对于高测试覆盖率的重构,仍需测试人员进行重复测试。不能仅通过开发人员提供的测试报告就认为重构前后软件对外功能表现完全一致。
人工审查代码也许能检查出测试代码不能检查出的问题
没有100%可靠的软件,仅靠开发人员编写的单元测试、组件测试、集成测试不能完全保证软件的可靠性。哪怕对于测试覆盖率100%的代码,仍有可能存在未覆盖到的业务场景。测试覆盖只能保证每一行代码都被执行到,每一个分支都被覆盖到。然而来自第三方的输入却无法穷举;多种分支的组合爆炸也使业务逻辑的完全覆盖会消耗开发人员大量的时间;业务系统上线后,也可能会出现各种意想不到的脏数据。不能完全的相信开发人员测试覆盖的报告(如Jacoco高测试覆盖率,CI/CD变绿通过)就能保证其交付软件的可靠性。因此,开发人员之间的Code Review,测试人员的手工测试,上线之前各部门之间的联调仍有很大的意义。
TDD不是万能的/反对教条主义
相比一些开发者对于TDD的狂热,我认为TDD并不是万能的。TDD是一种好的思想,能够帮助我们拆分Task,划分任务,理清需求,提升开发速度和软件质量。但在实际开发任务中,我建议在保证软件质量的前提下,无需严苛遵守TDD教条。比如对于算法等探索类项目,网络爬虫项目,细粒度的TDD开发只会降低开发效率,在测试与开发代码之间反复来回修改,徒耗时间。对于这类软件,建议明确需求后先写实现再补全测试。对于简单逻辑,可以在上层进行集成测试,无需自顶向下编写大量仅仅简单的直接调用的测试(如Controller->Service->Repository->Mapper)。对于一些过于严苛(在我看来)的教条,比如每次只编写一条测试,然后红绿实现;如果存在大量的相近测试场景,比如第三方输入的验证。若存在多种格式的校验,个人认为无必要单条测试与业务实现一对一进行,在明确Task后完全可以编写多条test后一并实现,能够一定程度上避免业务代码的反复的简单修改。TDD思想应该是保证软件的可靠性和健壮性,不应是开发效率的阻碍。哪怕是完全TDD驱动出的代码,仍建议开发者人工的对逻辑进行审查。开发人员除了相信绿色的CI/CD,也应当相信自己的眼睛和大脑。
总之对于测试驱动开发而言,个人认为没有严格意义的执行教条,测试驱动开发是一种指导思想,是软件质量的保证,但不应当是开发进度和效率的阻塞。
关于异常:受检异常(Checked exception)/非受检异常(Runtime exception)
关于受检异常存在的必要性一直存在较大的争议性。许多语言如C#、Python、Kotlin、JavaScript等都没有受检异常的概念,但Java中仍保留着受检异常的概念。我们在项目中将所有的业务异常定义为受检异常,将非业务异常定义为非受检异常。关于这样做的好处,个人觉得有两篇文章写的比较好,也比较赞同他们的观点:Kotlin 和 Checked Exception、Java设计出checked exception有必要吗? - BachScript的回答 - 知乎.他们的共同之处在于受检异常实际上对应编程语言理论中的"union type"思想,受检异常作为一种让用户必须处理或向上抛出的异常,实际上是一种函数返回值的体现。对应到系统中的业务异常,业务异常是一种可预估的异常,是我们作为业务处理方必须要处理的部分。我们可以理解为要执行一个任务,要么成功,要么就返回某种异常或更多种的业务异常。调用方有能力也有必要处理这种异常。即便有受检异常即业务异常发生,流程不应当被中断,业务处理方应当有也必须有对应的应对措施保证程序正常运作。从union type的理论来看,受检异常不是真正的Exception,而是特定类型的定义。而非受检异常才是真正的Exception,比如数组越界,0整除,空指针等场景。这些异常除了在代码层面保证尽量不会发生外,这种异常往往由统一的handler在外层拦截并处理。
关于关系型数据库:数据冗余/数据库范式
如果完全按照关系型数据库的设计原则,数据库中表于表之间不应当存在数据冗余。然而在实际开发中,合理的冗余数据是可以降低程序的复杂度,减少SQL性能开销,减少联表查询等操作。而数据库表与表之间的外键约束,也往往在代码中而不是数据库的schema中体现。