网站死链是什么,西宁高端网站开发公司,设计类专业学校,做网站 思源字体清洁代码编写使用JUnit和某些模拟库的“单元测试”测试很容易。 即使测试甚至不是单元测试并提供可疑的价值#xff0c;它们也可能产生使某些涉众满意的代码覆盖范围。 编写单元测试#xff08;在理论上是单元测试#xff0c;但是比基础代码更复杂#xff09;因此也很容易编… 清洁代码 编写使用JUnit和某些模拟库的“单元测试”测试很容易。 即使测试甚至不是单元测试并提供可疑的价值它们也可能产生使某些涉众满意的代码覆盖范围。 编写单元测试在理论上是单元测试但是比基础代码更复杂因此也很容易编写因此只会增加整个软件的熵。 这种特殊类型的软件熵具有令人不愉快的特征这使得底层软件的重组或满足新需求变得更加困难。 就像测试具有负值一样。 正确地进行单元测试比人们想象的要难得多。 在本文中我概述了一些旨在提高单元测试的可读性可维护性和质量的技巧。 注意对于代码段使用Spock。 对于那些不了解Spock的人可以认为它是围绕JUnit的非常强大的DSL它添加了一些不错的功能并减少了冗长性。 失败原因 仅当被测代码有问题时单元测试才应该失败。 仅当DBService存在错误时才对DBService类的单元测试失败而不是它依赖的其他任何类都存在错误时则该测试将失败。 因此在DBService的单元测试中唯一实例化的对象应该是DBService。 DBService依赖的所有其他对象都应该被存根或模拟。 否则您将测试DBService以外的代码。 尽管您可能错误地认为这更划算但这意味着定位问题的根本原因将需要更长的时间。 如果测试失败则可能是因为多个类存在问题但您不知道哪个类。 而如果仅由于被测代码错误而导致失败则您可以确切地知道问题出在哪里。 此外以这种方式思考将改善代码的面向对象性质。 测试仅测试班级的职责。 如果职责不明确或者没有另一个类就不能做任何事情或者该类太琐碎测试毫无意义它会提示这样一个问题就其职责的一般性而言该类存在问题。 不模拟或存根依赖类的唯一例外是如果您正在使用Java库中的知名类例如String。 存根或嘲笑没有什么意义。 或者从属类只是一个简单的不可变POJO在其中没有存根或模拟它的价值。 存根和模拟 术语嘲笑和存根通常可以互换使用就好像存在同一件事一样。 它们不是同一件事。 总而言之如果您的被测试代码依赖于某个对象而该对象从未在该对象上调用具有副作用的方法则应将该对象存根。 而如果它依赖对象并且确实为其调用了具有副作用的方法则应该对其进行模拟。 为什么这很重要 因为您的测试应根据与其依赖关系之间的关系类型来检查不同的事物。 假设您要测试的对象是BusinessDelegate。 BusinessDelegate接收编辑BusinessEntities的请求。 它执行一些简单的业务逻辑然后在DBFacade数据库前面的Facade类上调用方法。 因此正在测试的代码如下所示 public class BusinessDelegate { private DBFacade dbFacade; // ... public void edit(BusinessEntity businessEntity) { // Read some attributes on the business entity String newValue businessEntity.getValue(); // Some Business Logic, Data Mapping, and / or Validation //... dbFacade.update(index, data) } } 关于BusinessDelegate类我们可以看到两个关系。 与BusinessEntity的只读关系。 BusinessDelegate在其上调用一些getters并且从不更改其状态或调用任何具有副作用的方法。 与DBFacade的关系它要求DBFacade做我们假设的事情会产生副作用。 确保更新发生不是BusinessDelegate的责任这是DBFacade的工作。 BusinessDelegate的责任是确保仅使用正确的参数来调用更新方法。 很清楚在对BusinessDelegate进行单元测试时应将BusinessEntity存根并应模拟DbFacade。 如果我们使用Spock测试框架我们可以很清楚地看到这一点 class BusinessDelegateSpec { Subject BusinessDelegate businessDelegate def dbFacade def setup() { dbFacade Mock(DbFacade) businessDelegate new BusinessDelegate(dbFacade); } def edit(BusinessEntity businessEntity) () { given: def businessEntity Stub(BusinessEntity) // ... when: businessDelegate.edit(businessEntity) then : 1 * dbFacade.update(data) } } 对存根模拟差异的深入了解可以大大提高OO质量。 与其仅考虑对象的作用不如考虑它们之间的关系和依赖性。 现在单元测试可以帮助实施可能会迷失的设计原理。 存根和模拟在正确的位置 你们中的好奇者可能想知道为什么在上面的代码sampledbFacade中在类级别声明了而businessEntity在方法级声明了吗 好吧答案是单元测试代码的可读性越强它越能反映被测代码。 在实际的BusinessDelegate类中对dbFacade的依赖关系在类级别而对BusinessEntity的依赖关系在方法级别。 在现实世界中当实例化BusinessDelegate时将存在DbFacade依赖关系每当实例化BusinessDelegate进行单元测试时也可以存在DbFacade依赖关系。 听起来合理吗 希望如此。 这样做还有两个优点 减少代码详细程度。 即使使用Spock单元测试也可能变得冗长。 如果将类级别的依赖关系移出了单元测试则将减少测试代码的冗长性。 如果您的班级在班级级别上依赖于其他四个班级则每个测试中至少要包含四行代码。 一致性。 开发人员倾向于以自己的方式编写单元测试。 如果他们是唯一阅读其代码的人那就很好 但是这种情况很少。 因此我们在所有测试中拥有的一致性越强维护起来就越容易。 因此如果您读过从未读过的测试并且至少看到由于特定原因而在特定位置对变量进行了打桩和模拟那么您会发现单元测试代码更易于阅读。 可变声明顺序 这是最后一点的后续内容。 在正确的位置声明变量是一个很好的开始下一步是按照它们在代码中出现的顺序进行操作。 所以如果我们有类似下面的内容。 public class BusinessDelegate { private BusinessEntityValidator businessEntityValidator; private DbFacade dbFacade; private ExcepctionHandler exceptionHandler; Inject BusinessDelegate(BusinessEntityValidator businessEntityValidator, DbFacade dbFacade, ExcepctionHandler exceptionHandler) { // ... // ... } BusinessEntity read(Request request, Key key) { public BusinessEntity read(Request request, Key key) { // ... } } 如果测试存根和模拟的定义顺序与类声明它们的顺序相同则读取测试代码要容易得多。 class BusinessDelegateSpec { Subject BusinessDelegate businessDelegate // class level dependencies in the same order def businessEntityValidator def dbFacade def exceptionHandler def setup() { businessEntityValidator Stub(BusinessEntityValidator) dbFacade Mock(DbFacade) exceptionHandler Mock(ExceptionHandler) businessDelegate new BusinessDelegate(businessEntityValidator, dbFacade, exceptionHandler) } def read(Request request, Key key) () { given: def request Stub(Request) def key Stub(key) when: businessDelegate. read (request, key) then : // ... } } 变量命名 而且如果您认为最后一点是学究的那么您会很高兴知道这一点也是。 用于表示存根和模拟的变量名称应与实际代码中使用的名称相同。 更好的是如果您可以在测试代码中将变量命名为与类型相同的名称并且不会失去任何业务意义则可以这样做。 在最后一个代码示例中参数变量被命名为requestInfo和key并且它们对应的存根具有相同的名称。 这比做这样的事情容易得多 //.. public void read(Request info, Key someKey) { // ... } // corresponding test code def read(Request request, Key key) () { given: def aRequest Stub(Request) def myKey Stub(key) // you ill get dizzy soon! // ... 避免过度存根 过多的存根或嘲笑通常意味着出现了问题。 让我们考虑一下得墨meter耳定律。 想象一下一些伸缩方法调用… List queryBusinessEntities(Request request, Params params) { // check params are allowed Params paramsToUpdate queryService.getParamResolver().getParamMapper().getParamComparator().compareParams(params) // ... // ... } 仅仅存根queryService是不够的。 现在resolveAllowableParams返回的所有内容都必须进行存根并且该存根必须具有mapToBusinessParamsstubbed然后必须具有mapToComparableParams。 即使使用Spock这样的框架它可以最大限度地减少冗长但对于一行Java代码您将不得不进行四行存根。 def queryBusinessEntities() () { given: def params Stub(Params) def paramResolver Stub(ParamResolver) queryService.getParamResolver() paramResolver def paramMapper Stub(ParamMapper) paramResolver.getParamMapper() paramMapper def paramComparator Stub (ParamComparator) paramMapper.getParamComparator() paramComparator Params paramsToUpdate Stub(Params) paramComparator.comparaParams(params) paramsToUpdate when: // ... then : // ... } 查看那一行Java对我们的单元测试的效果。 如果您不使用Spock之类的东西情况将会更加糟糕。 解决方案是避免伸缩方法调用并尝试仅使用直接依赖项。 在这种情况下只需将ParamComparator直接注入到我们的类中即可。 然后代码变成… List queryBusinessEntities(Request request, Params params) { // check params are allowed Params paramsToUpdate paramComparator.compareParams(params) // ... // ... } 测试代码变成 setup() { // ... // ... paramComparator Stub (ParamComparator) businessEntityDelegate BusinessEntityDelegate(paramComparator) } def queryBusinessEntities() () { given: def params Stub(Params) Params paramsToUpdate Stub(Params) paramComparator.comparaParams(params) paramsToUpdate when: // .. then : // ... } 所有突然的人都应该感谢您减少头晕。 小Cucumber语法 不良的单元测试具有可怕的内容例如遍历顶部底部和底部的断言。 它会很快使人恶心哪些是重要的哪些是多余的。 哪些需要设置的位等等等等。原理图更容易理解。 那是Gherkin语法的真正优势。 该场景是在给定的条件下设置的总是该场景何时出现然后就是我们所期望的。 更好的用法是像Spock这样的东西意味着您拥有一个不错的整洁的DSL以便在给定的时间然后在一个测试方法中将它们放在一起。 窄时宽然后 如果单元测试正在测试四种方法那是单元测试吗 考虑以下测试 def test several methods { given: // ... when: def name personService.getname(); def dateOfBirth personService.getDateOfBirth(); def country personService.getCountry(); then : name tony dateOfBirth 1970-04-04 country Ireland } 首先如果Jenkins告诉您这失败了那么您将必须扎根找出班级的哪一部分是错误的。 由于测试不针对特定方法因此您不会立即知道哪个方法失败。 其次假设是getName失败了那么getDateOfBirth和getCountry的工作方式如何 测试在第一次失败时停止。 因此当测试失败时您甚至都不知道是有一种方法无效还是三种方法无效。 您可以四处告诉所有人您有99的代码覆盖率和一项测试失败。 但是-一项测试完成了多少 此外更容易修复吗 小测试还是长测试 理想情况下测试应检查与您正在测试的事物的单个交互。 现在这并不意味着您只能拥有一项资产但是您应该在此之后拥有一个狭窄的资产。 因此让我们先缩小一下范围。 理想情况下仅一行代码。 一行代码与您要进行单元测试的方法匹配。 def getName() { given: // ... when: def name personService.getname(); then : name tony } def getDateOfBirth() { given: // ... when: def dateOfBirth personService.getDateOfBirth(); then : dateOfBirth 1970-04-04 } def getCountry() { given: // ... when: def country personService.getCountry(); then : country Ireland } 现在如果getName失败但getCountry和getDateOfBirth通过则我们可以拥有完全相同的代码覆盖率但是getName而不是getCountry和getDateOfBirth出现了问题。 获得测试的粒度与代码覆盖率完全不同。 理想情况下对于每种非私有方法它应该至少是一个单元测试。 当您将否定测试等因素考虑在内时效果会更好。在单元测试中具有多个断言是完全可以的。 例如假设我们有一个委托给其他类的方法。 考虑一个resynceCache方法该方法在其实现中会在cacheService对象上调用另外两个方法clear和reload。 def resyncCache() { given: // ... when: personService.resyncCache(); then : 1 * cacheService. clear () 1 * cacheService.reload() } 在这种情况下进行两个单独的测试是没有意义的。 “时间”相同并且如果任何一个失败您将立即知道必须查看哪种方法。 进行两个单独的测试意味着付出两倍的努力却收效甚微。 要做的一个微妙的事情是确保您的资产顺序正确。 它们应与代码执行的顺序相同。 因此在reload之前调用clear。 如果在clear处测试失败则由于方法被破坏无论如何都没有必要检查reload。 如果您不遵循断言顺序提示而是先对reload进行断言并且被报告为失败那么您将不知道应该首先发生的clear是否已经发生。 以这种方式思考将帮助您成为一名测试忍者 嘲笑和存根的排序技巧也适用于断言。 按时间顺序断言。 这很花哨但是它将使测试代码更易于维护。 参数化 参数化是一项非常强大的功能可以大大降低测试代码的详细程度并Swift增加代码路径中的分支覆盖范围。 单元测试忍者应该总是能够发现何时使用它 一个明显的迹象表明可以将多个测试分组到一个测试中并进行参数化这表明它们在when块中具有相同的参数只是输入参数不同。 例如请考虑以下内容。 def addNumbers(), even numbers () { given: // ... when: def answer mathService.addNumbers(4, 4); then : // ... } def addNumbers(), odd numbers () { given: // ... when: def answer mathService.addNumbers(5, 5); then : // ... } 正如我们在这里看到的除了输入参数外when相同。 这对于参数化毫无疑问。 Unroll( number1#number1, number2#number2 ) // unroll will provide the exact values in test report unroll will provide the exact values def addNumbers() (int number1, int number2) { given: // ... when: def answer mathService.addNumbers(number1, number2); then : // ... where: number1 | number2 || answer 4 | 4 || 8 5 | 5 || 10 } 立即我们将代码减少了50。 通过将另外一行添加到where表中我们还使添加其他排列变得更加容易。 因此尽管看起来这两个测试应该是一个参数化测试非常明显但是只有遵守时机狭窄的准则才是显而易见的。 狭窄的“何时”编码风格使要测试的确切场景更容易看到。 如果将广泛的时间用于很多事情那么事实并非如此因此很难发现要参数化的测试。 通常唯一不对具有相同语法的测试进行参数化的时间是代码块是指期望是完全不同的结构。 期望一个int是相同的结构在一个场景中期望一个异常而一个int是另一个场景则是两个不同的结构。 在这种情况下最好不要参数化。 一个经典的众所周知的例子是将正面和负面的测试混在一起。 假设我们的addNumbers方法在接收到浮动后会抛出异常这是一个否定的测试应该分开放置。 then块绝不能包含if语句。 这是测试变得越来越灵活的标志而没有if语句的单独测试更有意义。 摘要 干净的单元测试对于拥有可维护的代码基础能够定期且快速地发布并更享受您的软件工程至关重要。 翻译自: https://www.javacodegeeks.com/2020/03/clean-unit-testing.html清洁代码