测试驱动开发(TDD)研究

网友投稿 792 2022-05-30

1      【引言】

测试驱动开发(TDD)是一种依靠超短开发周期理念而进行的软件开发过程:

将需求变成非常具体的测试用例,然后对代码进行编写改进,使测试通过。

这与软件开发中允许添加一些需求不关心的代码的软件开发理念是格格不入的。

2      【测试驱动开发(TDD)】

美国软件工程师肯特-贝克(Kent Beck)被认为是该技术的推动者和积极倡导者,他在2003年表示,TDD鼓励简单的设计,而简单的设计会让你对所开发的产品充满信心。

测试驱动开发与1999年开始的极限编程的测试第一的编程理念有些关联,但是近年来,测试驱动开发本身引起了更多的关注。

有的程序员还将这一概念推广到改进和调试现有老旧技术代码和代码重构上。

2.1    测试驱动开发的周期

下面的周期来自《测试驱动开发》一书中的例子:

2.1.1    添加一个测试

在测试驱动的开发中,每一个新功能都要从写一个测试开始。编写一个测试,定义一个功能或改进一个功能的测试,应该非常简洁。要编写测试,开发人员必须清楚地了解功能的规范和需求。开发者可以通过用例和用户案例故事来完成,覆盖需求和异常情况,可以用任何适合软件环境的测试框架来编写测试。它可以是现有测试的修改版本。

这是测试驱动开发与代码写完之后再写单元测试的区别:

测试驱动开发要求开发人员在写代码之前就把注意力放在需求上,这是一个微妙但重要的区别。

2.1.2    添加完测试以后,运行所有测试,查看新测试是否失败

这是为了验证测试工作是否正确:

如果新测试可以通过测试,说明开发人员不需要添加新的功能代码了,因为所需的功能已经存在了。

新的测试一般会失败,因为这是我们所预期的结果。

这也增加了开发人员对新测试的信心。

2.1.3    编写代码

下一步,就是写一些代码让测试通过。在这个阶段写出的新代码并不完美,比如说,可能通过测试的方式并不完美。这都是可以接受的,因为我们会在步骤5中进行改进。

这时,编写代码的唯一目的就是为了通过测试。程序员不能写出超出测试检查的功能之外的代码。

2.1.4    运行测试

如果现在所有的测试用例都通过了,那么程序员可以确信新的代码符合测试要求,不会破坏或降低任何现有的功能。如果不符合,则必须调整新代码,直到符合为止。

2.1.5    重构代码

在测试驱动的开发过程中,必须定期清理不断增长的代码库。新的代码可以从仅仅通过测试的目的转移到更符合逻辑的思考上来。重复的代码当然要被删除。对象、类、模块、变量和方法的名称应该清楚地表示它们当前的目的和用途。

随着功能的增加,方法函数可能变长,对象可能变大。此时我们可以进行拆分,并对新的部分进行准确的命名,以提高可读性和可维护性,这在软件生命周期的后期会越来越有价值。

继承层次结构可能会被重新安排,使其更有逻辑性和帮助性,有一些公认的设计模式可以参考一下,对于重构和干净的代码,有具体的和一般的准则。

通过在每个重构阶段不断地重新运行测试用例,开发人员可以确信这个过程不会改变任何现有的功能。

删除重复的概念是软件设计的一个重要方面。 在这种情况下,它也适用于消除测试代码和生产代码之间的重复。 例如,为了使测试在步骤3中通过而在两者中重复的模拟数字或字符串。

2.1.6    重复上述步骤

接下来就是重复上述5个步骤,从另一个新的测试开始,然后重复循环推进功能。步骤的大小应始终保持在较小的范围内,每次测试之间尽量少做1到10次编辑。如果新代码不能迅速满足新测试,或者其他测试意外失败,程序员应该撤销或还原,而不是过度调试。

这在持续集成中可以通过提供可恢复的一些检查点来实现。

在使用外部库的时候,不要忽略自身需求,以至于只是对库本身进行测试,除非想确定这个库是不是有bug,或者说这个库的功能不够完善。那这种情况可能要放弃这类程序库,或者自己研发。

2.2    最佳做法

2.2.1    测试结构

测试用例的有效布局可以确保所有需要的操作都能完成,提高测试用例的可读性,使执行流程更加顺畅。一致的结构有助于建立一个自记录的测试用例。一个常用的测试用例结构有(1)设置、(2)执行、(3)验证、(4)清理。

n  设置(Setup)。设置:将被测单元(UUT)或整个测试系统置于运行测试所需的状态中。

n  执行:触发/驱动UUT执行目标行为,并捕获所有输出,如返回值和输出参数等。这一步通常非常简单。

n  验证:验证。确保测试的结果是正确的。这些结果可能包括执行过程中捕获的显式输出或UUT中的状态变化。

n  清理。将UUT或整个测试系统恢复到测试前的状态。这种恢复允许在此测试之后立即执行另一个测试。

使用测试驱动开发有很多需要注意的方面,例如 "keep it simple, stupid"(KISS)和 "You aren't gonna need it"(YAGNI)的原则。通过专注于只写让测试通过所需的代码,设计往往可以比其他方法实现的设计更干净、更清晰,在《测试驱动开发实例》(Test-Driven Development by Example)一书中,Kent Beck也提出了 "Fake it till you make it "的原则。

为了实现某种高级设计概念,比如设计模式,要写出生成该设计的测试。代码可能会比目标模式更简单,但仍然通过所有需要的测试。这在一开始可能会让人不放心,但它的好处是让开发者更专注于业务本身。

2.2.2    先写测试:

测试应该写在要测试的功能之前。这样做有很多好处,它有助于确保应用程序的可测试性,因为开发人员必须从一开始就考虑如何测试应用程序,而不是事后再添加。它还能确保每个功能的测试都能被编写出来。此外,先写测试可以使开发人员更深入、更早地了解产品需求,确保测试代码的有效性,并保持对软件质量的持续关注。

在编写功能优先的代码时,开发人员和团队会有一种倾向,就是把开发人员不停的推到下一个功能上,最后甚至完全忽略了测试。

第一个TDD测试一开始可能甚至不能编译,因为它所需要的类和方法还不存在。然而,第一个测试可作为需求实现的第一步。

每个测试用例最初都会失败。这样可以确保测试真正的可以起作用,并能捕捉到错误。一旦成功,就实现了内部的功能代码。这就形成了 "测试驱动开发的口头禅",即 "红/绿/重构",红色表示失败,绿色表示通过。测试驱动开发不断地重复着添加失败的测试用例、修复代码让测试用例通过、重构的步骤。在每个阶段都存在预期的测试结果,这强化了开发人员对代码的预期效果,增强了信心,从而提高了工作效率。

2.2.3    要保证工作单元足够的小。

对于TDD来说,最常见的是将单元定义为一个类,或一组相关的功能,通常被称为模块。保持工作单元相对较小,好处包括:

n  减少调试工作 - 当测试失败被检测到时,使用较小的工作单元有助于追踪错误。

n  测试即文档--小的测试用例更容易阅读和理解。

测试驱动开发的先进实践可以推动验收测试驱动开发(ATDD)和用例规范化。

其中客户指定的需求标准被自动化地转为验收测试,然后推动传统的单元测试驱动开发(UTDD)过程。

这个过程确保客户有一个自动化的机制来决定软件是否满足他们的需求。

有了UTDD,开发团队就有了一个特定的目标--验收测试,从而让他们持续关注每个用户案例故事中真正想要的东西。

2.2.4    实践中的最佳做法

可以参考的一些最佳做法是:

n  将常见的设置和拆分逻辑分离成测试用例所使用的测试支持服务;

n  让每个测试用例只关注验证其测试所需的结果;

n  并设计与时间相关的测试,以允许在非实时操作系统中的可能存在的执行偏差。通常的做法是允许5%-10%的延迟执行余量,这样可以减少测试执行中可能出现的假失败结果;

n  同时,建议以对待生产代码相同的尊重态度来对待测试代码,测试代码案例必须都能正常工作,而且持之以恒,保证可读可维护;

n  团队可以聚在一起,并对测试编写和测试运行进行回顾,分享有效的技巧,摒弃坏的习惯。

2.2.5    应避免的做法

n  让测试用例依赖于从之前执行的测试用例中操纵的系统状态(也就是说,你应该总是从一个已知的、预先配置好的状态开始进行单元测试)。

n  测试用例之间的依赖关系。一个测试用例之间相互依赖的测试套件是脆弱而复杂的。执行顺序不应该被推定。

n  相互依赖的测试。相互依赖的测试会导致级联的假失败。即使UUT中不存在实际的故障,早期测试用例中的故障也会破坏后面的测试用例,增加分析失误和调试工作。

n  测试精确的执行时序或性能。

n  构建 "全能系统"。随着时间的推移,这种系统维护成本会越来越高高,也会更脆弱。这种非常常见的错误是致命的,危险的,因为它造成了整个项目复杂度无节制的增长。

n  测试代码实现的微小细节。

n  测试的缓慢运行。

2.3    测试驱动的工作

测试驱动开发除了应用在软件开发团队中,还可以用在产品和服务团队中,与TDD类似,非软件团队在工作开始前对工作的每个方面进行质量控制(QC)检查(通常是手动测试而不是自动测试)。然后,这些QC检查被用来为设计提供信息,并验证相关结果。在TDD周期的基础上少做改动形成了如下六个步骤:

1.       以 "添加检查 "取代 "添加测试"。

2.       以 "运行所有检查 "取代 "运行所有测试"

3.       用 "做工作 "代替 "写一些代码"

4.       以 "运行所有检查 "取代 "运行测试"

5.       "清理工作 "取代了 "重构代码"

6.       "重复"

2.4    TDD和ATDD

测试驱动开发与验收测试驱动开发(ATDD)相关,但又不同于验收测试驱动开发,TDD主要是开发者的工具,帮助开发者创建一个写得很好的代码单元(函数、类或模块),正确地执行一组操作。ATDD是客户、开发人员和测试人员之间的沟通工具,以确保需求的定义良好。TDD要求测试自动化。ATDD不需要,尽管自动化有助于回归测试。TDD中使用的测试通常可以从ATDD测试中派生出来,因为代码单元实现了需求的某些部分。ATDD测试是客户可以阅读的。TDD测试不需要。

2.5    TDD与BDD

BDD(行为驱动开发)结合了来自TDD和ATDD的实践,它也是先写测试,但更侧重于描述行为的测试,而不是功能单元的测试。诸如JBehave、Cucumber、Mspec和Specflow等工具提供了共识语法,允许产品所有者、开发人员和测试工程师一起定义共识行为,然后将其转化为自动测试。

2.6    代码可见性

自然的,测试套件的代码必须能够访问它正在测试的代码。另一方面,常规的设计原则,如信息隐藏、封装和其他关注点的分离等都不应该受到影响。因此,用于TDD的单元测试代码通常是在同一个项目或模块中编写的。

在面向对象设计中,这仍然不能提供对私有数据和方法的访问。因此,单元测试可能需要额外的工作。在Java和其他语言中,开发者可以使用反射来访问私有字段和方法,另外,也可以使用一个内部类来容纳单元测试,这样他们就能使用外部类的成员和属性。在.NET框架和其他一些编程语言中,可以使用部分类来暴露私有的方法和数据供测试访问。

重要的是,这样的测试实践不应该保留在生产代码中。

在C语言和其他语言中,编译器指令如#if DEBUG ..... #endif等指令可以放在这样的附加类以及所有其他测试相关的代码周围,以防止它们被编译到发布的代码中。这就意味着发布的代码与单元测试过的代码并不完全相同。在最终发布的代码中定期运行较少但更全面的端到端集成测试,以便可以确保生产代码不存在依赖测试环境的情况。

在TDD的实践者中,存在有一些争论,即测试私有方法和数据是否明智。

有些人认为,私有成员只是一个实现细节,可能会发生变化,应该在不破坏测试数量的情况下允许其发生变化。因此,对任何类通过其公有接口或子类接口进行测试就应该足够了,有些语言称其为 "protected "接口。"

另一些人说,功能的关键部分可能在私有方法中实现,直接测试它们提供了更小、更直接的单元测试的优势。

2.7    TDD软件

在TDD中,有很多测试框架和工具是很有用的。

2.7.1    xUnit Frameworks

开发者可以使用计算机辅助测试框架,通常统称为xUnit(源自1998年创建的SUnit),来创建和自动运行测试用例。这些能力对于自动化来说至关重要,因为它们将执行验证的负担从独立的后处理活动转移到了测试执行中。这些测试框架提供的执行框架允许自动执行所有系统测试用例或各种子集以及其他功能。

2.7.2    TAP Results

该测试框架可以辨识1987年创建的语言无关测试协议中的单元测试输出。

2.8    模拟和集成测试

单元测试之所以成为单元测试,是因为每一个测试只测试一个功能单元的代码。一个复杂的模块可能有一千个单元测试,而一个简单的模块可能只有十个。用于TDD的单元测试不应该跨越程序中的进程边界,更不用说网络连接了。这样做会引入延迟,使测试运行缓慢,使开发人员不愿意运行整个套件。引入对外部模块或数据的依赖关系也会使单元测试变成集成测试。如果一个模块在一连串相互关联的模块中出现了错误行为,那么就不那么容易立刻知道从哪里寻找故障原因了。

当开发中的代码依赖于数据库、Web服务或其他任何外部进程或服务时,进行单元测试分离是必须的,这也是为了设计出更加模块化、更容易测试的、更可复用的代码,一下两个步骤是必要的:

1.       每当设计中需要外部访问时,应该定义一个接口,并定义可用的访问方式。

测试驱动开发(TDD)研究

2.       接口应该用两种方式来实现,一种是真正访问外部环境,另一种是模拟对象。模拟对象只需要在跟踪日志中添加一条消息,比如 "对象已保存 "这样的消息来表明这个接口调用的任务内容,并在此基础上运行测试断言来验证行为是否正确。模拟对象的不同之处在于,它们本身就包含测试断言,可以使测试失败,例如,如果人的名字和其他数据与预期的不一样,那么测试就会失败。

模拟对象方法,看起来是返回来自数据存储或用户的数据,这可以通过始终返回相同的、真实的数据来完成测试过程,而这些数据是测试可以依赖的。

也可以使用预定义的故障数据模式,这样就可以测试错误处理例程。在故障模式下,方法可能会返回一个无效、不完整或空的响应,也可能抛出一个异常。

2.8.1    数据存储以外的模拟服务也可能在TDD中使用:

模拟的加密服务可能实际上并没有对传递的数据进行加密;

模拟的随机数服务可能总是返回1;

模拟实现是通过依赖注入使用。

测试替身(Test Double)是一种特别针对某些测试的能力,它可以替代UUT所依赖的系统能力,它通常是类或函数。

2.8.2    有两个时间点可以将测试替身引入系统中:

链接时间和执行时间。

链接时间替代是指测试替身被编译到加载模块中,执行时,测试替身被执行来验证测试。

这种方法一般是在目标环境以外的环境中运行时使用,因为目标环境要求硬件级的代码编译时需要用替身来编译。

链接器替换的替代方法是运行时替换,即在测试用例执行过程中替换真实功能。这种替换通常是通过对已知功能指针的重新分配或对象替换来完成。

测试替身有许多不同的类型,他们有不同的复杂度:

n  Dummy - Dummy是测试替身的最简单形式。它通过在需要的地方提供一个默认的返回值,这就方便了链接器的时间点的替换。

n  Stub -  stub为Dummy方式添加了简单的逻辑,提供了不同的输出。

n  Spy - 间谍可以捕获或者提供参数和状态信息,使用测试代码的访问器以获得更高级的状态验证。

n  Mock - Mock是由单个测试用例指定的,用于验证测试的特定行为,检查参数值和调用顺序。

n  Simulator –仿真器(Simulator)是一个全面的组件,提供目标能力(要被替换的功能)的高保真近似。仿真器(Simulator)通常需要大量的额外开发工作。

使用替身这种依赖注入的一个必然结果是,实际的数据库或其他外部访问代码从未被TDD过程本身测试过。为了避免由此产生的错误,需要其他测试,将测试驱动的代码与上面讨论的接口的 "真实"实现实例化。这就是集成测试,与TDD单元测试分离。它们的数量较少,而且运行的频率必然低于单元测试。

尽管如此,它们仍然可以使用相同的测试框架来实现。

可能改变存储或数据库的集成测试,即使任何测试失败,在设计时也应始终认真考虑文件或数据库的初始和最终状态。这通常可以通过以下技术的一些组合来实现:

n  TearDown方法,这是许多测试框架中不可或缺的。

n  try....catch...finally异常处理结构。

n  数据库事务中,一个事务原子化地包括写、读和删除操作。

n  在运行任何测试之前,对数据库进行 "快照"存取,并在每次测试后回滚到快照数据。这可以通过使用Ant或NAnt等框架或CruiseControl等连续集成系统自动完成。

n  在测试前将数据库初始化为清洁状态,而不是在测试后进行清理。这可能与清理工作有关,因为在进行详细的诊断之前删除数据库的最终状态可能会使测试失败的诊断变得困难。

2.9    复杂系统的TDD

在大型的、具有挑战性的系统上进行TDD,需要一个模块化的架构、定义好的组件和发布的接口,以及有规律的系统分层,并最大限度地提高平台的独立性。这些实践可以提高应用功能的可测性,并促进构建和测试自动化的实施。

2.9.1    确保设计可测

复杂的系统的架构需要满足一系列的要求。这些要求的一个关键点包括支持系统的完整和可以有效的测试。有效的模块化设计可以开发出共享特征明显的组件,这对TDD是至关重要的。

n  高内聚确保每个单元提供一组相关的功能,并使这些功能的测试更容易维护。

n  低耦合使每个单元都能有效地进行隔离测试。

n  发布的接口限制了组件的访问,并作为测试的接触点,方便了测试的创建,确保了测试和生产单元配置之间的最高保真度。

构建有效的模块化架构的一个关键技术是场景建模,在这个建模过程中要构建一组序列图,每个序列图都集中在一个单一的系统级执行场景下。

场景模型提供了一个很好的平台,用于创建组件之间响应特定实践的交互策略。

每一个场景模型为每一个组件提供所需的服务或功能需求集,它还决定了这些组件和服务一起交互的顺序。

场景模型可以大大促进复杂系统的TDD测试系统的构建。

2.9.2    管理大型团队的测试

在一个较大的系统中,组件质量差的情况因交互的复杂性而被放大。这种放大效应使得TDD的好处在大型项目中更好的展现出来。然而,测试系统复杂度本身就可能会成为一个问题,这会抵消测试系统可能带来的好处。

关键的初始步骤是认识到测试代码也是重要的软件开发功能,应该与生产代码一样进行认真的编写和维护。

在一个复杂系统中创建和管理测试软件的架构,与核心产品架构一样重要。测试驱动程序要与UUT、测试替身和单元测试框架协同工作。

3      优缺点

3.1    带来的好处

2005年的一项研究发现,使用TDD意味着写更多的测试,反过来,写更多测试的程序员往往会更有生产力。当然,与代码质量有关的推论, 与TDD和生产力之间的直接关联性目前还没有定论。

根据在新项目上使用纯TDD的程序员的体会,他们觉得很少有必要使用调试器。与版本控制系统配合使用,当测试意外失败时,将代码还原到最后一个可通过所有测试的版本,往往比调试更有效果。

测试驱动的开发提供的不仅仅是简单的正确性验证,还可以推动程序的设计,当你关注测试用例时,就必须想象客户机是如何使用该功能的。所以,程序员在实现功能之前,先关注接口,再关注实现。这是对约定设计的补充,因为它是通过测试用例而不是通过数学断言或预设来约束代码的。

测试驱动开发提供了按需分配的机动作战能力。它允许程序员专注于手头的任务,因为第一目标是使测试通过。异常情况和错误处理在一开始不会考虑,而这些外在情况的测试是单独进行的。测试驱动开发通过这种方式确保了所有写好的代码都至少有一个测试覆盖。这让编程团队以及后续的用户对代码有了更大的信心。

虽然由于单元测试代码的原因,使用TDD确实需要比没有TDD的时候需要更多的代码,但是根据Müller和Padberg的一个模型,总的代码实现时间可能会更短,大量的测试有助于限制代码中的缺陷数量。测试的早期性和频繁性有助于在开发周期的初期抓住问题,防止缺陷成为系统性的昂贵问题。在整个过程的早期阶段消除缺陷通常可以避免项目后期冗长而繁琐的调试。

TDD可以带来模块化、灵活性、可扩展性更高的代码。这种效果的产生往往是因为这种方法论要求开发人员将软件的思维方式看作是可以独立编写和测试的小单元,并在之后整合在一起。这推动开发了更小、更高内聚的类、更松散的耦合和更干净的接口。模拟对象设计模式的使用也有助于代码的整体模块化,因为这种模式要求写出的代码可以方便地在用于单元测试的模拟版本和用于部署的 "真实 "版本之间切换。

自动化测试往往会覆盖每一条代码路径。例如,对于一个TDD开发人员来说,如果要在现有的if语句中添加一个else分支,开发人员首先要写一个失败的测试用例来触发这个分支的测试失败。因此,TDD所产生的自动化测试往往非常彻底:它们会检测到代码行为中的任何意外变化。这可以检测出在开发周期后期的一个改变意外地改变了其他功能的问题。

Madeyski提供了关于TDD实践比传统的Test-Last方法或Test for correctness方法的优越性的经验证据(这些证据就是对200多名开发人员进行的一系列的实验室实验的结果),证明了TDD实践在对象之间的低耦合(CBO)方面的优越性。根据对所做实验的分析,平均效果大小代表了中等偏上的效果,这是一个现实性的实践发现。它表明由于TDD编程实践使开发出来的软件产品具有更好的模块化(即更多的模块化设计),更容易复用和测试,Madeyski也用分支覆盖率(BC)和突变评分指标(MSI)来衡量TDD实践对单元测试的影响,这两个指标分别是衡量单元测试的彻底性和故障检测有效性的指标。

TDD对分支覆盖率的影响大小为中等偏上,因此被认为是非常有效的方法。

3.2    局限性

3.2.1    无法做到100%的需求覆盖

在测试驱动开发里模式下,由于大量使用单元测试,在一些情况下,测试驱动开发无法做到100%的需求覆盖,这类情况的例子有:用户界面、与数据库一起工作的程序,以及一些依赖特定网络配置的程序。TDD鼓励开发人员将最少的代码放到这类模块中,并将可测试的库代码中的逻辑最大化,使用模拟数据来表示外部的依赖。

3.2.2    管理层可能会觉得编写测试的时间被浪费

管理部门的支持是必不可少的。如果没有整个团队相信测试驱动的开发会改善产品质量的化,管理层可能会觉得编写测试的时间被浪费了。

3.2.3    测试可能会与代码一样存在共同的盲点

在测试驱动的开发环境中创建的单元测试通常是由编写被测代码的开发人员创建的。因此,测试可能会与代码一样存在共同的盲点。例如,如果开发人员没有意识到某些输入参数必须被检查,那么很可能测试和代码都不会验证这些参数。又比如:如果开发人员误解了他正在开发的模块的需求,那么他编写的代码和单元测试都会出现同样的错误。此时,即使测试通过了,实质的内容确实不正确的。

3.2.4    虚假的安全感

大量通过的单元测试可能会带来一种虚假的安全感,从而减少额外的软件测试活动,如集成测试和合规性测试等。

3.2.5    测试成为项目维护开销的一部分

测试成为项目维护开销的一部分。写得不好的测试,例如包含硬编码错误字符串的测试,本身就很容易失败,而且维护成本很高。特别是对于脆弱的测试来说更是如此,定期产生虚假故障的测试有可能被忽略,这样一来,当真正的故障发生时,就可能无法被发现。编写测试的时候,可以通过重用错误字符串等方式来实现低维护成本,这应该是代码重构阶段的目标。

3.2.6    编写和维护过多的测试会花费时间

编写和维护过多的测试会花费时间。另外,一些灵活的模块本身测试数量有限,随时可能会接受新的需求,而很多时候并不需要改变测试。基于这些原因,通过添加针对极端条件或小样本数据进行的内容测试,比起写一套高度详细的测试更容易。

3.2.7    可能出现无法发现的漏洞

在反复的TDD周期中所达到的覆盖率和测试水平,是在长期的实践中得来的,不可能在短期内重现这种效果。因此,随着时间的推移,那些原始的、或者说早期的测试会变得越来越珍贵。 如果出现问题,不要拖,尽快尽早的修复。另外,如果出现了糟糕的架构,糟糕的设计,或者是糟糕的测试策略,致使后期的改动导致大量的如几十个现有的测试失败的话,就必须要对这些测试进行单独修复。不能仅仅删除、禁用或草率地改变它们,那样做会导致测试覆盖率的降低,可能出现无法发现的漏洞。

4      参考

http://agiledata.org/essays/tdd.html

https://www.guru99.com/test-driven-development.html

https://medium.com/javascript-scene/testing-software-what-is-tdd-459b2145405c

https://medium.com/javascript-scene/testing-software-what-is-tdd-459b2145405c

https://www.freecodecamp.org/news/test-driven-development-what-it-is-and-what-it-is-not-41fa6bca02a2/

自动化测试

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:【Python3网络爬虫开发实战】4.3-使用pyquery
下一篇:除了吴京,《流浪地球》背后还有华为云的神助攻
相关文章