关于单元测试:理解这八大优势才算精通单元测试

什么是单元测试在计算机编程中,单元测试是一种软件测试办法,通过该办法能够测试源代码的各个单元以确定它们是否适宜应用。 单元是最小的可测试软件组件, 它通常执行单个内聚性能。单元测试就是是指对这个最小可测试组件——即单元进行检查和验证。 单元体量小,因而比大块代码更容易设计、执行、记录和分析测试后果。 通过单元测试发现的缺点很容易定位,并且绝对容易修复。单元测试的指标是将程序拆散成各自独立的局部,并测试各个局部是否失常工作。它将可测试软件的最小局部与代码的其余部分隔离开来,并确定其行为是否与预期的完全一致。单元测试能在应用过程中发现很多缺点,在这种过程中证实本身价值。它实现了测试过程的自动化,缩小了发现应用程序中更简单局部中蕴含的谬误的艰难,并且因为能够关注到每一个单元而进步测试覆盖率。 单元测试工具 (图为禅道项目管理软件界面) 常见单元测试框架有JUnit, TestNG, PHPUnit, PyTest, Jest, CppUnit, GTest, QTest 等八种,目前国产支流项目管理软件禅道全面集成这八种单元测试框架,买通继续集成闭环,将测试用例细分了单元测试用例和性能测试用例,能够间接在禅道页面上导入各种各样的单元测试框架的执行后果。这八种单元测试框架通过禅道ZTF与Jenkins继续集成性能买通。用户发动工作后,通过ZTF主动执行测试脚本,把单元测试的后果回传给禅道,二者单干买通了继续集成闭环,买通了项目管理工具和继续集成工具之间的沟壑。 为何单元测试是麻利方法论在Apiumhub,咱们采纳麻利办法,并且大量利用单元测试。单元测试是极限编程(Extreme Programming,XP)的一个特色,极限编程是麻利软件开发办法之一,它能带来疾速的测试驱动开发。咱们深信麻利就要做继续集成和测试驱动开发。通过测试驱动开发,开发人员在开发代码时会创立单元测试,以便每个单元测试通常在编写代码之前就测试一小段软件代码。 单元测试的劣势单元测试提供了许多益处,包含及早发现软件谬误、促成变动、简化集成、提供文档起源以及许多其余长处,接下来将对其进行具体介绍。 1、使流程更灵便单元测试的次要益处之一是它使编码过程更加灵便,更遵循麻利开发方法论。 当向软件中增加越来越多的性能时,个别须要更改旧的设计和代码。 然而,更改曾经测试过的代码既冒险又高老本。 如果此时采纳单元测试,那么就能够释怀地进行重构。 单元测试实际上与各种类型的麻利编程紧密结合,因为测试被内置在其中,让程序员能够更轻松地进行更改。 换句话说,单元测试有助于平安重构。 2、保障代码品质单元测试能够进步代码的品质。 它可能确定在进一步发送代码进行集成测试之前可能呈现的每个缺点,在理论编码之前编写测试让人更难以思考到这种问题。 而单元测试能够暴露出极其状况,让人编写出品质更高的代码。 3、尽早发现软件Bug应用单元测试会让问题在晚期就被辨认发现。因为单元测试是由在集成之前测试单个代码的开发人员执行的,这样能够很早地发现问题,并在不影响其余代码片段的状况下解决问题。这既包含施行中的Bug,也包含单元标准中的缺点或缺失局部。 4、促成变动并简化集成单元测试容许在未来重构代码或降级零碎库,并确保该模块依然失常工作。单元测试能监测到可能违反设计合同的变动,有助于保护和更改代码。单元测试还能够缩小新开发性能中的缺点,缩小现有性能更改时呈现的谬误。而后通过单元测试对应用程序的各个局部进行测试,验证每个单元的准确性,再将单元集成到应用程序中。因为曾经对各个单元进行了验证,在之后的集成过程中对应用程序进行测试就变得更容易。 5、提供文档单元测试提供零碎的文档。心愿理解单元提供了哪些性能以及如何应用这些性能的开发人员能够查看单元测试,以取得对单元接口(API)的根本了解。 6、简化调试过程单元测试有助于简化调试过程。 如果测试失败,则仅须要调试代码中最新的更改,这样以往的简短的调试过程将被大大缩减。 7、设计率先编写测试会迫使程序员在编写代码之前就认真思考设计和其余必须实现的工作。 这不仅能够让人专一,还能够创立更好的设计。 测试一段代码会迫使程序员定义该代码的责任。如果能够轻松做到这一点,则意味着代码的职责是被明确定义的,因而将具备很高的凝聚力。 8、降低成本单元测试会更早地发现错误,有助于升高谬误修复的老本。设想一下在开发的前期阶段(比方在零碎测试或验收测试中)才发现Bug的老本将有多高。当然,后期检测到的谬误也更容易修复,因为前期检测到的谬误通常是许多更改的后果,测试人员可能就不会真正晓得是哪一个导致了谬误。 单元测试是针对代码单元的独立测试,外围是“独立”,劣势起源也是这种独立性,而所面临的有余也正是因为其独立性:既然是“独立”,就难以测试与其余代码和依赖环境的互相关系。 单元测试与零碎测试是互补而非代替关系。单元测试的劣势,正是零碎测试的有余,单元测试的有余,又恰是零碎测试的劣势。不能将单元测试当做解决所有问题的万金油,而需了解其劣势与有余,取长补短,与零碎测试相辅相成,实现测试的最大效益。

February 26, 2024 · 1 min · jiezi

关于单元测试:为什么单元测试不是持续交付的唯一答案

为了让继续集成和继续交付(CI/CD)成为事实,企业必须审查其外部流程,并从新思考如何处理软件交付生命周期。过来的清单和评论基本不是后退的方向。残暴的事实是,大多数企业在继续交付的路线上相当落后。对软件交付过程自身进行根本性的扭转与从货架上取下一些工具这样的半个步骤是齐全不一样的。 如果指标是对客户和用户做出更好的响应,软件团队须要专一于软件交付周期的更快迭代,并围绕疾速响应用户反馈进行组织。尽管可能有如每月公布数量这种代理指标,但采纳继续交付的最佳衡量标准是跟踪从反馈到更新软件的工夫。然而如果只是拼凑性地进行继续交付,将无奈达成指标。 人们很容易从渐进的角度来对待一个组织如何从现状倒退到它想驻足的地位。尽管渐进永远是正确的办法,但目前仅仅迈出第一步的企业不应自欺欺人地认为本人曾经走得足够远。 不要依赖于CI/CD工具通常,团队履行继续交付都是从一些自动化的单元测试来自动化构建过程开始。这是一个很好的开始,然而不要花太多的工夫去关注单元测试笼罩的代码行数。相同,企业应该将自动化测试的注意力集中在验证外围业务流程、用户事务和用户交互上,以确保它们依然依照预期和业务无效运行所需的形式运行。 CI/CD策略的下一个简单档次是偏向于将打算每季度进行的大量变动打包在一起(如果企业在这一步停留也是一种谬误)。实际上,CI成为了企业暂停致力的一个点。另一方面,CD则是尽可能频繁地通过管道和生产进行更改。一旦企业理解了代码更改对用户的影响以及主动实现这些更改的实现形式,它就须要鼓起勇气和付出财力来推动这些更改。 一般来说,即便是正在谋求CI/CD的组织,也会存在着将改革视为危险的心态。这就不可避免地导致了这样一种信念:更改的频率应该升高。这与继续交付正好相同,它会对企业齐全承受CI/CD造成妨碍。 较小的变更实质上会带来更小的危险,这意味着高生产率的软件组织应可能更快地迁徙。继续交付的概念和前景取决于企业一直部署渺小变更的能力。有必要冀望进行频繁的公布。 范畴软件相应更改那些单纯应用传统思维形式(CI工具、单元测试和验收测试)进行这项工作的企业依然没有取得任何真正的益处。企业正在部署的变更范畴应该作为它在软件开发生命周期中能够接受的品质问题级别的指导方针。 另一个常见的问题是,当一个组织决定将事件合成为一些小的变更,然而依然须要开一系列的会议,变更管制委员会或者开发团队必须通过的严格的安全检查。如果您的组织的指标是通过部署较小的变更堆栈来加快进度,那么在全面重新考虑外部正式的公布周期办法之前,它不会有任何停顿。 在政府机构等严格监管部门工作的组织,必须通过对其公布的产品进行批改和必要的文档化来克服这些挑战。政府部门以外的组织能够效仿他们的例子,通过对软件进行更改并形容这些更改将如何影响标记版本内的用户来克服文档需要。一个很好的例子就是美国政府的cloud.gov团队如何通过编程生成文档,比方他们的零碎图。 想要在CI/CD畛域取得成功的企业必须找到一种办法,将这种意见编入某种能够疾速实现的自动化测试中,而不是从任何人那里获取关于软件是否应该公布的意见。否则,这条路线上的每一个手动步骤只会进一步造成交付的提早。 组织如何解决这个问题许多企业陷入推广CI/CD至一半的地步——他们有各种各样的工具来容许一些这样的实际,然而外部流程、检查表或管理权限下的决定阻止了组织以失常的节奏公布更新。 大量的开发人员被困在传统的软件开发周期中,他们甚至不尝试CI/CD,或者通过专一于工具、测试和自动化来承受CI/CD的人工版本。有两种办法能够解决这个问题。一种办法是首先应用CI/CD工具,并受权各种开发团队开始在公司范畴内应用公共构建服务。另一种办法是确定将从较高的开发速度、较小的变更集中获益最多的开发团队,并容许从该实际中取得的教训渗透到整个业务中。 后一种模型在大多数状况下会工作得更好,因为所波及的人员数量被放弃在最低限度,而IT组织中更关怀听从性和审计的局部将有更大的灵活性来了解单个应用程序范畴内产生的事件。企业应该更违心在单个应用程序和团队中推广试验,而不是试图推动整个公司一起进行转变。 CI/CD的指标始终是一直变动的,这是无意设计的。然而请释怀,当所有可能从更快交付中获益的团队都实现了这些后果时,组织将分明本身曾经开始实现CI/CD的指标。 *该文为翻译文章,原文链接:https://thenewstack.io/how-to-avoid-pit-stops-on-the-road-to-...

February 19, 2024 · 1 min · jiezi

关于单元测试:谈谈如何使用好单元测试这把武器

前言如《Unit Testing》书里提到, 学习单元测试不应该仅仅停留在技术层面,比方你喜爱的测试框架,mocking 库等等,单元测试远远不止「写测试」这件事,你须要始终致力在单元测试中投入的工夫回报最大化,尽量减少你在测试中投入的精力,并最大化测试提供的益处,实现这两点并不容易。和咱们在日常开发中遇到的问题一样,学会一门语言,把握一种办法并不艰难,艰难的是把投入的工夫回报最大化。unit test有很多基础知识和框架,在google上一搜就一大堆,最佳实际的方法论也十分多,本文不筹备探讨这些问题,而是联合在咱们日常的工作,探讨如何应用好单元测试这把武器。 单元测试的定义什么是单元测试?来自百度 单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。至于【单元】的含意,一般来说,要依据理论状况断定具体含意,如Java里单元指一个类等。讲人话,单元测试就是为了验证一个类的准确性的测试。区别于集成测试和零碎测试。他是前置的,由开发人员主导的最小规模的测试。 一些学者们通过统计,还绘制出了下图: 85%的缺点都在代码设计阶段产生;发现bug的阶段越靠后,消耗老本就越高,呈指数级别的增长。由此看来,单测代码的编写对于交付品质以及人工消耗老本都有极其重要的影响 常见的误区浪费时间,影响开发速度不同我的项目的开发测试工夫曲线不同,你要综合思考你的代码的生命周期,你debug的能力,你平时花多少工夫review有问题的代码。随着我的项目的进行,这些工夫会递增,如果你想你所写的代码可能始终用上来,不让起初人吐槽这写的什么玩意,单元测试十分有必要。 测试应该是测试的工作开发是代码的第一责任人,最相熟代码的人,在设计阶段编辑单元测试,岂但能够让你更自信的交付,还能够缩小测试问题的产生。同时你本人的全栈能力也有所晋升。代码不是我写的,我不懂咱们常常埋怨老代码有坑难懂,或者是不足CR。其实在编写单元测试的过程中,也是CR和学习的一个过程,对于代码的主流程,边界,异样等有了深刻的了解。同时也是自我扫视代码标准、逻辑、设计的过程。我倡议在重构中写单测,在写单测中重构,相辅相成。 如何写出好的单测方法论上,有AIR 准则 ,像空气一样不会被感触到即 Automatic(自动化)、Independent(独立性)、Repeatable(可反复)。我集体的了解就是1、主动运行,通过CI集成的形式,保障单测可能主动运行,通过assert保障单元测试的验证后果,而不是print输入。确保单元测试可能自动化运行,不须要人工染指测试。 2、单元测试必须独立,不能相互调用,也不能有依赖的程序。每个测试用例之间包保障独立。3、不能够受运行的环境、数据库、中间件等影响。在编写单测的时候,须要把内部的依赖mock掉。从覆盖率的标准上来讲,不论是阿里外部还是业界,都有很多规范。 语句覆盖率达到70%;外围模块的语句覆盖率和分支覆盖率都要达到100%。 --- 《阿里巴巴Java开发手册》单测覆盖度分级参考Level1:失常流程可用,即一个函数在输出正确的参数时,会有正确的输入Level2:异样流程可抛出逻辑异样,即输出参数有误时,不能抛出零碎异样,而是用本人定义的逻辑异样告诉下层调用代码其谬误之处Level3:极其状况和边界数据可用,对输出参数的边界状况也要独自测试,确保输入是正确无效的Level4:所有分支、循环的逻辑走通,不能有任何流程是测试不到的Level5:输入数据的所有字段验证,对有简单数据结构的输入,确保每个字段都是正确的从下面的摘录看,语句覆盖率和分支覆盖率都有数值上和方法论上的要求,那在理论工作中,实际状况如何呢?笔者曾在一个季度,工作中提交的代码综合增量覆盖率简直达到了100%。我能够谈谈我的教训和实际。60%左右的单测覆盖率能够十分轻松达到,但达到95%以上的覆盖率,须要笼罩各种代码分支和异常情况等,甚至是配置和bean的初始化办法,所投入的工夫十分微小,但边际效应递加。我想测试toString, getter/setter这样的办法也没有意义。多少适合,我认为没有一个固定的规范。高代码覆盖率百分比不示意胜利,也不意味着高代码品质。该舍弃测试的局部就大胆的ignore掉。 最佳实际这个题目未免有些题目党。单元测试相干的书籍、ata文章,不可胜数,我的所谓“最佳实际”是在理论阿里工作中的一些本人踩过的坑,或者我集体认为一些重要的点,班门弄斧,如有谬误,欢送探讨。 1、暗藏的测试边界值public ApiResponse<List<Long>> getInitingSolution() { List<Long> solutionIdList = new ArrayList<>(); SolutionListParam solutionListParam = new SolutionListParam(); solutionListParam.setSolutionType(SolutionType.GRAPH); solutionListParam.setStatus(SolutionStatus.INIT_PENDING); solutionListParam.setStartId(0L); solutionListParam.setPageSize(100); List<OperatingPlan> operatingPlanList = operatingPlanMapper.queryOperatingPlanListByType(solutionListParam); for(; !CollectionUtils.isEmpty(operatingPlanList);){ /* do something */ solutionListParam.setStartId(operatingPlanList.get(operatingPlanList.size() - 1).getId()); operatingPlanList = operatingPlanMapper.queryOperatingPlanListByType(solutionListParam); } return ResponsePackUtils.packSuccessResult(solutionIdList);}下面这段代码,如何写单元测试? 很天然的,咱们写单测的时候会mock掉数据库查问,并且查出信息。然而如果查问的内容超过100,因为for循环进入一次,无奈通过jacoco的主动覆盖率发现。实际上没有笼罩这个边界case,只能通过开发者的习惯来解决这些边界状况。如何解决这些暗藏的边界值,开发者不能依赖集成测试或者代码CR,必须要在本人写单元测试的时候思考到这一状况,能防止起初保护的人掉坑。 2、不要在springboot测试中应用@Transactional以及操作实在数据库单元测试的上下文应该是洁净的,设计transactional的初衷是为了集成测试(如spring官网介绍): 尽管间接操作DB能更容易验证DAO层的正确性,然而也容易被线下数据库的脏数据净化,导致单测无奈通过的问题。笔者以前遇到直连数据库的单测代码,常常改个5分钟代码,数据库里脏数据清一个小时。第二就是集成测试须要启动整个利用的容器,违反了提高效率的初衷。如果切实要测DAO层的正确性,能够整合H2嵌入式数据库。这个网上教程十分多,不再赘述。 3、单测里工夫相干的内容笔者已经在工作中遇到过一个极其case,一个CI平时都失常运行,有一次深夜公布, CI跑不过,起初通过第二天check才发现有前人在单测中取了以后工夫,在业务逻辑中含有夜间逻辑(夜间音讯不发),导致了CI无奈通过。那么工夫在单测中要如何解决呢?在应用Mockito时,能够应用mock(Date.class)来模仿日期对象,而后应用when(date.getTime()).thenReturn(time)来设置日期对象的工夫。如果你应用了calendar.getInstance(),如何获取以后工夫?Calendar.getInstance()是static办法,无奈通过Mockito进行mock。须要引入powerMock,或者降级到mockito 4.x能力反对: @RunWith(PowerMockRunner.class) @PrepareForTest({Calendar.class, ImpServiceTest.class}) public class ImpServiceTest { @InjectMocks private ImpService impService = new ImpServiceImpl(); @Before public void setup(){ MockitoAnnotations.initMocks(this); Calendar now = Calendar.getInstance(); now.set(2022, Calendar.JULY, 2 ,0,0,0); PowerMockito.mockStatic(Calendar.class); PowerMockito.when(Calendar.getInstance()).thenReturn(now); }}4、final类,static类等的单元测试如第3点提到的calendar的例子,static类的mock须要mockito4.x的版本。否则就要引入powermock,powermock不兼容mockito3.x版本,不兼容mockito 4.x版本。因为老的利用引入了十分多的mockito3.x的版本,间接应用mockito4.x对final和static类进行mock须要排包。实际中看,JUnit、Mockito、Powermock三者之间的版本号有兼容性问题,可能会呈现java.lang.NoSuchMethodError,须要依据理论的状况抉择版本进行mock。然而在新我的项目立项的时候,要确定好应用的mockito和junit版本,是否引入powermock等框架,确保环境稳固可用。老我的项目倡议不要大规模改变mockito和powermock的版本,容易排包排到狐疑人生。 ...

June 12, 2023 · 2 min · jiezi

关于单元测试:禅道结合ZTF驱动单元测试执行

ZTF是禅道开源的一款自动化测试工具,反对两种模式的脚本: ZTF自治理脚本。它通过在脚本顶部的正文中退出用例的编号、题目、步骤和期待后果等信息,实现和禅道手工用例的同步,用于同执行时输入的理论后果进行比对,以实现检查点的断言。具体可参考这里的一个例子;其余单元测试或自动化测试工具的脚本。测试人员可依照原来的形式编写测试脚本,ZTF对他们并没有侵入,只是负责驱动这些工具脚本或我的项目的执行调度工作,剖析后果、并提交到禅道。这里有一个PyTest的例子,供大家参考。ZTF和市面上已有的自动化测试工具相比,更聚焦于自动化测试的治理性能,包含脚本的组织和调度、同测试管理系统的集成等。应用ZTF驱动组织的自动化或单元测试工作,可一改以前自动化测试同研发管理系统相割裂的状况。自动化测试的需要、设计和执行产生和源自于管理系统;自动化测试的执行后果(包含在继续集成流水线构建过程中的)通过ZTF再反馈到管理系统中。这样,有利于在同一个零碎中,实现软件交付品质的对立度量和集中展现,提供治理上的决策反对。 接下来,咱们介绍一下ZTF对目前市场上支流的单元测试框架的反对,并提供相应的示例我的项目,供大家参考。这些单元测试框架提供了数据驱动、用户并发、指定调度、报告剖析等一些优良的个性,不仅能够用来做单元测试,在其余类型的测试,如UI性能自动化测试、手机APP测试、接口和性能测试都能够应用。 编号框架名称应用介绍示例我的项目1JUnithttps://ztf.im/book/ztf/junit-33.htmlhttps://gitee.com/ngtesting/ci_test_junit2TestNGhttps://ztf.im/book/ztf/testng-34.htmlhttps://gitee.com/ngtesting/ci_test_testng3PHPUnithttps://ztf.im/book/ztf/phpunit-35.htmlhttps://gitee.com/ngtesting/ci_test_phpunit4PyTesthttps://ztf.im/book/ztf/pytest-36.htmlhttps://gitee.com/ngtesting/ci_test_pytest5Jesthttps://ztf.im/book/ztf/jest-37.htmlhttps://gitee.com/ngtesting/ci_test_jest6GTesthttps://ztf.im/book/ztf/gtest-39.htmlhttps://gitee.com/ngtesting/ci_test_gtest.git7QTesthttps://ztf.im/book/ztf/qtest-40.htmlhttps://gitee.com/ngtesting/ci_test_qtest8CppUnithttps://ztf.im/book/ztf/cppunit-38.htmlhttps://gitee.com/ngtesting/ci_test_cppunit9GoTesthttps://ztf.im/book/ztf/gotest-184.htmlhttps://gitee.com/ngtesting/ci_test_allure_gotest10Allurehttps://ztf.im/book/ztf/allure-183.htmlhttps://gitee.com/ngtesting/ci_test_allure_testng专题目录

May 7, 2023 · 1 min · jiezi

关于单元测试:云原生引擎单元测试实践

作者:京东批发 王雷 单元测试概念单元测试是用来对一个模块、一个函数或者一个类来进行正确性测验的测试工作。单元测试是一种白盒测试技术,个别都是由开发人员在编码阶段实现,目标就是验证软件代码中的每个单元(办法或类等)是否合乎预期,即尽早在尽量小的范畴内裸露问题。 疾速迭代的开发工作中如何进步代码品质始终是团队痛点,特地是没有测试反对的开发团队。正当的应用单元测试,并关注单元测试通过率、代码覆盖率能够无效进步代码品质。 云原生引擎服务,实际了单元测试,并在研发自测、预发、上线等阶段施行了相应的策略。在肯定水平上进步了代码的品质。 单元测试的目标单元测试的目标在于发现各模块外部可能存在的各种谬误,次要包含以下几个方面: (1) 验证代码是与设计相符合的。 (2) 发现设计和需要中存在的谬误。 (3) 发现在编码过程中引入的谬误。 在开发阶段尽可能发现代码中的问题;在预发集成阶段尽可能发现各个业务代码之间的问题;在上线阶段做最初的确认保障上线代码品质。 单元测试除了可能在较早阶段辨认软件中的谬误,它还有如下价值。 •反馈速度快:单元测试通常以自动化模式运行,执行速度十分快,能够疾速反馈后果,跟继续集成联合起来,造成无效的反馈环。 •重构的无力保障:零碎须要大规模重构时,单测能够确保对已有逻辑的兼容,如果单元测试都通过,基本上能够保障重构没有毁坏原来代码逻辑的正确性。 •使更相熟代码:写单元测试的过程自身就是一个扫视代码的过程,能够发现一些设计上的问题(代码设计的不可测试)、代码编写方面的问题(边界条件的处理不当)等。 云原生引擎单测实际整体单测率引擎在进行开发过程中,会重点关注外围模块代码和底层代码,针对重要的业务逻辑代码,通用组件类等,波及到重要的性能开发,对应的每一个办法咱们都要编写对应的单元测试代码。在提交代码之前,在本地进行单测回归,跑通单测之后,提交代码,分支合并。 单元测试重点引擎的单测重点次要体现在以下五个方面 1、输入输出测试这里次要是针对数据的输出和输入进行测试。 调用所测模块时的输出参数与模块的形式参数在个数、属性、程序上是否匹配。所测模块调用子模块时,它输出给子模块的参数与子模块中的形式参数在个数、属性、程序上是否匹配。是否批改了只用作输出的形式参数。2、门路测试在单元测试中,最次要的测试是针对门路的测试;测试用例必须可能发现因为计算错误、不正确的断定或不失常的控制流而产生的谬误。 常见的谬误有:误会的或不正确的算术优先级,混合模式的运算,谬误的初始化,精确度不够准确和表达式的不正确符号示意。 3、出错解决比较完善的单元设计要求能预感出错的条件,并设置适当的出错解决,以便在程序出错时,能对出错程序从新做安顿,保障其逻辑上的正确性。 4、边界条件次要测试方法对循环条件,管制条件,数据流等临界值的解决状况 比方针对一个办法中的不同分支进行单测的编写 5、部分数据结构在模块工作过程中,必须测试模块外部的数据是否放弃完整性,包含外部数据的内容、模式及互相关系不产生谬误。 对于部分数据结构,应该在单元测试中留神发现以下几类谬误: 1)不正确的或不统一的类型阐明 2)谬误的初始化或默认值 3)谬误的变量名,如拼写错误或书写谬误 4)下溢、上溢或者地址谬误 最佳实际如何写好单测1.代码设计:代码设计上要低耦合、可测试 2.度量指标:正当的单元测试用例数量以及正当的覆盖率 3.应用场景:要融入软件开发中,在开发过程中常常运行 4.测试指标:要专一于代码中重要的逻辑 5.保障独立性:应用mock形式搁置依赖系统对单元测试后果的影响 6.用例粒度:单元测试用例应该是对独自的性能的有意义的形容,通过用例能够理解改性能的逻辑 继续集成、继续卡点代码自测通过后提交MR让团队成员进行review,当review通过时咱们会通过webhook触发预发部署流水线执行单元测试和部署 上线前查看当代码验收通过后,进行线上部署时再次跑单元测试确保上线代码品质,不达标将不能部署

March 22, 2023 · 1 min · jiezi

关于单元测试:Golang-单元测试-其他小技

单元测试有很多技巧和科技,我都会缓缓汇总在这里打桩测试当咱们在编写单元测试的时候,有时咱们十分想 mock 掉其中一个办法,然而这个办法又没有接口去定义和实现(无奈用 github.com/golang/mock 来实现),这时就能够尝试看看咱们的打桩黑科技。 代码这里咱们应用 github.com/agiledragon/gomonkey 来实现。理论中,常常在代码中会遇到一些随机值的状况,比方验证码。为了不便测试,咱们会想要 mock 掉随机值办法,让每次产生的值固定不便后续的测试。 package mainimport ( "fmt" "testing" "github.com/agiledragon/gomonkey/v2" "go-demo/m/unit-test/other/rand")func init() { gomonkey.ApplyFunc(rand.Number, func() int { return 666 })}func TestRand(t *testing.T) { fmt.Println(rand.Number())}其中 rand.Number() 是咱们在另一个包中实现的办法。咱们应用 gomonkey.ApplyFunc 相当于间接替换了原有办法的实现,强制返回了 mock 的数据 666。 注意事项应用 gomonkey 时,留神肯定要应用 -gcflags=all=-l 来禁止内联优化,否则容易导致打桩不失效。如:go test -gcflags=all=-l -v在 Mac 的 M1 下打桩不失效,能够应用环境变量 GOARCH=amd64 来进行测试,只不过这样就无奈进行断点调试。https://github.com/agiledragon/gomonkey/issues/77毕竟是黑科技,理论应用环境对于测试还是有影响的。压测这里的压测通常不是对接口的压测,而是对于某些办法的压测。Golang 提供 十分好用的 b *testing.B 来专门进行压测。 代码非常容易上手,让咱们间接来看代码 var numbers = []int{ 100, 1000, 77777, 666666,}func BenchmarkPrimeNumbers(b *testing.B) { for _, v := range numbers { b.Run(fmt.Sprintf("calc_num_%d", v), func(b *testing.B) { for i := 0; i < b.N; i++ { primeNumbers(v) } }) }}应用应用 -bench=. 即可 ...

March 11, 2023 · 2 min · jiezi

关于单元测试:Golang-单元测试-接口层

上次咱们曾经搞定了逻辑层的单元测试,这次咱们来康康接口层的单元测试。接口层次要负责的就是申请的解决,最常见的就是 HTTP 申请的解决。 但针对 接口层 的单元测试其实是能够形形色色的。它并不像逻辑层和数据层一样的通用,对于它的测试往往有很多路能够走。 因为应用的 HTTP 框架不同,单元测试的实现形式则不同。 既能够通过程序来模仿 HTTP 申请,也能够通过实在的 HTTP 申请来测试,通过借助内部的一些测试工具来实现。 所以本文只能给出一种思路,具体的实现形式还是要依据理论的框架来实现。 环境本文以罕用的 gin 框架为例,应用一种集体比拟喜爱也非常简单的形式来实现单元测试。特点次要有: 不须要启动路由服务复用已有的我的项目内的申请构造代码因为之前曾经贴过,所以 service 层的 代码这里就不赘述了base casepackage controllerimport ( "context" "github.com/gin-gonic/gin" "go-demo/m/unit-test/entity")//go:generate mockgen -source=./user.go -destination=../mock/user_service_mock.go -package=mocktype UserService interface { AddUser(ctx context.Context, username string) (err error) GetUser(ctx context.Context, userID int) (user *entity.User, err error)}type AddUserRequest struct { Username string `json:"username" binding:"required"`}type GetUserRequest struct { UserID int `form:"user_id" binding:"required"`}type GetUserResponse struct { Username string `json:"username"`}type UserController struct { UserService UserService}func NewUserController(userService UserService) *UserController { return &UserController{UserService: userService}}func (uc *UserController) AddUser(ctx *gin.Context) { req := &AddUserRequest{} if err := ctx.BindJSON(req); err != nil { return } if err := uc.UserService.AddUser(ctx, req.Username); err != nil { ctx.JSON(400, gin.H{"error": err.Error()}) return } ctx.JSON(200, gin.H{"message": "success"})}func (uc *UserController) GetUser(ctx *gin.Context) { req := &GetUserRequest{} if err := ctx.BindQuery(req); err != nil { return } user, err := uc.UserService.GetUser(ctx, req.UserID) if err != nil { ctx.JSON(400, gin.H{"error": err.Error()}) return } ctx.JSON(200, &GetUserResponse{Username: user.Username})}既然之前咱们 service 的单元测试曾经通过,这次咱们就须要 mock 的是 service 层的接口 mockgen -source=./user.go -destination=../mock/user_service_mock.go -package=mock这里我将申请和返回的构造 如:GetUserRequest、GetUserResponse 放在了这里仅仅是为了不便展现代码单元测试根底代码非常简单,就是咱们常见的,最重要的让咱们来看看单元测试应该怎么写 ...

March 10, 2023 · 2 min · jiezi

关于单元测试:Golang-单元测试-逻辑层

后面咱们实现了最麻烦的数据层的单元测试,明天咱们来看看单元测试中最容易做的一层,数据逻辑层,也就是咱们通常说的 service 或者 biz 等,是形容具体业务逻辑的中央,这一层蕴含咱们业务最重要的逻辑。 所以它的测试十分重要,通常它测试的通过就意味着你的业务逻辑能失常运行了。 而如何对它做单元测试呢? 因为,这一层的依赖次要来源于数据层,通常这一层会调用数据层的接口来获取或操作数据。 因为咱们之前对于数据层曾经做了单元测试,所以这一次,咱们须要 mock 的不是数据库了,而是数据层。 Golang 提供了 github.com/golang/mock 来实现 mock 接口的操作,本文就是应用它来实现咱们的单元测试。 筹备工作装置 go install github.com/golang/mock/mockgen@v1.6.0 根本 case 代码首先咱们还是基于上一次的例子,这里给出上一次例子中所用到的接口 package serviceimport ( "context" "fmt" "go-demo/m/unit-test/entity")type UserRepo interface { AddUser(ctx context.Context, user *entity.User) (err error) DelUser(ctx context.Context, userID int) (err error) GetUser(ctx context.Context, userID int) (user *entity.User, exist bool, err error)}type UserService struct { userRepo UserRepo}func NewUserService(userRepo UserRepo) *UserService { return &UserService{userRepo: userRepo}}func (us *UserService) AddUser(ctx context.Context, username string) (err error) { if len(username) == 0 { return fmt.Errorf("username not specified") } return us.userRepo.AddUser(ctx, &entity.User{Username: username})}func (us *UserService) GetUser(ctx context.Context, userID int) (user *entity.User, err error) { userInfo, exist, err := us.userRepo.GetUser(ctx, userID) if err != nil { return nil, err } if !exist { return nil, fmt.Errorf("user %d not found", userID) } return userInfo, nil}能够看到咱们的指标很明确,就是须要 mock 掉 UserRepo 接口的几个办法,就能够测试咱们 AddUser 和 GetUser 办法了 ...

March 9, 2023 · 3 min · jiezi

关于单元测试:Golang-单元测试-数据层

前言明天咱们先来看看无关数据层(repo)的单元测试应该如何实际。 数据层,就是咱们经常说的 repo/dao,其性能就是和数据库、缓存或者其余数据源打交道。它须要从数据源中获取数据,并返回给上一层。在这一层通常没有简单业务的逻辑,所以最重要的就是测试各个数据字段的编写是否正确,以及 SQL 等查问条件是否失常能被筛选。 当然,数据层也基本上是最底层了,通常这一层的单元测试更加的重要,因为如果一个字段名称和数据库不统一下层所有依赖这个办法的中央全副都会报错。 因为数据层和数据源打交道,那么测试的麻烦点就在于,通常咱们不能要求外接肯定能提供一个数据源供咱们测试:一方面是因为咱们不可能随时都能连上测试服务器的数据库,另一方面咱们也不能要求单元测试运行的时候只有你一个人在应用这个数据库,而且数据库数据洁净。退一步讲,咱们也没方法 mock,如果 mock 了 sql,那么测试的意义就不大了。 上面咱们就以咱们常见的 mysql 数据库为例,看看在 golang 中如何进行单元测试的编写。 筹备工作的阐明数据源首先,咱们须要一个洁净的数据源,因为咱们没有方法依赖于内部服务器的数据库,那么咱们就利用最常应用的 docker 来帮忙咱们构建一个所须要应用的数据源。 咱们这里应用 github.com/ory/dockertest 来帮忙咱们构建测试的环境,它能帮忙咱们启动一个所须要的环境,当然你也能够抉择手动应用 docker 或者 docker-compose 来创立。 初始数据有了数据库之后,咱们还须要表构造和初始数据,这部分也有两种计划: 应用 orm 提供的 sync/migration 相似的性能,将构造体间接映射生成表字段,通过 go 代码创立初始数据间接应用 sql 语句,通过执行 sql 语句来创立对应的表构造和字段数据本案例应用第一种形式进行,第二种也相似根本 case 代码咱们首先来疾速搞定一下默认的 case 代码,也就是咱们经常搬砖的 CRUD。(这里仅给出最根本的实现,重点次要关注在单元测试上) package repoimport ( "context" "go-demo/m/unit-test/entity" "xorm.io/xorm")type UserRepo interface { AddUser(ctx context.Context, user *entity.User) (err error) DelUser(ctx context.Context, userID int) (err error) GetUser(ctx context.Context, userID int) (user *entity.User, exist bool, err error)}type userRepo struct { db *xorm.Engine}func NewUserRepo(db *xorm.Engine) UserRepo { return &userRepo{db: db}}func (ur userRepo) AddUser(ctx context.Context, user *entity.User) error { _, err := ur.db.Insert(user) return err}func (ur userRepo) DelUser(ctx context.Context, userID int) error { _, err := ur.db.Delete(&entity.User{ID: userID}) return err}func (ur userRepo) GetUser(ctx context.Context, userID int) (user *entity.User, exist bool, err error) { user = &entity.User{ID: userID} exist, err = ur.db.Get(user) return user, exist, err}初始化测试环境首先创立 repo_main_test.go 文件 ...

March 7, 2023 · 3 min · jiezi

关于单元测试:Golang-单元测试-前言

在测试上难以自动化的软件,很难成为好的软件。 -- 《Google软件测试之道》对于单元测试的想法对于单元测试,开发者总是有着十分矛盾的思维。 单元测试很好,它能帮忙咱们找到很多暗藏的bug但写单元测试,比搬砖还累,我真的不想写~没错,单元的确是一个磨炼意志的货色,如果不是在大公司,人力也不够,而因为单元测试往往是没有 KPI 的,所以常常在做完功能测试之后就疾速上线一直迭代了。 但,如果想要成为一个杰出的软件,或者说是作品,单元测试是保障可继续倒退的地基。比方,很多的开源软件,为了保障牢靠,单元测试覆盖率往往都有着很高的规范。 于是,从 JAVA 的 JUnitTest 的坑爬出来之后,我筹备来写写,在 Golang 中咱们如何做单元测试。不肯定是最佳实际,但相对算是不错的参考。本文先来说说单元测试的要求和留神点。 什么是单元测试笔者从书中总结了一句话:自动化验证一个独立模块的代码是否能满足预期要求的测试。 所以单元测试是一个最最底层的一个测试,思路就是保障最小的模块没有问题,只有底子稳了,下面的业务才不容易呈现问题。 要求总结自《代码整洁之道》自动化自动化是一个最根底的要求,你不能说单元测试是通过你手动点击某个按钮,输出一个用户名明码,这样进行测试。单元测试应该就是启动之后主动实现的,并且对于测试后果应该做到自动化验证,而不是通过人的眼睛去判断输入的内容是否合乎产品需要。 疾速单元测试应该测试的够快,如果单元测试跑的太慢会导致的一个问题就是人们很少去运行它。如果一个单元测试要跑 10 秒钟,那么开发者就会想着反正 10 秒钟,跑就跑一下。频繁的跑单元测试更容易提前发现未知的问题。 环境统一测试的环境应该是和应用环境统一的,这也是很多测试的一个根本保障。应用环境是 mysql8 你用 mysql5.7 进行测试就会可能脱漏问题。并且这个测试环境应该是包含在单元测试外面的。 独立单元测试应该是独立存在的,也就是对于执行程序应该是没有要求的。A 模块先测试而后 B 模块再测试就没有问题;然而 B 模块先测试 A 模块再测试就会报错,这样是不行的。这也是一个最根本的要求,一个单元测试运行的前后,要么数据现场复原,要么你能保障以后的测试数据肯定不会影响前面的测试。在你通常无奈保障的时候,原则上都要进行数据恢复。比方你测试减少1条数据,那么测试实现之后就须要将数据删除。 注意事项不要为了谋求覆盖率而忘本拿数据谈话,在很多中央都实用,数据往往能给人们很大的安全感,没错,单元测试覆盖率 99% 那么能够给人十分大的安全感。然而!这是不太事实的。 对于绝大多数的软件来说,只有软件我的项目够大,你要反对的覆盖率越大,你的开发成本也就越大。 最重要的是:即便你的代码覆盖率是 100% 也不是状况 100% 笼罩。代码尽管这部分跑过了,然而因为数据的不同,一个边界条件就能让你雷同的代码报错。打算法较量的人都晓得,同样的性能,只有测试数据全过能力 AC。所以与其去测试一个十分偶尔的数据库连贯异样,不如多测一组 '-1' 数据会不会报错。 单元测试既有开发成本也有保护老本《单元测试的艺术》中说到单元测试的老本:个别残缺的单元测试和开发成本是统一的(开发一天;测试一天,这件事其实很难掂量性价比)。而且单元测试写的越多,保护老本也就开始一直减少,当我的项目的性能一直变动,单元测试也要跟着调整,也就是说,你的麻利开发和疾速迭代会被单元测试所连累,你须要掂量老本。 并不举荐一开始就写在国内(拥抱变动),特地是中小型公司,往往业务性能是不稳固的,需要始终在扭转,所以其实我并不举荐一开始就写单元测试。有工夫不如将功能测试全副通过,这样能更快上线,更快有反馈。尤其是像国内这种以产品为导向的开发方式,需要变动往往只须要一个小时,前一秒是产品经理认为能够,下一秒老板就说不行的状况太多了。在业务稳固之后,缓缓补,我集体真的感觉并不是一件错事。当然,大公司为了保障业务牢靠,也有测试左移等等思路,并且一些有着残缺布局并且实施方案的我的项目,TDD(测试驱动开发)也未尝不可,总之也是须要具体情况具体分析的。 PS:我也已经有一个我的项目领会过测试驱动开发,写了一大堆测试用例和单元测试之后,当理论在开发性能的时候还再回过头来批改测试用例 Golang web 根本分层测试思路前面,我将会从一个最根底的 web 开发的思路,分层进行阐明(如下所示),如何编写 Golang 在 Web 开发中的单元测试,以及其中能够应用哪些工具来帮忙咱们疾速实现和测试。 repo 数据层:间接拜访数据库或者缓存这一层的单元测试service 逻辑层:针对业务逻辑层的单元测试API 接口层:http 的接口层如何进行正当的测试其余测试相干:如何做表格驱动测试,bentchmack 测试,测试覆盖率单元测试是一场辛苦的修行,来让咱们开启一个 Golang 单测之旅 ~

March 6, 2023 · 1 min · jiezi

关于单元测试:一台不容错过的Java单元测试代码永动机

作者:京东批发 陈志良 作为一名京东的软件匠人,咱们开发的软件撑持着数亿的用户,责任是重大的,因而咱们深深地敬畏每一行代码,那如何将咱们的失误降到最低呢?那就是单元测试,它会让咱们建立对代码的自信心。为此咱们冀望能打造一台生产Java单元测试代码的“永动机”,源源不断地为开发者生产代码,辅助大家高效地做好单元测试,节俭精力能投入到更多的业务翻新中去。一、开发者对代码的自信心来自哪里?京东随着业务高速倒退,咱们缔造的、承载着数亿用户的、功能强大的零碎,在通过十多年的打磨,也变得日益简单。作为JD软件开发者,咱们是骄傲的,但咱们承当的责任也是重大的。咱们每一次的翻新,就像打造一座下图这样的过山车。咱们在为客户带来如此顶级体验的同时,更重要的是保障每一次的旅行都能够平安地着陆。所以咱们深深敬畏每一行代码,致力将咱们的失误降到最低,为业务保驾护航。 然而,业务的迭代速度之快,交付压力之大,作为“过山车”的缔造者,你是否有以下的经验? 1)每一次上线也像坐了一次过山车呢? 2)你亲手打造的“过山车”,本人是否亲自体验过呢? 3)你是否曾对测试同学说,“你们先上去坐坐看,遇到了问题再下来找我”? 如果你的答案是:每一次上线也像坐了一次过山车,咱们本人打造的“过山车”本人不敢坐,咱们的代码要靠测试同学兜底,那么就阐明咱们对本人的代码是不足信念的,咱们的工作还有待晋升的空间;反之则阐明,作为一个开发者你曾经相当优良了。 那么如何让咱们开发者建设对本人代码的信念呢,一般来说有两种形式: 1)对“过山车”的每个整机都进行充沛的测试,保障每一部分在各种场景下都能够失常工作,对所有的异样也可能解决切当,这即是单元测试。 2)对“过山车”启动前做好充沛“查看”,这即是代码评审,咱们邀请其余大佬帮咱们把关,及时发现问题。 这两局部工作在开发阶段都是必要的工作,二者缺一不可。 代码评审是借助了外力,单元测试则是内功,靠本人,靠开发者自测来加强对代码的信念。 本文次要和大家一起探讨单元测试,如何把这单元测试的内功练好。 二、做好单测,慢即是快对于单元测试的认识,业界同仁了解多有不同,尤其是在业务变动疾速的互联网行业,通常的问题次要有,必须要做吗?做到多少适合?当初没做不也挺好的吗?甚至一些大佬们也是存在不同的认识。咱们如下先看一组数字: “在 STICKYMINDS 网站上的一篇名为 《 The Shift-Left Approach to Software Testing 》 的文章中提到,如果在编码阶段发现的缺点只须要 1 分钟就能解决,那么单元测试阶段须要 4 分钟,功能测试阶段须要 10 分钟,零碎测试阶段须要 40 分钟,而到了公布之后可能就须要 640 分钟来修复。”——来自知乎网站节选 对于这些数字的准确性咱们暂且持保留意见。大家能够想想咱们理论中遇到的线上问题大略须要耗费多少工时,除了要疾速找到bug,修复bug上线,还要修复因为bug引发的数据问题,最初还要复盘,看后续如何能防止线上问题,这样下来激进预计应该不止几人日吧。所以这篇文章作者所做的调研数据可信度还是很高的, 缺点发现越到交付流程的后端,其修复老本就越高。 有人说写单测太消耗工夫了,会缩短交付工夫,其实不然: 1)研测同学大量的往返交互比编写单测的工夫要长的多,集成测试的工夫被拖长。 2)没通过单测的代码bug会多,开发同学忙于修复各种bug,对代码debug跟踪调试找问题,也要耗费很多精力。 3)前期的线上问题也会须要大量的精力去补救。 如果有了单元测试的代码,且能实现一个较高的行覆盖率,则能够将问题尽可能毁灭在开发阶段。同时有了单测代码的积攒,每次代码改变后能够提前发现这次改变引发的其余关联问题,上线也更加释怀。单测尽管使提测变慢了一些,软件品质更加有保障,从而节俭了后续同学的精力,从整体看其实效率更高。 所以做好单测,慢即是快。 咱们团体技术委员会大佬们从去年开始也在倡导大家做单元测试, 做为一名开发者咱们须要对本人的代码品质负责, 也更能体现咱们大厂开发者的工匠精力。 三、如何编写单元测试1、单元测试的支流框架及核心思想以下咱们先通过一个案例介绍下支流框架的思维。下图为一个简略的函数执行逻辑,在函数体内间接调用了函数1、函数2、函数3,间接调用了函数2.1,其中1和2别离是一般函数,2.1和3波及到内部零碎调用,例如JSF、Redis、MySQL等操作,最初返回后果。 代码大抵如下: public class MyObject { @Autowired private RedisHelper redisHelper; public MyResult myFunction(InputParam inputParam){ MyResult myResult = new MyResult();//一般代码块 if(inputParam.isFlag()) { //如果标记flag为true,则执行函数1 String f1 = invokeFunction1(); //调用函数3,函数3封装了redis中间件操作 String f3 = redisHelper.get(f1); myResult.setResult(f3); } else { //调用函数2,在函数2外部又调用近程服务接口2.1 String f2 = invokeFunction2(); myResult.setResult(f2); } return myResult; } 在当下微服务时代,零碎间的交互变得更加日益简单,以上图例只是简化的例子,理论零碎中的上下游内部依赖多达十几个,甚至几十个。 ...

February 21, 2023 · 1 min · jiezi

关于单元测试:mockito入门

前言最近在我的项目中跑单元测试发现间接应用springboot自带的测试,一整套跑起来破费数十分钟,这是无法忍受的,思考到性能的特殊性,想到了Spring测试包自带的mockito单元测试,所以进行首次尝试应用。 测试代码pom包 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-inline</artifactId> <version>4.5.1</version> <scope>test</scope> </dependency> import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author Steven * @Date 2023/1/30 15:45 */ @Service public class OrderA { @Autowired private OrderC orderC; public int print() { System.out.println("D = "+ OrderD.getResult()); System.out.println(orderC.print(2)); System.out.println("hello world"); return -1; } } import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author Steven * @Date 2023/1/30 15:46 */ @Service public class OrderB { @Autowired private OrderA order; public boolean test() { System.out.println("order B test()"); System.out.println("order value = " + order.print()); System.out.println("order B hello world"); return true; } } import org.springframework.stereotype.Service; /** * @author Steven * @Date 2023/1/30 16:43 */ @Service public class OrderC { public int print(int a) { System.out.println("hello"+ a); return -1; } } import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * @author Steven * @Date 2023/1/30 17:32 */ @Component public class OrderD { private static OrderC orderC; @Autowired public OrderD(OrderC orderCa) { System.out.println("=================="); orderC = orderCa; } public static int getResult(){ System.out.println("hhhhh====" + OrderE.print()); return orderC.print(3); } } /** * @author Steven * @Date 2023/1/31 14:04 */ public class OrderE { public static int print() { System.out.println("ahhahahahahhaha"); return 1111111; } }次要测试类 ...

February 1, 2023 · 2 min · jiezi

关于单元测试:基于Unittest框架使用PythonSeleniumWebdriver的WebUI自动化测试项目应用实例附源码

@TOC 1、我的项目背景测试背景:在业务零碎的web页面,有一个分辨率设置性能,而这个性能是自定义的一个区间,用户能够设置分辨率800600到20482048, 共计1809801个分辨率,如果人工去进行遍历的话,预计得用半年工夫,十分吃力解决方案:应用webUI自动化管制分辨率性能的输出,其中每次输出都不反复,遍历所有的分辨率遍历数据解决:如果在脚本中惟一取值,间接由代码生成须要的数据的话,效率十分慢;所以把1809801个分辨率数据间接在txt文本中写入,只须要关上一次,而后每次从txt取值,直到取完为止业务UI图: 2、框架环境Python 3.5Python的sendmail、xlrd、HTMLtestRuner、CSV、ConfigParser、Json模块SeleniumPycharm 3、业务实现思路设置界面,批改输出源的分辨率的高和宽,以及刷新率,进行利用设施界面,查看对应输出源的分辨率信息把设置界面输出的分辨率信息和设施界面的显卡返回的分辨率信息进行比照,判断设置是否OK4、业务后果判断把设置界面输出的分辨率信息和设施界面的显卡返回的分辨率信息进行比照,判断设置是否OK,次要有两种状况: 超出带宽:在脚本中退出判断信息,如果输出的值依照计算公式大于165M带宽,才判断为超出带宽返回异样:输出的和返回的值不统一,这种状况个别保留数据,具体分析,如下示例: 5、数据处理对于运行的后果数据处理,目前反对三种形式: 把测试用例后果,通过HtmlTestRunner.py库封装成测试用例集,而后通过SendMail.py库,发送邮件给我的项目组成员。示例: 应用Eclipse开发平台,把测试后果的Console,保留到log中,实时抓取运行过程和后果数据(这个能够疏忽,后续间接在代码中加log) 把测试后果,间接保留到config的配置文件中,间接查看 6、框架阐明 7、操作阐明关上all_test,py批改接管邮箱地址和保留保留门路关上tools中的sendMail.py批改发送者的邮箱地址在Utils中的Settings3写业务模块的性能(Settings1和Settings2是多余的)在test_case中test_Settings_Custom_Resolution.py写测试用例执行all_test.py 8、数据存储成果 9、源码地址https://github.com/NoamaNelson/UnittestWebUIFour

January 11, 2023 · 1 min · jiezi

关于单元测试:unittest中使用ddt后生成的测试报告名称如何修改如testapi0修改成testapi0titile

批改前:Unittest应用ddt后生成的测试报告用例名称为:即就是,以“test_xx_数字”为格局的用例名称,感觉满足不了咱们的测试需要,不够直观。那么怎么批改呢? 查看ddt源码def mk_test_name(name, value, index=0): """ Generate a new name for a test case. It will take the original test name and append an ordinal index and a string representation of the value, and convert the result into a valid python identifier by replacing extraneous characters with ``_``. We avoid doing str(value) if dealing with non-trivial values. The problem is possible different names with different runs, e.g. different order of dictionary keys (see PYTHONHASHSEED) or dealing with mock objects. Trivial scalar values are passed as is. A "trivial" value is a plain scalar, or a tuple or list consisting only of trivial values. """ # Add zeros before index to keep order index = "{0:0{1}}".format(index + 1, index_len, ) if not is_trivial(value): return "{0}_{1}".format(name, index) try: value = str(value) except UnicodeEncodeError: # fallback for python2 value = value.encode('ascii', 'backslashreplace') test_name = "{0}_{1}_{2}".format(name, index, value) return re.sub(r'\W|^(?=\d)', '_', test_name) 从办法mk_test_name中,咱们看到该办法的形容是“Generate a new name for a test case.”,即就是为测试用例创立一个名称,那么改这个办法就行了办法中返回的是name和index,即"{0}_{1}".format(name, index)那么就明确了,咱们改返回的内容就行了批改后def mk_test_name(name, value, index=0): """ Generate a new name for a test case. It will take the original test name and append an ordinal index and a string representation of the value, and convert the result into a valid python identifier by replacing extraneous characters with ``_``. We avoid doing str(value) if dealing with non-trivial values. The problem is possible different names with different runs, e.g. different order of dictionary keys (see PYTHONHASHSEED) or dealing with mock objects. Trivial scalar values are passed as is. A "trivial" value is a plain scalar, or a tuple or list consisting only of trivial values. """ # Add zeros before index to keep order index = "{0:0{1}}".format(index + 1, index_len, ) if not is_trivial(value) and type(value) is not dict: # 减少的中央,减少value的字典判断 return "{0}_{1}_{2}".format(name, index, value.name) # 批改的中央,减少返回的值 if type(value) is dict: # 减少的中央 try: # 减少的中央 value = value["name"] + "_" + value["function"] # 减少的中央,name和function必须是execl用例中整正存在的表头,这里我是把两个表头合并了(name是我表格中接口的名称,function是表格中接口的性能形容) except: # 减少的中央 return "{0}_{1}".format(name.index) # 减少的中央 try: value = str(value) except UnicodeEncodeError: # fallback for python2 value = value.encode('ascii', 'backslashreplace') test_name = "{0}_{1}_{2}".format(name, index, value) # 批改的中央 return re.sub(r'\W|^(?=\d)', '_', test_name) ...

January 10, 2023 · 2 min · jiezi

关于单元测试:unittest使用parameterized参数化后如何调用添加到测试套件中

写了一个Unittest+Python+execl的一个接口自动化,在参数化的时候遇到了一个问题。具体的“坑”如下 要实现的需要在execl中波及或写接口测试用例,而后读取execl中每一行的数据,每一行数据就相当于一条用例 需要实现path = "F:\InterFace_JIA1\dataconfig\source_user_case.xlsx"params_list = TestRunCase(path).get_params()print("params_list:",params_list)class TestRun(unittest.TestCase): #params_list = [(2, 100000, 100001),(1, 100000, 100003)] @parameterized.expand(params_list) # 这里参数化了params_list def test_run(self, name, expect_res, actual_res): self.assertEqual(expect_res, actual_res)if __name__ == '__main__': unittest.main()用例为:后果为:先不论接口是不是有问题,从这个运行看,流程是OK的参数化后调用退出测试条件中if __name__ == '__main__': suite = unittest.TestSuite() now = datetime.datetime.now().strftime('%Y-%m-%d_%H_%M_%S') filename = "./report/" + now + '_result.html' fp = open(filename, 'wb') suite.addTest(TestRun('test_run')) runner = HTMLTestRunner.HTMLTestRunner( stream=fp, title=u'测试后果', description=u'全副测试用例') runner.run(suite) fp.close() time.sleep(2) print("sdasdasdasdasdasdsa")后果出错TypeError: 'NoneType' object is not callable 排查剖析应用unittest.defaultTestLoader.discover,打印所有的case,发现用例格局是“test_run_0” <unittest.suite.TestSuite tests=[<unittest.suite.TestSuite tests=[<main.run.TestRun testMethod=test_run_0>, <main.run.TestRun testMethod=test_run_1>]>]>if __name__ == '__main__': suite = unittest.defaultTestLoader.discover('./', pattern='run.py') for case in suite: print (case)从新调用把test_run改成test_run_0 ...

January 10, 2023 · 1 min · jiezi

关于单元测试:Go-test-单元测试用起来

什么是单元测试(unit testing)单元测试,是指对软件中的最小可测试单元进行检查和验证 单元就是人为规定的最小的被测功能模块 一般来说,要依据理论状况去断定其具体含意,如 C 语言中单元指一个函数,Go 外面也单元也是一个函数 单元测试是在软件开发过程中要进行的最低级别的测试流动,软件的独立单元将在与程序的其余局部相隔离的状况下进行测试。 单元测试,咱们平时也叫它单测,平时开发的时候,也须要写一些 demo 来测试咱们的我的项目中的函数或者某个小性能 go test 单元测试GO 语言外面的单元测试,是应用规范库 testing 有如下简略规定: 导入 test 规范库单测文件名,前面跟上_test单测文件中的函数名为 Test结尾,且参数必须是 t *testing.T简略例子:写一个简略的例子,增加后缀和前缀 .├── cal.go├── cal_test.go├── lll└── sub.gocal.go package mainfunc Addprefix(str string) string { return "hello_"+str}func Addsuffix(str string) string { return str+"_good"}cal_test.go package mainimport "testing"func TestAddprefix(t *testing.T) { Addprefix("xiaomotong")}func TestAddsuffix(t *testing.T) { Addsuffix("xiaomotong")}sub.go package mainfunc MyAdd(a int, b int) int { if a+b > 10{ return 10 } return a+b}func MySub(one int, two int) int{ if one - two < 0{ return 1 } return one - two}sub_test.go ...

October 29, 2022 · 2 min · jiezi

关于单元测试:golang单元测试一简单函数测试

0.1、索引https://blog.waterflow.link/articles/1663688140724 1、简介单元测试是测试代码、组件和模块的单元函数。单元测试的目标是革除代码中的谬误,减少代码的稳定性,在更改代码时提供正确性。单元测试是软件测试的第一级,而后是集成测试和 ui 测试。 2、编写测试代码首先测试文件的命名必须以_test.go结尾,测试方法必须以Test结尾 咱们创立一个testexample我的项目,执行go mod init初始化我的项目。 而后创立一个uri.go的文件,外面的代码是我摘抄自golang的amqp包中的一段解析ampq url的代码,具体链接 package uriimport ( "errors" "net" "net/url" "strconv" "strings")var errURIScheme = errors.New("AMQP scheme must be either 'amqp://' or 'amqps://'")var errURIWhitespace = errors.New("URI must not contain whitespace")var schemePorts = map[string]int{ "amqp": 5672, "amqps": 5671,}var defaultURI = URI{ Scheme: "amqp", Host: "localhost", Port: 5672, Username: "guest", Password: "guest", Vhost: "/",}// URI represents a parsed AMQP URI string.type URI struct { Scheme string Host string Port int Username string Password string Vhost string}// ParseURI attempts to parse the given AMQP URI according to the spec.// See http://www.rabbitmq.com/uri-spec.html.//// Default values for the fields are://// Scheme: amqp// Host: localhost// Port: 5672// Username: guest// Password: guest// Vhost: ///func ParseURI(uri string) (URI, error) { builder := defaultURI // 如果链接中有空字符串,返回默认值和谬误 if strings.Contains(uri, " ") { return builder, errURIWhitespace } // 解析url为构造体,解析失败返回默认值和谬误 u, err := url.Parse(uri) if err != nil { return builder, err } // 依据scheme获取默认端口 defaultPort, okScheme := schemePorts[u.Scheme] if okScheme { builder.Scheme = u.Scheme } else { // 获取不到就返回默认值和谬误 return builder, errURIScheme } host := u.Hostname() port := u.Port() if host != "" { builder.Host = host } if port != "" { port32, err := strconv.ParseInt(port, 10, 32) if err != nil { // 解析进去的端口转整型失败,返回最新的URI和谬误 return builder, err } builder.Port = int(port32) } else { builder.Port = defaultPort } if u.User != nil { builder.Username = u.User.Username() if password, ok := u.User.Password(); ok { builder.Password = password } } if u.Path != "" { if strings.HasPrefix(u.Path, "/") { if u.Host == "" && strings.HasPrefix(u.Path, "///") { // net/url doesn't handle local context authorities and leaves that up // to the scheme handler. In our case, we translate amqp:/// into the // default host and whatever the vhost should be if len(u.Path) > 3 { builder.Vhost = u.Path[3:] } } else if len(u.Path) > 1 { builder.Vhost = u.Path[1:] } } else { builder.Vhost = u.Path } } return builder, nil}func (uri URI) String() string { authority, err := url.Parse("") if err != nil { return err.Error() } authority.Scheme = uri.Scheme if uri.Username != defaultURI.Username || uri.Password != defaultURI.Password { authority.User = url.User(uri.Username) if uri.Password != defaultURI.Password { authority.User = url.UserPassword(uri.Username, uri.Password) } } authority.Host = net.JoinHostPort(uri.Host, strconv.Itoa(uri.Port)) if defaultPort, found := schemePorts[uri.Scheme]; !found || defaultPort != uri.Port { authority.Host = net.JoinHostPort(uri.Host, strconv.Itoa(uri.Port)) } else { // JoinHostPort() automatically add brackets to the host if it's // an IPv6 address. // // If not port is specified, JoinHostPort() return an IP address in the // form of "[::1]:", so we use TrimSuffix() to remove the extra ":". authority.Host = strings.TrimSuffix(net.JoinHostPort(uri.Host, ""), ":") } if uri.Vhost != defaultURI.Vhost { // Make sure net/url does not double escape, e.g. // "%2F" does not become "%252F". authority.Path = uri.Vhost authority.RawPath = url.QueryEscape(uri.Vhost) } else { authority.Path = "/" } return authority.String()}先不必思考下面函数的复杂性,咱们的目标很简略,就是要测试ParseURI函数。那如何测试呢?首先咱们须要创立一个uri_test.go文件,个别和须要测试的uri.go在同一个目录下。其次文件的包名也须要和uri.go 统一。 ...

September 20, 2022 · 11 min · jiezi

关于单元测试:淘系用户平台技术团队单元测试建设

简介:单元测试是工程交付前品质保障的第一环,也无疑是软件工程品质保障的重要基石,无效的单元测试可能提前发现90%以上的代码Bug问题,同时也能避免代码的腐化,在工程重构演进时起到至关重要的作用。 作者 | 问元起源 | 阿里开发者公众号 为什么须要单元测试纵观优良的开源工程,齐备的单元测试总是必须的条件。通过这些单元测试,咱们能够充沛理解代码中相干类和办法的作用和外围逻辑,相熟各种场景的运行状况。同时也因为有了单元测试,开源作者在承受各种feature的代码提交时才有稳固平安的保障。其实单元测试的重要性所有开发同学应该都了然于胸,同样TDD(测试驱动开发)也不是一个新的概念,然而真当咱们落地实际时,又总会找出各种各样的理由来劝服本人下次肯定好好写单元测试,这一次先放过本人。这些理由无外乎,开发周期太紧了; 测试同学能保障性能正确性;写单元测试代码量比业务代码还大; 又不是不能跑。所以尽管咱们总是在追赶工程师文化,却又时不时放荡在放弃工程师底蕴的路上。 单元测试是工程交付前品质保障的第一环,也无疑是软件工程品质保障的重要基石,无效的单元测试可能提前发现90%以上的代码Bug问题,同时也能避免代码的腐化,在工程重构演进时起到至关重要的作用。 怎么写单元测试好的单元测试的几个要点摘自阿里巴巴开发规约 单元测试必须恪守AIR准则,单元测试必须具备Automatic(自动化),Independent(独立性),Repeatable(可反复)性;单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须齐全自动化才有意义。输入后果须要人工查看的测试不是一个好的单元测试;单元测试要保障测试粒度足够小。单元测试测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,个别是办法级别;单元测试要恪守BCDE准则,Border,边界值测试,包含循环边界、非凡取值、非凡工夫点、数据程序等;Correct,正确的输出,并失去预期的后果;Design,与设计文档相结合,来编写单元测试;Error,强制错误信息输出(如:非法数据、异样流程、非业务容许输出等),并失去预期的后果;外围业务、外围利用、外围模块的增量代码要确保单元测试通过;单元测试编码范式这里次要以Mockito单元测试框架为模版 Mock : 通过when().thenReturn/thenAnswer/thenThrow 或者doReturn().when()等mock形式将依赖类办法进行模仿,模仿服务依赖或者两头后果DO : 调用被测试类办法,执行测试链路Verify : 校验执行后果正确性,通过Assert校验数据后果精确,通过Verify校验链路执行精确,通过expected=Exception.class校验异样链路public class Test { // 0. 依赖类 @Mock DependencyClass dependencyClass; // 0. 待测试类 @InjectMocks TestClass testClass; @Before public void setUp() { MockitoAnnotations.initMocks(this); } @Test public void testMethod() { // 1. Mock, 依赖办法,结构中间层数据 when(dependencyClass.someMehod(any())).thenReturn(mockData()); // 2. Do, 调用被测试类 Result result = testClass.testMehod(); // 3. Verify, 校验后果数据 Assert.assertEquals("some expected result string", result.getModel()); }}当然写单元测试用例尽管套路比拟模版化,然而咱们也要充分利用单元测试框架(Junit/Mockito/PowerMock/Spock),把握其中的一些技巧,能力写出快准狠的单元测试用例,这也是研发同学必须要把握的基本功。对于如何利用单元测试框架这里不再赘述(具体能够参考阿里技术《Java编程技巧之单元测试用例编写流程》)。 ...

May 16, 2022 · 1 min · jiezi

关于单元测试:项目里的UT越来越慢怎么办

我的项目里的UT越来越慢,怎么办?JUnit是一个Java语言的单元测试框架。它由Kent Beck和Erich Gamma建设,逐步成为源于Kent Beck的sUnit的xUnit家族中最为胜利的一个。 JUnit有它本人的JUnit扩大生态圈。少数Java的开发环境都曾经集成了JUnit作为单元测试的工具。它曾经倒退有20余年历史了当初咱们的我的项目在Jenkins流水线上每次部署时,随着code越来越多,UnitTest这部分Stage也每次都跑得最慢,怎么能够加快速度呢? 能够试着从code层面去尝试 refactor,比方将Junit4降级到更好效率更快的Junit5 JUnit5JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage JDK Version >= 8 (而 JUnit4是要求 JDK Version >= 5) JUnit平台它定义了TestEngine用于开发在平台上运行的新测试框架的APIJUnit Jupiter它具备所有新的junit正文和TestEngine实现,以运行应用这些正文编写的测试JUnit Vintage反对在JUnit 5平台上运行JUnit 3和JUnit 4编写的测试JUnit5的规范用法 集成Spring(The SpringJUnitConfig and SpringJUnitWebConfig Annotations in Spring 5 | Baeldung) 应用JUnit5的拓展模型个性,@ExtendWith(SpringExtension.class),能够应用Spring的上下文装载性能,而不必去应用重量级的@SpringBootTest集成Mockito (Mockito and JUnit 5 - Using ExtendWith | Baeldung) 引入JUnit5和Mockito的maven依赖配置Surefire Plugin,使得测试代码都运行在新的JUnit平台上有必要兼容JUnit4的测试代码的话,还需退出JUnit5 vintage-engine的maven依赖@ExtendWith(MockitoExtension.class), 集成Mockito,能够应用@Mock, @Spy, @InjectMocks等注解,不便咱们写UTJUnit 4 VS JUnit 5注解的区别注解的应用区别 罕用的//Exception@Test(expected = Exception.class) // JUnit4@Test(timeout = 1) // JUnit4//TimeOutAssertions.assertThrows(); // JUnit5Assertions.assertTimeout(1); //JUnit5@RunWith 和 @ExtendWith(最大个性差别) ...

April 16, 2022 · 1 min · jiezi

关于单元测试:适用于-androidjvm-的单测生成器-randunit

背景在继续交付越来越风行的明天,单测作为保障 CI 品质的重要一环也开始在国内被器重起来。 不过在单测上大家的态度还是比拟矛盾的,放心的事件次要有两个: 从 0 到 1 的第一步不晓得从何下手万一花大力量做了之后发现没什么用怎么办然而短少这一环,在整个 devops 流程中很多编译时重大问题会被延缓到运行时裸露,这对于我的项目效率妨碍也不小。。 于是就有了灵感起源: 主动生成一系列冒烟级别单测用例,并能发现重大问题接入成本低,能无痛与现有流程联合能对 android 失效(我司重挪动端 做了什么RandUnit 取义自 Random UnitTest,他会: 依据提供的包名或入口类,搜寻所有相干的待测试类与办法依据搜寻后果,为每个办法生成一系列 statements 用于测试像惯例单测流程个别,在 junit 上运行这些 statements,失去测试后果而这所有只须要一次简略的复制粘贴: import com.williamfzc.randunit.env.NormalTestEnvimport com.williamfzc.randunit.models.StatementModelimport com.williamfzc.randunit.scanner.ScannerConfigimport org.junit.Testimport org.junit.runner.RunWithimport org.junit.runners.Parameterized@RunWith(Parameterized::class)class MinExampleTest(private val statementModel: StatementModel) { companion object { private val testEnv = NormalTestEnv() private const val packageName = "com.your.package" private val cases by lazy { val scannerConfig = ScannerConfig() scannerConfig.includeFilter.add(packageName) RandUnit.collectStatementsWithPackage(packageName, scannerConfig) } @JvmStatic @user3ters(name = "{0}") fun data(): Collection<StatementModel> { return cases } } @Test fun runStatements() { testEnv.runStatementInSandbox(statementModel) }}因为它是非法的 junit 用例,所以你能够在 ide 里间接运行它。间接 run with coverage 的话: ...

January 20, 2022 · 1 min · jiezi

关于单元测试:基于链路思想的SpringBoot单元测试快速写法

简介:本文更偏差实际而非方法论,所提及的SpringBoot单元测试写法亦并非官网解,仅仅是笔者本身感觉比拟不便、效率较高的一种写法。每个团队甚至团队内的每位开发可能都有本人的写法习惯和格调,只有能实现单元测试的成果,就没必要纠结于写法的简略抑或简单。这里也欢送各位大佬们发表认识或分享本人的单测心得,帮忙像笔者这样的新人疾速成长。 作者 | 桃符起源 | 阿里技术公众号 引言: 本文更偏差实际而非方法论,所提及的SpringBoot单元测试写法亦并非官网解,仅仅是笔者本身感觉比拟不便、效率较高的一种写法。每个团队甚至团队内的每位开发可能都有本人的写法习惯和格调,只有能实现单元测试的成果,就没必要纠结于写法的简略抑或简单。这里也欢送各位大佬们发表认识或分享本人的单测心得,帮忙像笔者这样的新人疾速成长。 一 为什么要写单元测试?测试是Devops上极重要的一环,但大多数开发的眼光都停留在集成测试这一环——只有能联调胜利,那么我这次筹备上线的个性肯定是没问题的。 诚实抵赖,我已经是这样的可能当初也还是这样。作为非科班出身的笔者,研究生毕业后就立刻进入了同在杭州的xx厂,先后参加了外部Devops平台建设和xx云Paas我的项目垦荒,在这两个我的项目中,开发 > 测试是很失常的场景,甚至局部测试也是原开发情谊客串的:因为短少业余的测试人员,开发往往须要兼顾集成测试甚至是线上测试的活儿。为了提高效率,我将一部分罕用的测试用例保护在了外部的自动化测试平台上。即便如此,我仍能清晰地感觉到,测试所能笼罩的场景比比皆是,以至于每次自信地上线大个性后,都会因一些奇怪的问题而定位到大半夜。幸好前面遇到了一位资深大佬,在code review时,他间接点出我不写单元测试的坏习惯,并用本身惨痛的线上教训反复强调单测的重要性。 当然上述只是我的亲身经历,勉强作为日常闲聊的谈资。如果想要深刻了解单元测试的重要性,举荐Google上搜寻the importance of unit test关键字,能够感触下不同国家、不同畛域的程序员对单元测试的不同了解,想必能有更大的播种。 二 为什么举荐链路思维?深刻接触单元测试,开发难免会遇到以下场景: 应该如何设计测试用例?应该如何编写测试用例?测试用例的品质该如何断定?刚开始学习写单元测试,我也曾参考并尝试过网上形形色色的写法。这些写法可能用到了不同的单测框架,也可能偏重了不同的代码环节(例如特定的某个service办法)。一开始我为本人可能纯熟应用多种单测框架而沾沾自喜,但随着工作的推动,我逐步意识到,单元测试中重要的并不是框架选型,而是如何设计一套优良的用例。之所以用"一套"而不是"一个",是因为在咱们的业务代码中,逻辑往往并非"一帆风顺",有许多if-else会妆点咱们的业务代码。显然对于这类业务代码,"一个"测试用例无奈齐全满足所有可能呈现的场景。如果为了偷懒,尝试仅仅用"一个"用例去笼罩主流程,无异于给本人埋了个雷——线上场景可没"一个"用例这么简略! 我开始专一于测试用例的设计,从输入输出开始,从新扫视已经开发过的代码。我发现,如果将某个controller办法作为入口,那这一套业务流程能够当做一条链路,而上下文中所关联的service层、dao层、api层的各办法都能够作为链路上的各环节。通过绘制链路图,将各环节依据是否关联内部零碎大抵分成黑、白两类,整套业务流程和各环节的潜在分支便会变得清晰,测试用例便从"一个"自然而然地变成了"一套"。此处多提一嘴,链路思维设计用例的根底是构造清晰、圈复杂度可管制的代码格调,如果开发的时候仍然尊敬"论文式"、"一刀流",在单个办法内"简明扼要",那链路式将是一个微小的累赘。 编写测试用例其实不是一件吃力的事,对于深耕业务代码的开发而言,编写测试用例便像是做一盘小菜,举手可为。于我而言,现在写测试用例所破费的工夫甚至没有设计测试用例的工夫长(凸显用例设计的重要性但也有可能是我对测试用例的设计还不够纯熟)。在测试框架选型上,我更习惯于Junit+Mockito的组合,起因仅仅是相熟与简略,且参考文档亘古未有。如果各位曾经有本人习惯的框架和写法,也不用照搬本文所提及的货色,毕竟单测是为了better code,而不是自找麻烦。 但无论测试用例如何设计或是如何编写,我始终认为,在不思考测试代码的格调和标准的前提下,掂量测试用例品质的外围指标是分支覆盖率。这也是我举荐链路思维的一大起因——从入口登程,遍历链路上各个环节的各个分支,遇到妨碍就Mock;相比于别离单测各个独立办法,单测链路所须要的入参和出参更加清晰,更是大大节俭了编写测试代码所需的工夫老本!计算分支覆盖率的工具有很多,例如本地的JaCoCo或是各类云化测试工具。试想,每当看到单测完满地笼罩了本人所提交的个性代码时,心里是不是释怀了许多? 三 如何用链路思维设计/结构单测?作为程序员,大家更为相熟的链路概念应该是全链路压测。 全链路压测简略来说,就是基于理论的生产业务场景、零碎环境,模仿海量的用户申请和数据对整个业务链进行压力测试,并继续调优的过程,实质上也是性能测试的一种伎俩。... 通过这种办法,在生产环境上落地常态化稳固压测体系,实现IT零碎的长期性能稳固治理。 如果将残缺的业务流程视作全链路,那作为业务链上的一环,即某个后端服务,它其实也是一个微链路。这里以自上而下的开发流程为例,对于新增的性能接口,咱们会习惯性地由controller开始设计,而后构建service层、dao层、api层,最初再精益求精地加些aop。如果以链路思维,将简单的流程拆成各个链路的各个环节,那这样的代码性能清晰,保护起来也相当不便。我十分认同 限度单个办法行数<=50 的代码门禁,对于简明扼要的代码“论文”,想必没有哪位接手的同学脸上能露出笑容的;针对这类代码,我认为clean code的优先级比补充单测用例更高,连逻辑都无奈理清,即使硬着头皮写出单测用例,后续的调试和保护工作量也是不可意料的(试想,如果前面有位A同学接手了这块代码,他在“论文”中加了xx行导致ut失败了,他该如何去定位问题)。 简略画个图来强调一下我的观点。这是一张"用户买猪"的性能逻辑图。以链路思维,开发人员将整套流程拆分为相应的链路环节,涵盖了controller、service、dao、api各层;整条链路清晰明了,只有搭配欠缺的上下文日志,定位线上问题亦是轻而易举。 当然,基于链路思维的开发还远远不够,在补充单测用例时,咱们同样也能用链路思维来结构测试用例。测试用例的要求很简略,须要笼罩controller、service等自主编写的代码(多分支场景也须要齐全笼罩),对于周边关联的零碎能够采纳Mock进行屏蔽,对于Dao层的SQL能够视需要决定是否Mock。秉承这个思路,咱们能够对“用户买猪”图进行革新,将容许Mock的环节涂灰,从而变成咱们在编写单元测试用例时所须要的“虚构用户买猪”图。 四 疾速写法实际案例1 疾速写法的外围步骤有哪些?疾速写法的入口是controller层办法,这样对于controller层存在的大量逻辑代码也能做到笼罩。 设计测试用例的输出与预期输入 设计测试用例的目标不仅仅是跑通主流程,而是要跑通全副可能的流程,即所谓的分支全笼罩,因而设计用例的输出与输入尤为重要。即使是新增分支的增量批改(例如加了一行if-else),也须要补充相应的输出与预期输入。十分不倡议依据单测运行后果批改预期后果,这阐明原先的代码设计有问题。 确定链路上的全副Mock点 Mock点的判断根据是链路上该环节是否依赖第三方服务。强烈建议在设计前画出大略的性能流程图(如”用户买猪“图),这能够大大提高确定Mock点的速度和准确性。 收集Mock点的模仿返回数据 确定Mock点后,咱们就须要结构相应的模仿返回数据。Mock数据须要思考多个因素: a. 是否与api层对应办法的冀望返回值匹配: 不能把从猪厂返回的Mock数据用牛肉代替 b. 是否与模仿输出数据匹配:用户须要1斤猪肉,不能返回5斤猪肉的数据 c. 是否与api层的所有分支匹配:局部api层会对返回值进行响应码(2xx || 3xx || 4xx)校验,这类场景便须要结构不同响应码的Mock数据 2【开发篇】实在用户买猪该我的项目基于PandoraBoot构建,手动降级SpringBoot版本至2.5.1,应用Mybatis-plus组件简化Dao层开发过程。上面选取了上文图中所波及的重要办法进行展现,仅实现了简略的业务流程,零碎框架和工程构造能够参考代码仓。 业务对象 PorkStorage.java - 猪肉库存的数据库实体类/** * 猪肉库存的数据库实体类 */@Data@NoArgsConstructor@AllArgsConstructor@Builder@TableName(value = "pork_storage", autoResultMap = true)public class PorkStorage { @TableId(value = "id", type = IdType.AUTO) private Long id; private Long cnt;}PorkInst.java - 猪肉实例,由仓库打包后生成 ...

January 19, 2022 · 3 min · jiezi

关于单元测试:JUnit单元测试

JUnit单元测试一.JUnit介绍JUnit是Java中最有名的单元测试框架,用于编写和运行可反复的测试,少数Java的开发环境都曾经集成了JUnit作为单元测试的工具。好的单元测试能极大的进步开发效率和H5游戏。测试类命名规定:被测试类+Test,如UserServiceTest测试用例命名规定:test+用例办法,如testGetMaven导入junit、sprint-test  <dependencies> <!-- Test Unit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.3.10.RELEASE</version> <scope>test</scope> </dependency> <!-- Json断言测试 --> <dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <version>2.4.0</version> <scope>test</scope> </dependency></dependencies><build> <plugins> <!-- 单元测试插件 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.20</version> <dependencies> <dependency> <groupId>org.apache.maven.surefire</groupId> <artifactId>surefire-junit4</artifactId> <version>2.20</version> </dependency> </dependencies> <configuration> <!-- 是否跳过测试 --> <skipTests>false</skipTests> <!-- 排除测试类 --> <excludes> <exclude>**/Base*</exclude> </excludes> </configuration> </plugin> </plugins></build> 二.Service层测试示例创立Service层测试基类,新建BaseServiceTest.java // 配置Spring中的测试环境@RunWith(SpringJUnit4ClassRunner.class)// 指定Spring的配置文件门路@ContextConfiguration(locations = {"classpath*:/spring/applicationContext.xml"})// 测试类开启事务,须要指定事务管理器,默认测试实现后,数据库操作主动回滚@Transactional(transactionManager = "transactionManager")// 指定数据库操作不回滚,可选@Rollback(value = false)public class BaseServiceTest {}测试UserService类,新建测试类UserServiceTest.javapublic class UserServiceTest extends BaseServiceTest { ...

July 23, 2021 · 1 min · jiezi

关于单元测试:单元测试不规范事后运维两行泪

单元测试好的单元测试应该恪守AIR准则 单元测试在线上运行时,应该感觉像空气(AIR)一样,并不存在,但在测试品质的保障上,的确十分要害的好的单元测试宏观上来说,具备以下的特点: 自动化(A: Automatic)独立性(I: Independent)可反复(R: Repeatable)单元测试应该是全自动执行的,并且是非交互式的 测试用例通常是被定期执行的,执行过程必须齐全自动化才有意义输入后果须要人工查看的测试不是一个好的单元测试单元测试中不准应用System.out来进行人的验证,必须应用assert来验证放弃单元测试的独立性 为了保障单元测试稳固牢靠且便于保护: 单元测试用例之间决不能相互调用不能依赖执行的先后秩序单元测试是能够反复执行的,不能受到外界环境的影响 单元测试通常会被放到继续集成中,每次代码有check in时单元测试都会被执行 如果对外部环境(网络,服务,中间件等)有依赖,容易导致集成机制不可用为了不受外界环境的影响,要求设计代码时就把SUT的依赖改成注入在测试时用spring这样的DI框架注入一个本地(内存)实现或者Mock实现对于单元测试,要保障测试粒度足够小,有助于精确定位问题,单元测试粒度至多是类级别,通常是办法级别的 只有测试粒度小能力在出错时尽快定位到出错地位单元测试不负责查看跨类或者跨零碎的交互逻辑,那是集成测试的畛域外围业务,外围利用,外围模块的增量代码确保单元测试通过 新增代码及时补充单元测试如果新增代码影响了原有代码,确保及时修改单元测试代码必须写在如下工程目录中 :src/test/java, 不容许写在业务代码目录下 源码构建会跳过此目录,而单元测试框架默认扫描此目录单元测试的根本指标: 语句覆盖率达到70%外围模块语句覆盖率和分支覆盖率都要达到100%在工程规约的利用分层中提到的DAO层 ,Manager层,可重用度高的Service, 都应该进行单元测试编写单元测试代码要恪守BCDE规定,以确保被测试模块交付品质: B: Border ,边界值测试, 包含循环边界,非凡取值,非凡工夫点,数据程序等C: Correct ,正确的输出,并失去预期的后果D: Design ,与设计文档相结合, 来编写单元测试E: Error ,强制错误信息输出, 比方非法数据,异样流程,非业务容许输出,并失去预期的后果对于数据库的查问,更新,删除等操作: 不能够假如数据库里的数据是存在的不能够间接操作数据库将数据插入进去必须应用程序插入或者导入数据的形式来筹备数据和数据库相干的单元测试,能够设定主动回滚机制,不给数据库造成脏数据,或者对单元测试产生的数据有明确的前后缀标识 比方在RDC外部的单元测试中,应用RDC_UNIT_TEST_的前缀标识数据对于不可测的代码要做必要的重构,使代码变得可测,防止为了达到测试要求而书写不标准的测试代码在设计评审阶段,开发人员须要和测试人员一起确定单元测试范畴,单元测试最好笼罩所有测试用例单元测试作为一种品质保障伎俩,不要在我的项目公布后补充单元测试用例,须要在我的项目提测前实现单元测试为了更不便地进行单元测试,业务代码须要防止以下状况: 构造方法中做的事件过多存在过多的全局变量和静态方法存在过多的内部依赖存在过多的条件语句: 多层条件语句倡议应用卫语句,策略模式,状态模式重构不要对单元测试存在误会: 认为单元测试是测试的事件认为单元测试代码是多余的.零碎整体性能与各个单元部件的测试失常与否是强相干的认为单元测试代码不须要保护.这样会导致一段时间过后,单元测试简直处于废除的状态认为单元测试与线上故障没有辩证关系.好的单元测试能最大限度地躲避线上故障

June 30, 2021 · 1 min · jiezi

关于单元测试:前端面试每日-31-第800天

明天的知识点 (2021.06.24) —— 第800天 (我也要出题)[html] 应用HTML5实现窗户玻璃雨滴的真切成果[css] 应用CSS3实现响应式win8 metro格调的页面[js] js如何做单元测试?步骤是什么?[软技能] 你有做过基于地图的利用吗?《论语》,曾子曰:“吾日三省吾身”(我每天屡次检查本人)。前端面试每日3+1题,以面试题来驱动学习,每天提高一点!让致力成为一种习惯,让奋斗成为一种享受!置信 保持 的力量!!! 欢送在 Issues 和敌人们一起探讨学习! 我的项目地址:前端面试每日3+1【举荐】欢送跟 jsliang 一起折腾前端,零碎整顿前端常识,目前正在折腾 LeetCode,打算买通算法与数据结构的任督二脉。GitHub 地址 微信公众号欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个Star, 同时欢送微信扫码关注 前端剑解 公众号,并退出 “前端学习每日3+1” 微信群互相交换(点击公众号的菜单:交换)。 学习不打烊,充电加油只为遇到更好的本人,365天无节假日,每天早上5点纯手工公布面试题(死磕本人,愉悦大家)。心愿大家在这虚夸的前端圈里,放弃沉着,保持每天花20分钟来学习与思考。在这变幻无穷,类库层出不穷的前端,倡议大家不要等到找工作时,才狂刷题,提倡每日学习!(不忘初心,html、css、javascript才是基石!)欢送大家到Issues交换,激励PR,感激Star,大家有啥好的倡议能够加我微信一起交换探讨! 心愿大家每日去学习与思考,这才达到来这里的目标!!!(不要为了谁而来,要为本人而来!)交换探讨欢送大家前来探讨,如果感觉对你的学习有肯定的帮忙,欢送点个[Star]

June 24, 2021 · 1 min · jiezi

关于单元测试:单元测试PowerMock

前言PowerMock是一个单元测试框架,能够模仿静态方法,公有办法和final办法等来简化单元测试的编写。本篇文章将联合简略例子对PowerMock的罕用办法进行阐明。 筹备工作一. 注解增加与应用场景在应用PowerMock时须要针对不同场景增加对应注解,次要是@RunWith和@PrepareForTest注解。注解增加和场景对应如下所示。 场景注解模仿final办法@PrepareForTest,@RunWith模仿静态方法@PrepareForTest,@RunWith模仿公有办法@PrepareForTest应用whenNew@PrepareForTest,@RunWith二. 应用PowerMock须要增加的依赖须要引入的依赖如下所示。 <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.23.0</version> <scope>test</scope></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.2</version> <scope>test</scope></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.2</version> <scope>test</scope></dependency>引入mockito-core是为了提供Mockito性能,次要应用到org.mockito.ArgumentMatchers参数占位符,局部状况须要应用到org.mockito.BDDMockito。引入powermock-api-mockito2和powermock-module-junit4是为了提供PowerMock性能,其中powermock-module-junit4中还引入了hamcrest-core,次要是应用其提供的org.hamcrest.MatcherAssert.assertThat和org.hamcrest.Matchers.is进行断言判断。在引入依赖时,须要留神核查Mockito和PowerMock的版本对应关系,否则会报java.lang.ClassNotFoundException: org.mockito.exceptions.Reporter谬误。版本对应关系能够去PowerMock官网进行查问:PowerMock官网,通常状况下,如果引入的mockito-core版本为2.x,则PowerMock的api须要应用powermock-api-mockito2。 注释一. mock public办法public class Mock { public boolean isTrue_1() { return true; }}public class PowerMockTest { @Test public void mockPublic() { Mock mock = PowerMockito.mock(Mock.class); PowerMockito.when(mock.isTrue_1()).thenReturn(false); assertThat(mock.isTrue_1(), is(false)); }}mock public办法时须要应用PowerMockito.mock(办法所在类.class)获取mock进去的对象,这里称之为mock实例,mock实例的办法均为假办法,不对mock实例进行任何操作的状况下,调用mock实例的办法会返回(如果有返回值的话)返回值类型的默认值(零值,比方String返回null,Integer返回0)。如果想要调用mock实例的办法时使其执行实在办法,那么打桩时须要应用thenCallRealMethod(),如下所示。 public class Mock { public boolean isTrue_1() { return true; }}public class PowerMockTest { @Test public void mockPublicThenCallRealMethod() { Mock mock = PowerMockito.mock(Mock.class); PowerMockito.when(mock.isTrue_1()).thenCallRealMethod(); assertThat(mock.isTrue_1(), is(true)); }}同时能够应用whenNew()来实现在程序中new一个对象时失去一个mock实例,如下所示。 ...

June 8, 2021 · 4 min · jiezi

关于云原生:云原生应用的测试指南-IDCF

译者注:这是国外一篇介绍如何测试云原生利用的文章,云原生利用通常是基于微服务架构格调的分布式应用程序,传统的测试方法和技术已不能满足产品交付的品质要求,只有联合古代测试技术,既增强研发阶段的测试,又引入产品公布后的线上测试,能力有效应对云原生利用在性能、性能、平安、可靠性方面的挑战。这些新的技术包含契约测试,灰度公布,混沌工程,在线监控和日志剖析等。一、应用程序的“云原生化”越来越多的公司正将本地部署的利用迁徙(或打算迁徙)到云上,它们的指标是设计、架构并构建的应用程序能够很容易地扩大并部署到云上,并且能够充分利用云平台(如AWS、Azure,或GCP)提供的计算模式。 “云原生化”和开发云原生利用指的是设计、架构并构建的分布式软件应用可能齐全利用云服务商提供的PaaS(Platform-as-a-Service,平台即服务)和IaaS(Infrastructure-as-a-Server,基础设施即服务)的服务模式。这些利用通常是作为一组微服务(基于微服务架构格调)构建的。 这些松耦合的微服务运行在一个由私有云、公有云和混合云提供的动静环境中的容器化和动静编排平台上(基于Kubernets、Docker等技术)。促使企业进行利用的“云原生化”只管有不同方面的起因,但其中包含一些最重要的驱动因素,如:缩短重要的软件应用的宕机工夫、减少利用的弹性能力、依据业务须要动静扩容或缩容,进步利用开发速度、疾速响应用户需要、更专一于翻新从而减少更多的业务价值。 二、云原生利用的测试方法测试总是帮忙咱们更深刻地开掘问题,并向用户交付高质量的产品。软件测试在帮忙咱们收集产品的状态、可维护性、性能、健壮性和可靠性等大量有用信息方面施展了重要作用。通过对这些信息进行剖析,企业的决策者们能够更有信念地进行产品公布的决策。 相比其它软件应用(例如单体架构的利用)的传统测试方法,云原生利用的测试要更简单。这是因为云原生利用是动静、分布式构建的一组微服务(每个微服务能够独立公布),公布速度更快(通常采纳CI/CD和DevOps实际),而且存在难以预测和跟踪的故障模式。 这就要求咱们适应这些变动,从新扫视传统的测试技术,并采纳新的、古代的测试方法来预感、检测、响应、调试、剖析和报告问题。 这些测试技术将在许多方面帮忙咱们找到并揭示大量信息,这将有助于进步云原生利用的整体品质。对于这类利用,软件测试已成为软件开发生命周期的所有阶段中不可分割的一部分,并且促使业务剖析人员、开发人员、测试人员、架构师和软件设计人员等进行更多沟通和交换:提出疑难,共享信息,探讨并评估问题和危险。当初就让咱们一起看看针对云原生利用的无效的测试技术。 1)单元测试,集成测试,端到端的测试通过在微服务架构的云原生利用中测试单个服务组件中的最小可测试单元,能够在开发晚期阶段发现许多缺点。疾速、牢靠、自动化的单元测试不仅能确定服务组件的独立单元/模块是否能失常工作,也将有助于开发人员察看单元/模块状态的变动,查看对象之间的交互,以及对象之间的依赖,对于组件的状态取得疾速反馈,或者因为代码变更导致了回归缺点等。这些测试让软件代码更具备可测试性并有利于代码的重构。 软件应用的服务组件在集成之后,由继续集成服务器触发的集成测试将有助于测试各个服务组件之间或服务组件与内部服务、零碎、数据存储之间的通信门路和交互。只管测试所有的集成点是艰难的,但团队必须采纳基于危险的测试方法,依据制订的指标、范畴和优先级执行测试。 对于云原生利用来说,执行端到端测试是比拟艰难的,因为这波及到微服务架构中每个独立开发的局部,测试进度会比拟迟缓,而且有时候会十分不稳固,不得不思考到服务组件之间的异步调用和环境因素的影响。这都造成端到端的测试在测试筹备、运行和保护方面的老本较高。尽管如此,研发团队依然须要运行其中的一些端到端的测试,只管不那么频繁,但须要笼罩重要的业务门路,并验证残缺的利用是否满足业务需要。 2)契约测试既然会波及单个独立的微服务,研发团队也须要对微服务执行契约测试。微服务体系架构由“生产者”服务组件和“消费者”服务组件组成。当消费者组件试图与生产者组件联合并应用它的输入时,一个“服务契约”(由预期的输出和输入组成)就在它们之间造成。 由这些契约测试组成的自动化测试套件能够集成到流水线中,运行这些测试来验证生产者和消费者组件中的任何更改是否合乎二者之间的服务契约。开发和运行契约测试是测试云原生利用的一项重要内容。 3)非功能测试对于云原生利用来说,功能测试十分重要,能够验证产品是否满足业务需要。然而,当产品公布到生产环境中,功能测试可能提供产品将以预期的形式响应的信念吗?当忽然呈现服务器解体、服务组件宕机或某些依赖服务不可用时,服务是否可能平安降级?产品上线后是否足够平安?该产品是否应酬突发的大量用户申请? 非功能测试对于云原生利用十分重要。对于上述问题,任何缺点或者偏离预期行为的状况都要求咱们以起码的工作量和步骤尽快发现、剖析并修复问题,以防止这些问题再次发生。 为了确保这些问题产生的机率或影响很小,咱们须要借助有用的工具(许多工具是云提供商本人提供的)测试产品的性能(如提早、负载平衡的影响、缓存、产品性能中的危险因素、基于性能指标来比照和提供反馈后果的基准测试等)、可用性、负载(例如在靠近实在负载条件下对产品吞吐量的影响)和安全性(动态和动静),并事后解决任何潜在的危险。 4)混沌工程和生效模式测试说到品质工程,咱们大多数人都晓得像“FMEA”(生效模式和影响剖析)技术,它能够帮忙咱们辨认产品中的潜在生效模式及其起因和结果。对于单体利用,大多数潜在的故障模式是已知的,能够辨认的,因而能够在代码构造中解决。即便不解决,当缺点产生时也可能疾速修复。 然而对于微服务来说,产品在生产环境中失败的形式是难以计数、不可预测的,因为波及到大量的复杂性。在这些状况下,“混沌工程”会很有帮忙。它是一种辨认在生产环境中产品生效的办法,以建设对系统应答意外或未知操作能力的信念。 “混沌工程”和FMEA一起,通过注入可控的故障,帮忙咱们取得一个可靠性和弹性能力更高的产品,让咱们可能检测和剖析这些故障,从而预知产品在哪些方面会出故障。这将帮忙咱们调整现有的流程,以避免故障级联的结果,并提前打算如何缩短MTTR(Mean Time to Recovery/Restore,故障均匀复原工夫)。 5)可察看性、在线监控和日志剖析作为软件工程师,对云原生利用咱们既要在上线前进行测试也要在上线后进行测试。如果操作切当,线上测试能够为咱们提供许多有价值的信息,为打算下一个版本的弹性、可伸缩性和灵活性提供重要反馈信息。但必须记住,线上测试的设置和执行很简单,在执行这些测试时必须十分小心,并充分考虑到,如果线上测试没有正确和平安的执行,对业务和用户带来什么样的影响。 “可察看性”是帮忙咱们更好地了解产品中软件行为的办法之一,是指通过观察产品的输入来理解产品外部状态的办法。咱们能够应用一些监控技术和工具收集、存储、保护和查问利用的状态、行为,以及服务之间交互相干的信息。这些日志和指标能够被进一步剖析来获取有价值的发现,或者疾速评估和剖析缺点。一些云服务提供商会提供开箱即用的性能和工具帮忙咱们实现监控、信息收集和剖析。 三、论断咱们必须明确,无论打算和执行多少功能测试和非功能测试,无论咱们如何努力提高这些云原生利用的品质,最终用户依然会面临问题。咱们的指标是缩小意外事件的危险,疾速剖析和修复故障,从既往事件中学习并将这些常识用于下一个版本。 在生产环境中发现缺点的老本是十分高的,咱们应该在软件的开发生命周期中尽早发现缺点。在生产环境中,咱们能够利用金丝雀部署(把所有性能推送给局部用户),暗启动(把新性能/次要性能推送给局部用户),智能性能切换/标记/位/翻转器(容许特定性能的利用被激活或停用)等技术在生产环境中逐步裸露缺点。 但咱们也必须记住,因为各种限度因素,包含估算、团队承接能力、时间表、上市工夫、大量相互依赖/独立的服务、环境的可用性等,通过已知的测试策略做详尽的测试是不可能的。因而,团队须要采取基于危险的测试方法,也必须意识到和缺点无关的各种类型的老本,比方检测老本,剖析调试老本,机会成本,修复老本,验证老本和保护老本。 思考到所有本文中探讨的因素,能够必定的是,只管测试云原生利用是艰难和具备挑战性的,但咱们能够让新的测试方联合本身的专业知识、不同的测试技术和策略,向用户交付高质量的产品。 起源:软件品质报道 作者:Sumon Dey 4月每周四晚8点,【冬哥有话说】DevOps之庖丁解牛,拆解DevOps的工具及具体实战。公众号留言“解牛”可获取地址 0401《数据库继续交付流水线分享与演示(Azure DevOps+Flyway)》0408《继续交付中的版本治理与基于Azure DevOps扩大框架的插件开发》0415《微服务,多团队合作中的API测试怎么做 - Pact契约测试》0422《BoatHouse端到端流水线展现》

April 15, 2021 · 1 min · jiezi

关于react.js:那些年错过的React组件单元测试下

写在后面上篇文章咱们曾经理解了前端单元测试的背景和根底的jestapi,本篇文章我会先介绍一下Enzyme,而后联合我的项目中的一个实在组件,来为它编写测试用例。 Enzyme上一篇中咱们其实曾经简略介绍了enzyme,但这远远不够,在本篇的组件测试用例编写中,咱们有很多中央要用到它,因而这里专门来阐明一下。 Enzyme是由Airbnb开源的一个React的JavaScript测试工具,使React组件的输入更加容易。Enzyme的API和jQuery操作DOM一样灵便易用,因为它应用的是cheerio库来解析虚构DOM,而cheerio的指标则是做服务器端的jQuery。Enzyme兼容大多数断言库和测试框架,如chai、mocha、jasmine等。 对于装置和配置,上一大节曾经有过阐明,这里就不赘述了罕用函数enzyme中有几个比拟外围的函数,如下: simulate(event, mock):用来模仿事件触发,event为事件名称,mock为一个event object;instance():返回测试组件的实例;find(selector):依据选择器查找节点,selector能够是CSS中的选择器,也能够是组件的构造函数,以及组件的display name等;at(index):返回一个渲染过的对象;text():返回以后组件的文本内容;html(): 返回以后组件的HTML代码模式;props():返回根组件的所有属性;prop(key):返回根组件的指定属性;state():返回根组件的状态;setState(nextState):设置根组件的状态;setProps(nextProps):设置根组件的属性;渲染形式enzyme 反对三种形式的渲染: shallow:浅渲染,是对官网的Shallow Renderer的封装。将组件渲染成虚构DOM对象,只会渲染第一层,子组件将不会被渲染进去,因此效率十分高。不须要 DOM 环境, 并能够应用jQuery的形式拜访组件的信息;render:动态渲染,它将React组件渲染成动态的HTML字符串,而后应用Cheerio这个库解析这段字符串,并返回一个Cheerio的实例对象,能够用来剖析组件的html构造;mount:齐全渲染,它将组件渲染加载成一个实在的DOM节点,用来测试DOM API的交互和组件的生命周期,用到了jsdom来模仿浏览器环境。三种办法中,shallow和mount因为返回的是DOM对象,能够用simulate进行交互模仿,而render办法不能够。个别shallow办法就能够满足需要,如果须要对子组件进行判断,须要应用render,如果须要测试组件的生命周期,须要应用mount办法。 渲染形式局部参考的这篇文章 “踩坑之路”开启组件代码首先,来看下咱们须要对其进行测试的组件局部的代码: ⚠️ 因为牵扯到外部代码,所以很多中央都打码了。重在演示针对不同类型的测试用例的编写import { SearchOutlined } from "@ant-design/icons"import { Button, Col, DatePicker, Input, message, Modal, Row, Select, Table,} from "antd"import { connect } from "dva"import { Link, routerRedux } from "dva/router"import moment from "moment"import PropTypes from "prop-types"import React from "react"const { Option } = Selectconst { RangePicker } = DatePickerconst { confirm } = Modalexport class MarketRuleManage extends React.Component { constructor(props) { super(props) this.state = { productID: "", } } componentDidMount() { // console.log("componentDidMount生命周期") } getTableColumns = (columns) => { return [ ...columns, { key: "operation", title: "操作", dataIndex: "operation", render: (_text, record, _index) => { return ( <React.Fragment> <Button type="primary" size="small" style={{ marginRight: "5px" }} onClick={() => this.handleRuleEdit(record)} > 编辑 </Button> <Button type="danger" size="small" onClick={() => this.handleRuleDel(record)} > 删除 </Button> </React.Fragment> ) }, }, ] } handleSearch = () => { console.log("点击查问") const { pagination } = this.props pagination.current = 1 this.handleTableChange(pagination) } render() { // console.log("props11111", this.props) const { pagination, productList, columns, match } = this.props const { selectedRowKeys } = this.state const rowSelection = { selectedRowKeys, onChange: this.onSelectChange, } const hasSelected = selectedRowKeys.length > 0 return ( <div className="content-box marketRule-container"> <h2>XX录入零碎</h2> <Row> <Col className="tool-bar"> <div className="filter-span"> <label>产品ID</label> <Input data-test="marketingRuleID" style={{ width: 120, marginRight: "20px", marginLeft: "10px" }} placeholder="请输出产品ID" maxLength={25} onChange={this.handlemarketingRuleIDChange} ></Input> <Button type="primary" icon={<SearchOutlined />} style={{ marginRight: "15px" }} onClick={() => this.handleSearch()} data-test="handleSearch" > 查问 </Button> </div> </Col> </Row> <Row> <Col> <Table tableLayout="fixed" bordered="true" rowKey={(record) => `${record.ruleid}`} style={{ marginTop: "20px" }} pagination={{ ...pagination, }} columns={this.getTableColumns(columns)} dataSource={productList} rowSelection={rowSelection} onChange={this.handleTableChange} ></Table> </Col> </Row> </div> ) }MarketRuleManage.prototypes = { columns: PropTypes.array,}MarketRuleManage.defaultProps = { columns: [ { key: "xxx", title: "产品ID", dataIndex: "xxx", width: "10%", align: "center", }, { key: "xxx", title: "产品名称", dataIndex: "xxx", align: "center", }, { key: "xxx", title: "库存", dataIndex: "xxx", align: "center", // width: "12%" }, { key: "xxx", title: "流动有效期开始", dataIndex: "xxx", // width: "20%", align: "center", render: (text) => { return text ? moment(text).format("YYYY-MM-DD HH:mm:ss") : null }, }, { key: "xxx", title: "流动有效期完结", dataIndex: "xxx", // width: "20%", align: "center", render: (text) => { return text ? moment(text).format("YYYY-MM-DD HH:mm:ss") : null }, }, ],}const mapStateToProps = ({ marketRuleManage }) => ({ pagination: marketRuleManage.pagination, productList: marketRuleManage.productList, productDetail: marketRuleManage.productDetail,})const mapDispatchToProps = (dispatch) => ({ queryMarketRules: (data) => dispatch({ type: "marketRuleManage/queryRules", payload: data }), editMarketRule: (data) => dispatch({ type: "marketRuleManage/editMarketRule", payload: data }), delMarketRule: (data, cb) => dispatch({ type: "marketRuleManage/delMarketRule", payload: data, cb }), deleteByRuleId: (data, cb) => dispatch({ type: "marketRuleManage/deleteByRuleId", payload: data, cb }),})export default connect(mapStateToProps, mapDispatchToProps)(MarketRuleManage)简略介绍一下组件的性能:这是一个被connect包裹的高阶组件,页面展现如下: ...

April 1, 2021 · 6 min · jiezi

关于测试驱动开发:单元测试的一些分享

背景最近在给一个客户做技术咨询,而后发现了客户对于单元测试的一个有意思的景象。分享进去,大家一起学习探讨一下。 现状剖析这里以java后端我的项目例,发现客户写的测试长上面的样子。(代码曾经脱敏解决过。) @Autowired private SampleJob handler; @Test public void testStart() throws Exception { SampleParamVo paramVo = new SampleParamVo(); paramVo.setStartTime("2021-03-18"); paramVo.setEndTime("2021-03-18"); handler.execute(paramVo); } @Autowired private SampleHandler handler; @Test public void testHandler() { handler.doHandler(new DateTime("2021-11-26"), null); } 那么这样的测试代码有什么问题呢? 他人看不懂这个测试是在做什么。首先测试的办法名没有任何意义,其次测试代码也只是调用了某个函数。无奈运行。这类测试代码运行往往须要启动其余服务或者须要一些非凡的设置。无奈运行就意味着它不能成为CI跑测试的一部分。没有断言。没有断言就无奈晓得测试的代码的正确性。应用了@Autowired这样的代码,减少了测试的耦合以及编写老本。和客户深聊了之后发现,原来客户不同的人对单元测试的了解也不一样。 写这个代码的开发人员说,“这些代码是在开发实现之后做一些自测的辅助脚本。”有的开发人员说,“咱们是微服务,单元测试须要调用其余服务,写起来很麻烦,而且如果其余服务不可用时,测试也跑不过。”测试人员说:“单元测试咱们有的,我每天都在写测试用例,到单元测试的时候我就会把我的用例全副过一遍。”所以咱们能够发现,有的开发人员口中的单元测试其实应该属于集成测试或者E2E测试,有的开发人员齐全没有写过单元测试,而测试人员了解单元测试是本人手动测试的时候用的测试用例。 那咱们就先来说说什么是单元测试。 什么是单元测试?单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。通常在java的世界外面,单元测试就是指对一个public的办法编写检查和验证的代码。 为什么要写单元测试?写单元测试次要有两大目标: 验证性能实现。爱护已有性能不被毁坏。当咱们写完一个办法,咱们如何晓得本人写的办法是按冀望工作的呢?这个时候就能够增加单元测试来验证咱们的代码是按冀望工作的。即当咱们给定指定的输出,咱们取得冀望的输入,则咱们说这个性能是合乎冀望的。 其次,代码不是写了就永远不变的,当需要变更时,新增需要时,修复bug时,都会批改代码,而单元测试则能爱护咱们已有的性能不被毁坏。爱护已有性能不会被本人毁坏,被新人毁坏,被新性能毁坏。 如何写单元测试?上面是一个单元测试的例子 @Test public void should_return_fizz_given_input_can_be_divided_by_3() { FizzBuzz fizzBuzz = new FizzBuzz(); // Given String actual = fizzBuzz.sayIt(6); // When Assertions.assertEquals("Fizz", actual); // Then } 一个规范的单元测试蕴含以下几个局部: ...

March 22, 2021 · 1 min · jiezi

关于单元测试:完整单元测试解决方案Telerik-正式支持NET-5

Telerik JustMock是一个灵便、功能齐全的.NET mocking框架。Telerik JustMock可能简化单元测试,当初测试简单的场景比以前更加容易了。同时JustMock还与Visual Studio以及其余工具集成,JustMock与VS 2017,2015和2013以及其余一些工具配合应用。 最新的Telerik JustMock Service Pack版本反对.NET 5的正式版本,可帮忙您利用性能、性能等方面的即时改良。.NET 5在.NET Conf 2020上正式公布,其中包含许多改良。在R3 SP2 2020版本中,Telerik JustMock正式反对.NET 5。 .NET 5中应如何应用Telerik JustMock,是否有任何更改? 通常当须要将应用程序迁徙到较新版本的第三方库时,会因解决很多重大更改破费许多工夫。对于高于.NET Core 2.0和.NET 5的所有版本,Telerik.JustMock.dll放弃雷同,您能够在JustMockInstallationPath / Libraries / netcoreapp2.0 /中找到它。 当您应用JustMock在测试项目中迁徙到.NET 5时,应增加对System.Security.Permissions版本5.0的援用。 如果您应用Telerik JustMock NuGet软件包,则该软件包将主动解决。 异步办法中的部分函数模仿 mocking位于异步办法中的本地函数已损坏,对于R3 SP 2 2020版本,此问题已修复。 援用Ninject.Extensions.Conventions Nuget包时引发异样 JustMock的Ninject.Extensions.Conventions NuGet包导致引发了异样,R3 SP2 2020版本已解决此问题。

December 10, 2020 · 1 min · jiezi

关于单元测试:golang协程

一、过程和线程过程就是程序在操作系统中的一次执行过程,是零碎进行资源分配和调度的根本单位线程是过程的一个执行实例,是程序执行的最小单元,它是比过程更小的能独立运行的根本单位一个过程能够创立和销毁多个线程,同一个过程中的多个线程能够并发执行一个程序至多有一个过程,一个过程至多有一个线程二、并发和并行多线程程序在单核上运行,就是并发多线程程序在多核上运行,就是并行 并发 :在一个cpu中,比方10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,如同10个线程都在运行,但从宏观上看,某一时间点看,其实只有一个线程在执行,这就是并发。 并行 :在多个cpu中(如10个),比方10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,10个线程都在运行,但从宏观上看,某一时间点看,也同时10个线程在执行,这就是并行。 三、go协程和主线程go主线程是一个物理线程,间接作用在CPU上,是重量级的,十分耗cpu资源,一个go线程上能够起多个协程。协程是主线程开启的,轻量级的线程,逻辑态的,对资源耗费小go协程特点: 有独立的栈空间共享程序的堆空间调度由用户管制协程是轻量级的线程四、channel(管道)channel实质是一个数据结构-队列数据是先进先出线程平安,多goroutine拜访时,不须要加锁,就是说channel自身是线程平安的channel是由类型的,一个string的channel只能寄存string类型的数据遍历时,如果channel没有敞开,则会呈现deadlock谬误,如果channel曾经敞开,则会失常遍历数据,遍历结束后,就会退出遍历 细节:管道可申明为只读或只写 var cha1 chan int // 可读可写var cha2 chan<- int // 只写var cha2 <-chan int // 只读应用select能够解决从管道取数据的阻塞问题 // 理论开发中,不好确定什么时候敞开该管道// 可用select解决for { select { // 如果管道始终没敞开,不会始终阻塞而deadlock // 会主动到下一个case匹配 case v := <-intChan: fmt.Printf("读取数据") case v := <-stringChan: fmt.Printf("读取数据") default: fmt.Printf("都取不到") }}goroutine中应用recover,解决协程中呈现的panic,导致程序解体

December 9, 2020 · 1 min · jiezi

Vue单元测试

单元测试,就是为了测试某一个类或者是某一个方法能否正常工作而写的测试代码。 关于单元测试是什么:单元测试是针对 程序的最小单元 来进行正确性检验的测试工作。就是测试某一个页面或者是某一个方法来进行测试的代码单元。 意义:可以减少bug,提高代码的效率质量,同时可以快速定位bug存在的地点位置,减少调试时间,放心重构代码。 目的:当我们的项目足够大的时候,在重叠的模块和组件的过程中,可能会有影响到之前的模板。 测试命令:npm run unit 测试的文件内容(List.vue): <template> <div> <h1>My To Do List</h1> <br/> <ul> <!-- 红线警告是这个编辑器不支持这种格式的写法 --> <li v-for="item in listItems">{{ item }}</li> </ul> </div> </template> <script> export default { name: "list", data() { return { listItems: ["buy food", "play games", "sleep"] }; } }; </script>测试的路由配置(index.js): import Vue from 'vue' import Router from 'vue-router' import HelloWorld from '@/components/HelloWorld' import List from '@/components/List' Vue.use(Router) export default new Router({ routes: [ { path: '/', name: 'HelloWorld', component: HelloWorld }, { path: '/to-do', name: 'ToDo', components: List }, ] });配置的测试文件内容(List.spc.js): ...

October 9, 2019 · 1 min · jiezi

学习单元测试告别祈祷式编程

祈祷式编程祈祷式编程如果代码中包含以下代码 或者上线后进行这种活动 那么这种编程方式就是祈祷式编程。 用流程图表示基本就是这个样子。 祈祷式编程有什么危害呢? 累,每次写完代码还需要再祈祷不受控,代码运行结果主要看运气,大仙忙的时候可能保佑不了解决这个问题有好多种方法,单元测试是其中之一。 单元测试什么是单元测试单元测试是由开发人员编写的,用于对软件基本单元进行测试的可执行的程序。单元(unit)是一个应用程序中最小的课测试部分。(比如一个函数,一个类google 把测试分成小型测试、中型测试和大型测试。单元测试基本和小型测试的作用类似,但是通常也会使用mock或者stub 的方式模拟外部服务。 理想情况下,单元测试应该是相互独立、可自动化运行的。 目的: 通常用单元测试来验证代码逻辑是否符合预期。完整可靠的单元测试是代码的安全网,可以在代码修改或重构时验证业务逻辑是否正确,提前发现代码错误,减少调试时间。设计良好的单元测试某些情况下可以比文档更能反应出代码的功能和作用。 单元测试这么多优点为什么有人不喜欢写单元测试呢? 单元测试太费时间了,对于编写单元测试不熟练的新手来说,编写单元测试可能比写代码的还费时间单元测试运行时间太长(这通常是单元测试设计不合理或者代码可测试性较差造成的祖传代码,看都看不懂怎么写单元测试(这个确实优点棘手。。可以考虑先给新代码加单元测试不会写单元测试这篇文章主要关注第四个问题,如何写单元测试。 单元测试的结构首先看一下单元测试的结构,一个完整的单元测试主要包括Arrange-Act-Assert(3A) 三部分。 Arrange--准备数据Act--运行代码Assert--判断结果是否符合预期比如我们要给下面这段代码(golang)加单元测试: func Add(x, y int) int { return x + y}单元测试代码如下: import "testing"func TestAdd(t *testing.T) { // arrange 准备数据 x, y := 1, 2 // act 运行 got := Add(x, y) //assert 断言 if got != 3 { t.Errorf("Add() = %v, want %v", got, 3) }}如何编写好的单元测试什么样的单元测试才是好的单元测试呢?先看一个例子: ...

October 7, 2019 · 3 min · jiezi

Spock-in-Java-慢慢爱上写单元测试

Spock in Java 慢慢爱上写单元测试 前言最近小组里面引进了Spock这个测试框架,本人在实际使用了之后,体验非常不错,同时在整个学习过程当中,学习了单元测试的相关知识。 本篇文章一是为了巩固输入的知识,二是为了向大家推广一下。 在了解学习Spock测试框架之前,我们应该先关注单元测试本身,了解我们常见的单测痛点,这样才能更好地去了解Spock这个测试框架是什么,我们为什么要使用它,能解决我们什么痛点。 现在让我们开始吧。 关于单元测试我们写代码免不了要测试,测试有很多种,对于Javaer们来说,最初级的测试是写个main函数运行一个函数结果,或者说把系统启起来自己模拟一下请求,看输入输出是否符合预期,更高级地,会用各种测试套件,测试系统。每个测试都有它的关注点,比如测试功能是否正确,系统性能瓶颈等等。 那我们常说的单元测试呢? 单元测试(英语:Unit Testing)又称为模块测试,是针对程序模块)(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。-- 摘自维基百科 以上是维基百科的说明。 单元测试当然不是必须之物,没了单测你的程序经过QA团队的端到端测试和集成测试之后,也能保证正确性。但是从另外的角度来看,单元测试也是必须之物。比如持续部署的前提之一就是有单元测试的保障,还有在重构代码的时候,没有单元测试你会寸步难行。 1.1 单元测试的好处单元测试的好处包括但不限于: 提升软件质量优质的单元测试可以保障开发质量和程序的鲁棒性。越早发现的缺陷,其修复的成本越低。 促进代码优化单元测试的编写者和维护者都是开发工程师,在这个过程当中开发人员会不断去审视自己的代码,从而(潜意识)去优化自己的代码。 提升研发效率编写单元测试,表面上是占用了项目研发时间,但是在后续的联调、集成、回归测试阶段,单测覆盖率高的代码缺陷少、问题已修复,有助于提升整体的研发效率。 增加重构自信代码的重构一般会涉及较为底层的改动,比如修改底层的数据结构等,上层服务经常会受到影响;在有单元测试的保障下,我们对重构出来的代码会多一份底气。 1.2 单元测试的基本原则宏观上,单元测试要符合 AIR 原则: A: Automatic(自动化)I: Independent(独立性)R: Repeatable(可重复)微观上,单元测试代码层面要符合 BCDE 原则: B: Border,边界性测试,包括循环边界、特殊取值、特殊时间点、数据顺序等C: Correct,正确的输入,并且得到预期的结果**D: Design,与设计文档相符合,来编写单元测试E: Error,单元测试的目的是为了证明程序有错,而不是证明程序无错。为了发现代码中潜藏的错误,我们需要在编写测试用例时有一些强制的错误输入(如非法数据、异常流程、非业务允许输入等)来得到预期的错误结果。1.3 单元测试的常见场景开发前写单元测试,通过测试描述需求,由测试驱动开发。(如果不熟悉TDD的同学可以去google一下)在开发过程中及时得到反馈,提前发现问题。应用于自动化构建或持续集成流程,对每次代码修改做回归测试。(CI/CD 质量保障)作为重构的基础,验证重构是否可靠。1.4 单元测试的常见痛点下列痛点是日常开发中可能会遇到的, 测试上下文依赖外部服务(如数据库服务)测试上下文存在代码依赖(如框架等)单元测试难以维护和理解(语义不清)对于多场景不同输入输出的函数,单元测试代码量会很多...对上面几点稍微做下解释。 首先,测试代码的代码量绝对不会比业务代码少(假设有覆盖率指标,且不作弊),有时候一个函数,输入和输出会有多种情况,想要完全覆盖,代码量只会更多。较多的代码量,加上单测代码并不想业务代码那样直观(靠写注释的方式,看的乱,写的累),还有一部分编码人员对代码可读性不重视,最终就会导致单元测试的代码难以阅读,更难以维护。同时,大部分单元测试的框架都对代码有很强的侵入性,要想理解单元测试,首先得学习一下那个单元测试框架。从这个角度来看,维护的难度又增加了。 再说说,单元测试存在外部依赖的情况,也就是第一、二点,想要写一个纯粹的无依赖的单元测试往往很困难,比如依赖了数据库,依赖了其他模块,所以很多人在写单元测试时选择依赖一部分资源,比如在本机启动一个数据库。这类所谓的“单元测试”往往很流行,但是对于多人合作的项目,这类测试却经常容易造成混乱。 比如说要在本地读个文件,或者连接某个数据库,其他修改代码的人(或者持续集成系统中)并没有这些东西,所以测试也都没法通过。最后大部分这类测试代码的下场都是用不了、也舍不得删,只好被注释掉,扔在那里。随着开源项目逐渐发展,对外部资源的依赖问题开始可以通过一些测试辅助工具解决,比如使用内存型数据库H2代替连接实际的测试数据库,不过能替代的资源类型始终有限。 而实际工作过程中,还有一类难以处理的依赖问题:代码依赖。比如一个对象的方法中调用了其它对象的方法,其它对象又调用了更多对象,最后形成了一个无比巨大的调用树。后来出现了一些mock框架,比如java的JMockit、EasyMock,或者Mockito。利用这类框架可以相对比较轻松的通过mock方式去做假设和验证,相对于之前的方式有了质的飞跃。 但是,在这里需要强调一个观点,写单元测试的难易程度跟代码的质量关系最大,并且是决定性的。项目里无论用了哪个测试框架都不能解决代码本身难以测试的问题。 简单来说,有时候你觉得你的代码很难写单元测试,说明代码写的不是很好,需要去关注代码的逻辑抽象设计是否合理,一步步去重构你的代码,让你的代码变得容易测试。但这些又属于代码重构方面的知识了,涉及到很多的设计原则。推荐阅读《重构-改善既有代码的设计》《修改代码的艺术》 《敏捷软件开发:原则、模式与实践》这几本著作。 1.5 心态的转变很多开发人员对待单元测试,存在心态上的障碍, 那是测试同学干的事情。(开发人员要做好单元测试单元测试代码是多余的。 (汽车的整体功能与各单元部件的测试正常与否是强相关单元测试代码不需要维护。 一年半载后,那么几乎处于废弃状态(单元测试代码是需要随着项目开发一直维护的单元测试与线上故障没有辩证关系。(好的单元测试能最大限度规避线上故障关于SpockSpock能给你提供整个测试生命周期中可能需要的所有测试工具。它带有内置的模拟打桩,以及专门为集成测试创建的一些额外的测试注释。同时,由于Spock是较新的测试框架,因此它有时间观察现有框架的常见缺陷,并加以解决或提供更优雅的解决方法。 Spock是Java和Groovy应用程序的测试和规范框架测试代码使用基于groovy语言扩展而成的规范说明语言(specification language)通过junit runner调用测试,兼容绝大部分junit的运行场景(ide,构建工具,持续集成等)Groovy以“扩展JAVA”为目的而设计的JVM语言JAVA开发者友好可以使用java语法与API语法精简,表达性强典型应用:jenkins, elasticsearch, gradlespecification languagespecification 来源于近期流行起来写的BDD(Behavior-driven development 行为驱动测试)。在TDD的基础上,通过测试来表达代码的行为。通过某种规范说明语言去描述程序“应该”做什么,再通过一个测试框架读取这些描述、并验证应用程序是否符合预期。把需求转化成Given/When/Then的三段式,所以你看到测试框架有这种Given/When/Then三段式语法的,一般来说背后都是BDD思想,比如上图中的Cucumber和JBehave。 Spock快速使用现在让我们以最快速的方式,来使用一次Spock 3.0 创建一个空白项目创建一个空白项目:spock-example,选择maven工程。 3.1 依赖 <dependencies> <!-- Mandatory dependencies for using Spock --> <!-- 使用Spock必须的依赖 --> <dependency> <groupId>org.spockframework</groupId> <artifactId>spock-core</artifactId> <version>1.3-groovy-2.5</version> <scope>test</scope> </dependency> <!-- Optional dependencies for using Spock --> <!-- 选择性使用的Spock相关依赖 --> <dependency> <!-- use a specific Groovy version rather than the one specified by spock-core --> <!-- 不使用Spock-core中定义的Groovy版本,而是自己定义 --> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.5.7</version> <type>pom</type> </dependency> <dependency> <!-- enables mocking of classes (in addition to interfaces) --> <!-- mock 接口和类时要用 --> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.9.3</version> <scope>test</scope> </dependency> <dependency> <!-- enables mocking of classes without default constructor (together with CGLIB) --> <!-- mock 类要用 --> <groupId>org.objenesis</groupId> <artifactId>objenesis</artifactId> <version>2.6</version> <scope>test</scope> </dependency> <dependency> <!-- only required if Hamcrest matchers are used --> <!-- Hamcrest 是一个用于编写匹配对象的框架,如果用到了Hamcrest matchers,需要加这个依赖 --> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-core</artifactId> <version>1.3</version> <scope>test</scope> </dependency> <!-- Dependencies used by examples in this project (not required for using Spock) --> <!-- 使用h2base做测试数据库--> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.197</version> <scope>test</scope> </dependency> </dependencies>3.2 插件 <plugins> <!-- Mandatory plugins for using Spock --> <!--使用Spock的强制性插件 --> <plugin> <!-- The gmavenplus plugin is used to compile Groovy code. To learn more about this plugin,visit https://github.com/groovy/GMavenPlus/wiki --> <!-- 这个 gmavenplus 插件是用于编译Groovy代码的 . 想获取更多此插件相关信息,visit https://github.com/groovy/GMavenPlus/wiki --> <groupId>org.codehaus.gmavenplus</groupId> <artifactId>gmavenplus-plugin</artifactId> <version>1.6</version> <executions> <execution> <goals> <goal>compile</goal> <goal>compileTests</goal> </goals> </execution> </executions> </plugin> <!-- Optional plugins for using Spock --> <!-- 选择性使用的Spock相关插件--> <!-- Only required if names of spec classes don't match default Surefire patterns (`*Test` etc.) --> <!--只有当测试类不匹配默认的 Surefire patterns (`*Test` 等等.)--> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.20.1</version> <configuration> <useFile>false</useFile> <includes> <include>**/*Test.java</include> <include>**/*Spec.java</include> </includes> </configuration> </plugin> ... </plugins>3.3 设计测试源码目录由于spock是基于groovy语言的,所以需要创建groovy的测试源码目录:首先在test目录下创建名为groovy的目录,之后将它设为测试源码目录。 ...

October 5, 2019 · 6 min · jiezi

单元测试在-golang-中的实践

单元测试是什么?首先需要明确的就是,单元是什么?是一个函数?一个接口?还是一个模块? 这个可能每个人心中都用不同的定义。我比较赞同观点是:单元是指一段逻辑。 因此,单元测试就是对一段代码逻辑的正确性进行校验进行测试 单元测试的意义从我自己的切身体会上来说,单元测试意义在于: 最基本的,保证代码逻辑上的正确性能够进行回归,避免修改导致把以前的代码改挂迫使自己写出好的程序开发的时候能够快速的把代码跑起来,验证效果(在程序体量特别大,构建一次成本特别高;或者程序整体运行条件比较苛刻的时候会更明显)单测即文档明确自己到底测什么明确好单元的定义后,写单元测试前必须明确的一个点就是:到底需要测什么?也就是这个单元的边界在哪里。 例如下面这段代码: func (d *DeploymentModel) DeployConfirm(user string, deployID int64) error { var deploy Deploy if err := d.db.Find(&deploy, "id = ?", deployID).Error; err != nil { return err } if deploy.GetConfirm() == DEPLOY_UNCONFIRMED { cfm := &confirmer{} if err := cfm.ConfirmDeployInNeed(deploy.Id, user); err != nil { return err } } return nil}这是一个使用了 gorm 作为数据库驱动的 web 项目中的一段代码。d.db 是 gorm 中的 *gorm.DB 对象。这个函数干了这么一件事: 根据 deployID 从数据库中拿出对应的记录如果这个记录的状态是没有被确认,就去确认一下否则之间返回。我们要写一个 TestDeployConfirm 函数: ...

August 28, 2019 · 3 min · jiezi

iOS单元测试详解

似此星辰非昨夜为谁风露立中宵 前言我们在做组件化的过程中,肯定需要做的一步就是拆组件,把我们项目中的各个模块拆分为基础组件、功能组件、业务组件,但是在拆分的过程中很容易就会出现问题,所以我们在做组件化的过程中把每个组件都加上了单元测试,这样可以大大提高了我们组件的健壮性,随着我们的组件完成告一段落,今天就把当初探索学习单元测试的经验分享给大家,每种断言、每种测试场景都对应的有例子,希望能对你能有帮助 什么是单元测试(Unit Testing)又称为模块测试, 是针对程序模块的最小单位来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。 单元测试的发展 1.Xcode在XCode4.x时代集成的是OCUnit 2.XCode5.x时代就升级为了XCTest 3.XCode7增加UI测试单元测试框架XCTest(苹果自带,推荐)KiwiGHUintOCMock单元测试带来的好处测试做了我们期望它做的事情尽早的发现程序的 bug 和不足保证在加入新功能或修改旧功能时代码的正确性缺点: 没有足够的时间编写单元测试,或者说大家都没有写单元测试的习惯XCTestXCTest是Xcode自带的一个测试框架,苹果官方推荐我们使用 XCTestCaseXCTest中的测试类都是继承自XCTestCase XCTestCase类结构 - (void)setUp: 在调用类中的每个测试方法之前调用此方法。 - (void)tearDown 这个方法在类中的每个测试方法调用之后调用- (void)testPerformanceExample 将要度量时间的代码放在这里- (void)testExample 使用XCTAssert和相关函数验证测试结果是否正确断言无条件报错等价测试nil测试布尔测试异常测试无条件报错1.XCTFail.生成一个无条件报错。 等价测试1.XCTAssertEqualObjects 2.XCTAssertNotEqualObjects 3.XCTAssertEqual. 当expression1不等于expression2时报错,这个测试用于C语言的常量 4.XCTAssertNotEqual 5.XCTAssertEqualWithAccuracy. 当expression1和expression2之间的差别高于accuracy 将报错。这种测试适用于floats和doubles这些常量,两者之间的细微差异导致它们不完全相等,但是对所有的常量都有效。 6.XCTAssertNotEqualWithAccuracy Nil(空)测试1.XCTAssertNil. 当expression参数非nil时报错2.XCTAssertNotNil Boolean测试1.XCTAssertTrue. 当expression计算结果为false时报错。2.XCTAssertFalse. 当expression计算结果为true报错。3.XCTAssert. 当expression计算结果为false时报错,与XCTAssertTrue同义。 异常断言测试1.XCTAssertThrows.当expression不抛出异常时报错。2.XCTAssertNoThrow. 当expression抛出异常时报错。 3.XCTAssertThrowsSpecific.当expression针对指定类不抛出异常时报错。4.XCTAssertNoThrowSpecific. 当expression针对指定类抛出异常时报错。任意其他异常都可以;也就是说它不会报错。 5.XCTAssertThrowsSpecificNamed. 当expression针对特定类和特定名字不抛出异常时报错。对于AppKit框架或Foundation框架非常有用,抛出带有特定名字的NSException(NSInvalidArgumentException等)。 6.XCTAssertNoThrowSpecificNamed. 当expression针对特定类和特定名字抛出异常时报错。对于AppKit框架或Foundation框架非常有用,抛出带有特定名字的NSException(NSInvalidArgumentException等) 如何写单元测试找到测试场景 逻辑测试异步测试性能测试准备测试数据 边界测试数据正确测试数据错误测试数据验证结果 使用断言验证单元测试的规范1.合理命名测试用例 1.一个测试方法只测试被测类的一个明确功能, 并命名相应测试方法需要被测试的方法 相应的测试方法 ...

July 16, 2019 · 1 min · jiezi

JAVA中单元测试的常用方式

什么是单元测试单元测试(英语:Unit Testing)又称为模块测试, 是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。通常来说,程序员每修改一次程序就会进行最少一次单元测试,在编写程序的过程中前后很可能要进行多次单元测试,以证实程序达到软件规格书要求的工作目标,没有程序错误;虽然单元测试不是什么必须的,但也不坏,这牵涉到项目管理的政策决定。单元测试的优点优质的单元测试可以保障开发质量和程序的鲁棒性。在大多数互联网企业中开发工程师在研发过程中都会频繁地执行测试用例,运行失败的单测能帮助我们快速排查和定位问题 使问题在被带到线上之前完成修复。正如软件工程界的一条金科玉律----越早发现的缺陷,其修复成本越低。一流的测试能发现未发生的故障;二流的测试能快速定位故障的发生点;三流的测试则疲于奔命,一直跟在故障后面进行功能回归。JAVA中常用的单元测试工具JUnit/JUnit5https://junit.org/junit5/ junit是老牌测试框架了,也是目前引用最广泛的一个框架。当前已经更新到Junit5,功能更强大。 class StandardTests { @BeforeAll static void initAll() { } @BeforeEach void init() { } @Test void succeedingTest() { } @Test void failingTest() { fail("a failing test"); } @Test @Disabled("for demonstration purposes") void skippedTest() { // not executed } @Test void abortedTest() { assumeTrue("abc".contains("Z")); fail("test should have been aborted"); } @AfterEach void tearDown() { } @AfterAll static void tearDownAll() { }}assertjhttps://assertj.github.io/doc/ 一个功能强悍的断言工具,支持各种断言方式 ...

June 22, 2019 · 2 min · jiezi

持续集成之测试篇

持续集成单元测试(unit)karmaKarma 是Google开源的一个基于Node.js 的 JavaScript 测试执行过程管理工具(Test Runner)。该工具可用于测试所有主流Web浏览器,也可集成到 CI (Continuous integration)工具,也可和其他代码编辑器一起使用。 我们测试用的无界面浏览器phantomjs。测试框架使用mocha和chai。 以下是我们项目中使用的主要配置信息: /** * 测试启动的浏览器 * 可用的浏览器:https://npmjs.org/browse/keyword/karma-launcher */browsers: ['PhantomJS'],/** * 测试框架 * 可用的框架:https://npmjs.org/browse/keyword/karma-adapter */frameworks: ['mocha', 'chai'],/** * 需要加载到浏览器的文件列表 */files: [ '../../src/dcv/plugins/jquery/jquery-1.8.1.min.js', '../../src/dcv/plugins/common/mock.min.js', '../../src/dcv/plugins/common/bluebird.min.js', '../../src/dcv/javascripts/uinv.js', '../../src/dcv/javascripts/uinv_util.js', '../../src/dcv/javascripts/browser/uinv_browser.js', 'specs/validators.js'],/** * 排除的文件列表 */exclude: [],/** * 在浏览器使用之前处理匹配的文件 * 可用的预处理: https://npmjs.org/browse/keyword/karma-preprocessor */preprocessors: { //报告覆盖 "../../src/dcv/javascripts/**/*.js": ["coverage"]},/** * 使用测试结果报告者 * 可能的值: "dots", "progress" * 可用的报告者:https://npmjs.org/browse/keyword/karma-reporter */reporters: ['spec', 'coverage'],/** * 使用reporters为"coverage"时报告输出的类型和那目录 */coverageReporter: { type: 'html', dir: 'coverage/'},/** * 服务端口号 */port: 9876,/** * 启用或禁用输出报告或者日志中的颜色 */colors: true,/** * 日志等级 * 可能的值: * config.LOG_DISABLE //不输出信息 * config.LOG_ERROR //只输出错误信息 * config.LOG_WARN //只输出警告信息 * config.LOG_INFO //输出全部信息 * config.LOG_DEBUG //输出调试信息 */logLevel: config.LOG_INFO,/** * 启用或禁用自动检测文件变化进行测试 */autoWatch: true,/** * 开启或禁用持续集成模式 * 设置为true, Karma将打开浏览器,执行测试并最后退出 */// singleRun: true,/** * 并发级别(启动的浏览器数) */concurrency: Infinity在package.json中配置如下: ...

June 20, 2019 · 5 min · jiezi

单元测试规范

原则单元测试文件必须拥有良好的结构和格式;测试用例的分组名称和用例名称必须清晰易懂;测试用例必须能描述测试目标的行为;优先测试代码逻辑(过程)而非执行结果;单元测试的各项覆盖率指标必须在95%以上; 技术Jest:https://facebook.github.io/jest 结构编写单元测试所涉及的文件应存放于以下两个目录: __mocks__/:模拟文件目录[name].mock.json:【例】单个模拟文件__tests__/:单元测试目录[target].test.js:【例】单个单元测试文件,[target]与目标文件名保持一致,当目标文件名为index时,采用其上层目录或模块名。[target].test.js 文件应按照如下结构编写测试文件,注意其中的空行: /* eslint global-require: 0 */const thirdPartyModule = require('thrid-party-module')describe('@zpfe/module-name' () => { const mocks = {} beforeAll(() => {}) beforeEach(() => {}) test('描述行为', () => { mocks.fake.mockReturnValue('控制模拟行为的代码置于最上方') const target = require('../target.js') const result = target.foo('执行目标待测功能') expcet(result).toBe('断言置于最下方') })})保证每个describe内部只有mock对象、生命周期钩子函数和test函数,将模拟对象都添加到mocks对象的适当位置,将初始化操作都添加到适当的生命周期函数中。 mocks 对象常量mocks的结构如下: const mocks = { zpfe: { // @zpfe模块,若有,将包名转换为驼峰式以便访问,比如:koaMiddleware log: { info: jest.fn() } }, dependencies: { thirdPartyModule1: { // 第三方依赖模块,若有 }, files: { // 本地依赖文件 router: jest.fn() }, others: { // 公共假对象 ctx: jest.fn() }}请注意,mocks对象的价值在于保存模拟依赖项及部分复用对象,请勿添加不涉及模拟也没有被复用的内容。 ...

May 15, 2019 · 2 min · jiezi

第三讲使用JUnit对Spring-Boot中的Rest-Controller进行单元测试

(第三讲)使用JUnit对Spring Boot中的Rest Controller进行单元测试本次教程主要讲解如何对Spring Boot中的Rest Service进行单元测试。以往我们主要是使用JUnit对业务层进行单元测试,本次课程将使用一个简单的案例来说明如何使用JUnit对Spring Boot的Rest Service进行单元测试。1. 主要类容快速搭建Restfull Service 环境创建GET请求以检索用户信息创建GET请求检索用户角色信息创建POST请求新增用户角色信息如何使用PostMan请求Restfull Service使用JUnit对GET请求进行单元测试使用JUnit对POST请求进行单元测试2. 你将需要准备的工具JDK 1.8及以上版本Maven 3.0及以上版本的项目构建工具IDEA代码编辑器3. 你可以通过以下的地址获取本次课程的所有示例代码项目代码已经上传到GitHub仓库中,你可以通过以下的地址获取示例源码:https://github.com/ramostear/Spring_Boot_2.X_Tutorial/tree/master/spring-boot-junit-rest-service 4. 项目结构下面通过一张截图来了解以下本次课程中我们使用到的项目结构。 首先我们需要位单元测试提供一个可用的Rest Controller。UserController文件为我们提供了一个可用于测试的Rest Controller。在UserController类中,我们提供两种请求类型的方法,一种是GET请求,另一种是POST请求。然后我们为这两种请求方式的方法编写单元测试用例。 在接下来的测试过程中,我们将使用Mockito来模拟请求UserService的过程,使用MockMvc来模拟请求UserController。单元测试的目的是将测试范围尽可能的缩小。在本次案例中,我们仅对UserController中的方法进行测试。 5. 初始化项目我们依然使用Spring Initializr来初始化本次课程的项目,你需要配置如下图中的参数: 现在我们需要提供两个实体类:User和Role: User.java Role.java 6. 提供可用的业务服务所有的应用都需要有数据的存储,本次课程主要的重点是为了Rest Controller的单元测试,因此使用ArrayList来充当数据库的角色。在案例中,一个用户可以有多个角色,一个角色也可以被赋予给多个用户。用户有ID,名字,别名和角色列表,角色具有ID,名称和描述。在UserService类中,将提供如图所示的公共方法。 7. 提供GET请求方法在UserController类中,我们将提供如下几个公开的GET请求方法: @GetMapping(value="/users") : 获取所有的用户信息@GetMapping(value="/users/{id}/roles") : 根据用户ID获取该用户的所有角色信息UserController.java类中的详细代码如下: package com.ramostear.spring.boot.test.restservice.controller;import com.ramostear.spring.boot.test.restservice.model.Role;import com.ramostear.spring.boot.test.restservice.model.User;import com.ramostear.spring.boot.test.restservice.service.UserService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.http.HttpStatus;import org.springframework.http.ResponseEntity;import org.springframework.web.bind.annotation.*;import java.util.List;@RestControllerpublic class UserController { private final UserService userService; @Autowired public UserController(UserService userService){ this.userService = userService; } @GetMapping(value = "/users") public List<User> findAllStudents(){ return userService.findAllUsers(); } @GetMapping(value = "/users/{id}/roles") public List<Role> findUserRoles(@PathVariable(value = "id")String id){ return userService.findUserAllRoles(id); }}8. 使用Postman对RestController进行测试我们将使用Postman工具对上述两个Rest API进行请求,首先向Postman地址栏输入http://localhost:8080/users 进行测试,获得的响应信息如下: ...

May 10, 2019 · 2 min · jiezi

Python中的单元测试

来源 | 愿码(ChainDesk.CN)内容编辑愿码Slogan | 连接每个程序员的故事网站 | http://chaindesk.cn愿码愿景 | 打造全学科IT系统免费课程,助力小白用户、初级工程师0成本免费系统学习、低成本进阶,帮助BAT一线资深工程师成长并利用自身优势创造睡后收入。官方公众号 | 愿码 | 愿码服务号 | 区块链部落免费加入愿码全思维工程师社群 | 任一公众号回复“愿码”两个字获取入群二维码本文阅读时长:11min 基本单元测试在我们开始讨论新的概念和功能之前,让我们来看看如何使用unittest来表达我们已经学到的想法。这样,我们就能有一些坚实的基础来建立我们的新理解。 采取行动的时间-用unittest测试PID我们将访问PID类(或至少访问PID类的测试)。我们将编写测试,以便它们在unittest框架内运行。 我们将使用unittest框架实现测试。 创建一个名为新文件test_pid.py在同一目录pid.py。请注意,这是一个.py文件:unittest测试是纯 python源代码,而不是包含源代码的纯文本。这意味着从纪录片的角度来看,测试的用处不大,但可以交换其他好处。将以下代码插入到新创建的test_pid.py中from unittest import TestCase, mainfrom mocker import Mockerimport pidclass test_pid_constructor(TestCase): def test_without_when(self): mocker = Mocker() mock_time = mocker.replace('time.time') mock_time() mocker.result(1.0) mocker.replay() controller = pid.PID(P=0.5, I=0.5, D=0.5, setpoint=0, initial=12) mocker.restore() mocker.verify() self.assertEqual(controller.gains, (0.5, 0.5, 0.5)) self.assertAlmostEqual(controller.setpoint[0], 0.0) self.assertEqual(len(controller.setpoint), 1) self.assertAlmostEqual(controller.previous_time, 1.0) self.assertAlmostEqual(controller.previous_error, -12.0) self.assertAlmostEqual(controller.integrated_error, 0) def test_with_when(self): controller = pid.PID(P=0.5, I=0.5, D=0.5, setpoint=1, initial=12, when=43) self.assertEqual(controller.gains, (0.5, 0.5, 0.5)) self.assertAlmostEqual(controller.setpoint[0], 1.0) self.assertEqual(len(controller.setpoint), 1) self.assertAlmostEqual(controller.previous_time, 43.0) self.assertAlmostEqual(controller.previous_error, -11.0) self.assertAlmostEqual(controller.integrated_error, 0)class test_calculate_response(TestCase): def test_without_when(self): mocker = Mocker() mock_time = mocker.replace('time.time') mock_time() mocker.result(1.0) mock_time() mocker.result(2.0) mock_time() mocker.result(3.0) mock_time() mocker.result(4.0) mock_time() mocker.result(5.0) mocker.replay() controller = pid.PID(P=0.5, I=0.5, D=0.5, setpoint=0, initial=12) self.assertEqual(controller.calculate_response(6), -3) self.assertEqual(controller.calculate_response(3), -4.5) self.assertEqual(controller.calculate_response(-1.5), -0.75) self.assertEqual(controller.calculate_response(‑2.25), ‑1.125) mocker.restore() mocker.verify() def test_with_when(self): controller = pid.PID(P=0.5, I=0.5, D=0.5, setpoint=0, initial=12, when=1) self.assertEqual(controller.calculate_response(6, 2), -3) self.assertEqual(controller.calculate_response(3, 3), -4.5) self.assertEqual(controller.calculate_response(‑1.5, 4), ‑0.75) self.assertEqual(controller.calculate_response(‑2.25, 5), ‑1.125)if __name__ == '__main__': main()键入以下命令运行测试:$ python test_pid.py ...

May 10, 2019 · 2 min · jiezi

angular单元测试遇到the icon user-o does not exist or is not registered

这篇博客本不应由我写,但由于团队要求每周最少一篇,而这周又实在不知道写啥,又正好我也遇到了这个问题。所以我就“抢”了过来……在此,感谢潘哥!团队在上周才启用前台的单元测试,对于解决前台的单元测试的错误还十分不成熟,遇到错误时经常一脸懵逼,这次的这个问题也是在潘老师的帮助下才解决的。错误描述在前台运行单元测试时出现了如下错误the icon user-o does not exist or is not registered…看意思是图标没有找到,但是运行的时候却是能够看到图标的,应该怎么解决呢?解决办法之所以找不到,是因为找的地方不对测试时默认是去/src/assets寻找,但是我们引用的图片不在那,自然找不到。比如在本项目中修改成下面这样就行了解决问题很重要,但更重要的是要知道为什么要这样解决,这也是为什么别人能解决一个bug,而自己对此却束手无策的原因。只有真正理解了大佬debug的思路,自己才能获得真正的提升,下面就让我们来看看潘老师的网上教学。为什么要这样?两点疑惑不是太懂这个单元测试的思路,为什么以前的单元测试就能通过,新写的功能也完全没调用与之相关的模块,却无法通过了。不知道前台的单元测试的意义是什么,也没有测出你写的方法的正确与否,对此应该是因为我们前台单元测试还使用的相当浅薄,并且前后台都写,不像公司里面前台就只有前台吧。

March 28, 2019 · 1 min · jiezi

模拟HTTP请求调用controller

可参考本人简书:模拟HTTP请求调用controller写在前面MockMvc实现了对Http请求的模拟,能够直接使用网络的形式,转换到Controller调用,这样使得测试速度更快,不依赖网络环境。而且提供了一套验证的工具。单测代码如下:@RunWith(SpringRunner.class)@WebMvcTest(MyController.class)public class MyControllerTest { @Autowired private MockMvc mockMvc; /** * 测试方法 / private void bindAndUnbindTenantPoiTest() throws Exception { MvcResult mvcResult = mockMvc.perform(post(${“访问的url”}) .param("${key1}", “${value1}”) .param("${key2}", “${value2}”) .param("${key3}", “${value3}”)) .andDo(print()) // 定义执行行为 .andExpect(status().isOk()) // 对请求结果进行验证 .andReturn(); // 返回一个MvcResult jsonObject = toJsonObject(mvcResult); assert jsonObject.getIntValue(“code”) == code; // 断言返回内容是否符合预期 assert message.equals(jsonObject.getString(“message”)); } }Perform介绍perform用来调用controller业务逻辑,有post、get等多种方法,具体可以参考利用Junit+MockMvc+Mockito对Http请求进行单元测试参数Param介绍通过param添加http的请求参数,格式是K-V,一个参数一个参数添加或者通过params添加MultiValueMap<String, String>。parma部分源码如下:/* * Add a request parameter to the {@link MockHttpServletRequest}. * <p>If called more than once, new values get added to existing ones. * @param name the parameter name * @param values one or more values / public MockHttpServletRequestBuilder param(String name, String… values) { addToMultiValueMap(this.parameters, name, values); return this; } /* * Add a map of request parameters to the {@link MockHttpServletRequest}, * for example when testing a form submission. * <p>If called more than once, new values get added to existing ones. * @param params the parameters to add * @since 4.2.4 */ public MockHttpServletRequestBuilder params(MultiValueMap<String, String> params) { for (String name : params.keySet()) { for (String value : params.get(name)) { this.parameters.add(name, value); } } return this; }写在后面还有个坑就是使用注解的时候,看看注解之间是否有重叠,否则会报错。如果同时使用@WebMvcTest @Configuration就错了。具体可以查看注解源码 ...

March 14, 2019 · 1 min · jiezi

Junit借助Groboutils Core进行并发测试

背景junit是无法进行并发测试,但是又有需要并发测试的场景怎么办呢?此时可以借助一个插件(Groboutils Core)来完成这种功能。maven仓库地址:点我直达实现第一步:在项目的pom.xml中加入依赖<!– https://mvnrepository.com/artifact/net.sourceforge.groboutils/groboutils-core –><dependency> <groupId>net.sourceforge.groboutils</groupId> <artifactId>groboutils-core</artifactId> <version>5</version> <scope>test</scope></dependency>第二步:在单测中进行代码编写@Test public void testConcurrentInitOrBind() { // mock一个返回 doReturn(Lists.newArrayList(userMemberCard)).when(operateCardDao) .queryCardByRegisterMobileAndTenantId(anyString(), anyLong()); TestRunnable runner = new TestRunnable() { // 在runTest方法中填写自己的测试方法 @Override public void runTest() throws Throwable { InitCardResVo resVoFirst = operateCardService.initOrBindCard(requestVo); System.out.println(“result resVoFirst is:” + resVoFirst.toString()); } }; // 一个数组,代表并发个数。此处并发5个 TestRunnable[] trs = new TestRunnable[5]; for (int i = 0; i < 5; i++) { trs[i] = runner; } MultiThreadedTestRunner mttr = new MultiThreadedTestRunner(trs); try { mttr.runTestRunnables(); } catch (Throwable ex) { ex.printStackTrace(); } } ...

March 13, 2019 · 1 min · jiezi

使用 PHPUnit 进行单元测试并生成代码覆盖率报告

安装PHPUnit使用 Composer 安装 PHPUnit#查看composer的全局bin目录 将其加入系统 path 路径 方便后续直接运行安装的命令composer global config bin-dir –absolute#全局安装 phpunitcomposer global require –dev phpunit/phpunit#查看版本phpunit –version使用Composer构建你的项目我们将新建一个unit项目用于演示单元测试的基本工作流创建项目结构mkdir unit && cd unit && mkdir app tests reports#结构如下./├── app #存放业务代码├── reports #存放覆盖率报告└── tests #存放单元测试使用Composer构建工程#一路回车即可composer init#注册命名空间vi composer.json… “autoload”: { “psr-4”: { “App\”: “app/”, “Tests\”: “tests/” } }…#更新命名空间composer dump-autoload#安装 phpunit 组件库composer require –dev phpunit/phpunit到此我们就完成项目框架的构建,下面开始写业务和测试用例。编写测试用例创建文件app/Example.php 这里我为节省排版就不写注释了<?phpnamespace App;class Example{ private $msg = “hello world”; public function getTrue() { return true; } public function getFalse() { return false; } public function setMsg($value) { $this->msg = $value; } public function getMsg() { return $this->msg; }}创建相应的测试文件tests/ExampleTest.php<?phpnamespace Tests;use PHPUnit\Framework\TestCase as BaseTestCase;use App\Example;class ExampleTest extends BaseTestCase{ public function testGetTrue() { $example = new Example(); $result = $example->getTrue(); $this->assertTrue($result); } public function testGetFalse() { $example = new Example(); $result = $example->getFalse(); $this->assertFalse($result); } public function testGetMsg() { $example = new Example(); $result = $example->getTrue(); // $result is world not big_cat $this->assertEquals($result, “hello big_cat”); }}执行单元测试[root@localhost unit]# phpunit –bootstrap=vendor/autoload.php \tests/PHPUnit 6.5.14 by Sebastian Bergmann and contributors…F 3 / 3 (100%)Time: 61 ms, Memory: 4.00MBThere was 1 failure:1) Tests\ExampleTest::testGetMsgFailed asserting that ‘hello big_cat’ matches expected true./opt/unit/tests/ExampleTest.php:27/root/.config/composer/vendor/phpunit/phpunit/src/TextUI/Command.php:195/root/.config/composer/vendor/phpunit/phpunit/src/TextUI/Command.php:148FAILURES!Tests: 3, Assertions: 3, Failures: 1.这是一个非常简单的测试用例类,可以看到,执行了共3个测试用例,共3个断言,共1个失败,可以参照PHPUnit手册学习更多高级用法。代码覆盖率代码覆盖率反应的是测试用例对测试对象的行,函数/方法,类/特质的访问率是多少(PHP_CodeCoverage 尚不支持 Opcode覆盖率、分支覆盖率 及 路径覆盖率),虽然有很多人认为过分看重覆盖率是不对的,但我们初入测试还是俗气的追求一下吧。测试覆盖率的检测对象是我们的业务代码,PHPUnit通过检测我们编写的测试用例调用了哪些函数,哪些类,哪些方法,每一个控制流程是否都执行了一遍来计算覆盖率。PHPUnit 的覆盖率依赖 Xdebug,可以生成多种格式:–coverage-clover <file> Generate code coverage report in Clover XML format.–coverage-crap4j <file> Generate code coverage report in Crap4J XML format.–coverage-html <dir> Generate code coverage report in HTML format.–coverage-php <file> Export PHP_CodeCoverage object to file.–coverage-text=<file> Generate code coverage report in text format.–coverage-xml <dir> Generate code coverage report in PHPUnit XML format.同时需要使用 –whitelist dir参数来设定我们需要检测覆盖率的业务代码路径,下面演示一下具体操作:phpunit --bootstrap vendor/autoload.php --coverage-html=reports/ --whitelist app/ \tests/#查看覆盖率报告cd reports/ && php -S 0.0.0.0:8899这样我们就对业务代码App\Example做单元测试,并且获得我们单元测试的代码覆盖率,现在自然是百分之百,因为我的测试用例已经访问了App\Example的所有方法,没有遗漏的,开发中则能体现出你的测试时用力对业务代码测试度的完善性。基境共享测试数据可能你会发现我们在每个测试方法中都创建了App\Example对象,在一些场景下是重复劳动,为什么不能只创建一次然后供其他测试方法访问呢?这需要理解 PHPUnit 执行测试用例的工作流程。我们没有办法在不同的测试方法中通过某成员属性来传递数据,因为每个测试方法的执行都是新建一个测试类对象,然后调用相应的测试方法。即测试的执行模式并不是testObj = new ExampleTest();testObj->testMethod1();testObj->testMethod2();而是testObj1 = new ExampleTest();testObj1->testMethod1();testObj2 = new ExampleTest();testObj2->testMethod2();所以testMethod1()修改的属性状态无法传递给 testMethod2()使用。PHPUnit则为我们提供了全面的hook接口:public static function setUpBeforeClass()/tearDownAfterClass()//测试类构建/解构时调用protected function setUp()/tearDown()//测试方法执行前/后调用protected function assertPreConditions()/assertPostConditions()//断言前/后调用当运行测试时,每个测试类大致就是如下的执行步骤#测试类基境构建setUpBeforeClass#new一个测试类对象#第一个测试用例setUpassertPreConditionsassertPostConditionstearDown#new一个测试类对象#第二个测试用例setUpassertPreConditionsassertPostConditionstearDown…#测试类基境解构tearDownAfterClass所以我们可以在测试类构建时使用setUpBeforeClass创建一个 App\Example 对象作为测试类的静态成员变量(tearDownAfterClass主要用于一些资源清理,比如关闭文件,数据库连接),然后让每一个测试方法用例使用它:<?phpnamespace Tests;use App\Example;use PHPUnit\Framework\TestCase as BaseTestCase;class ExampleTest extends BaseTestCase{ // 类静态属性 private static $example; public static function setUpBeforeClass() { self::$example = new Example(); } public function testGetTrue() { // 类的静态属性更新 self::$example->setMsg(“hello big_cat”); $result = self::$example->getTrue(); $this->assertTrue($result); } public function testGetFalse() { $result = self::$example->getFalse(); $this->assertFalse($result); } /** * 依赖 testGetTrue 执行完毕 * @depends testGetTrue * @return [type] [description] / public function testGetMsg() { $result = self::$example->getMsg(); $this->assertEquals($result, “hello big_cat”); }}或者使用@depends注解来声明二者的执行顺序,并使用传递参数的方式来满足需求。public function testMethod1(){ $this->assertTrue(true); return “hello”;}/* * @depends testMethod1 */public function testMethod2($str){ $this->assertEquals(“hello”, $str);}#执行模式大概如下testObj1 = new Test;$str = testObj1->testMethod1();testObj2 = new Test;testObj2->testMethod2($str);理解测试执行的模式还是很有帮助的,其他高级特性请浏览官方文档。使用phpunit.xml编排测试套件使用测试套件来管理测试,vi phpunit.xml:<?xml version=“1.0” encoding=“UTF-8”?><phpunit backupGlobals=“false” backupStaticAttributes=“false” bootstrap="./vendor/autoload.php" colors=“true” convertErrorsToExceptions=“true” convertNoticesToExceptions=“true” convertWarningsToExceptions=“true” processIsolation=“false” stopOnFailure=“false”> <testsuites> <!–可以定义多个 suffix 用于指定待执行的测试类文件后缀–> <testsuite name=“Tests”> <directory suffix=“Test.php”>./test</directory> </testsuite> </testsuites> <filter> <whitelist processUncoveredFilesFromWhitelist=“true”> <!–可以定义多个 对./app下的业务代码做覆盖率统计–> <directory suffix=".php">./app</directory> </whitelist> </filter> <logging> <!–覆盖率报告生成类型和输出目录 lowUpperBound低覆盖率阈值 highLowerBound高覆盖率阈值–> <log type=“coverage-html” target="./reports" lowUpperBound=“35” highLowerBound=“70”/> </logging></phpunit>然后直接运phpunit行即可:[root@localhost unit]# phpunit PHPUnit 6.5.14 by Sebastian Bergmann and contributors.Time: 81 ms, Memory: 4.00MBNo tests executed!Generating code coverage report in HTML format … done ...

March 7, 2019 · 2 min · jiezi

PHPUnit实践三(构建模块化的测试单元)

本系列教程所有的PHPUnit测试基于PHPUnit6.5.9版本,Lumen 5.5框架目录结构模块下的目录是符合Lumen的模块结构的如:Controllers、Models、Logics等是Lumen模块目录下的结构目录如果有自己的目录同级分配即可,如我这里的Requests整体结构├── BaseCase.php 重写过Lumen基类的测试基类,用于我们用这个基类做测试基类,后续会说明├── bootstrap.php tests自动加载文件├── Cases 测试用例目录│ └── Headline 某测试模块│ ├── logs 日志输出目录│ ├── PipeTest.php PHPUnit流程测试用例│ ├── phpunit.xml phpunit配置文件xml│ └── README.md 本模块测试用例说明├── ExampleTest.php 最原始测试demo└── TestCase.php Lumen自带的测试基类某模块的目录结构Headline //某测试模块测试用例目录├── Cache├── Controllers│ ├── ArticleTest.php│ ├── …├── Listeners│ └── MyListener.php├── Logics├── Models│ ├── ArticleTest.php│ ├── …├── README.md├── Requests│ ├── ArticleTest.php│ ├── …├── logs //日志和覆盖率目录│ ├── html│ │ ├── …│ │ └── index.html│ ├── logfile.xml│ ├── testdox.html│ └── testdox.txt├── phpunit-debug-demo.xml //phpunit.xml案例├── phpunit-debug.xml //改名后测试用的└── phpunit.xml //正式用的xml配置BaseCase.php<?phpnamespace Test;use Illuminate\Database\Eloquent\Factory;class BaseCase extends TestCase{ protected $seeder = false; const DOMAIN = “http://xxx.com”; const API_URI = []; const TOKEN = [ ’local’ => ’token*’, ‘dev’ => ’token*’, ‘prod’ => ’’ //如果测试真实请填写授权token ]; /** * 重写setUp / public function setUp() { parent::setUp(); $this->seeder = false; if (method_exists($this, ‘factory’)) { $this->app->make(‘db’); $this->factory($this->app->make(Factory::class)); if (method_exists($this, ‘seeder’)) { if (!method_exists($this, ‘seederRollback’)) { dd(“请先创建seederRollback回滚方法”); } $this->seeder = true; $this->seeder(); } } } /* * 重写tearDown / public function tearDown() { if ($this->seeder && method_exists($this, ‘seederRollback’)) { $this->seederRollback(); } parent::tearDown(); } /* * 获取地址 * @param string $apiKey * @param string $token * @return string / protected function getRequestUri($apiKey = ’list’, $token = ‘dev’, $ddinfoQuery = true) { $query = “?token=” . static::TOKEN[strtolower($token)]; if ($ddinfoQuery) { $query = $query . “&” . http_build_query(static::DDINFO); } return $apiUri = static::DOMAIN . static::API_URI[$apiKey] . $query; }}phpunit-debug-demo.xml本文件是我们单独为某些正在测试的测试用例,直接编写的xml,可以不用来回测试,已经测试成功的测试用例了,最后全部编写完测试用例,再用正式phpunit.xml即可,具体在运行测试阶段看如何指定配置<?xml version=“1.0” encoding=“UTF-8”?><phpunit bootstrap="../../bootstrap.php" convertErrorsToExceptions=“true” convertNoticesToExceptions=“false” convertWarningsToExceptions=“false” colors=“true”> <filter> <whitelist processuncoveredfilesfromwhitelist=“true”> <directory suffix=".php">../../../app/Http/Controllers/Headline</directory> <directory suffix=".php">../../../app/Http/Requests/Headline</directory> <directory suffix=".php">../../../app/Models/Headline</directory> <exclude><file>../../../app/Models/Headline/ArticleKeywordsRelationModel.php</file> </exclude> </whitelist> </filter> <testsuites> <testsuite name=“Headline Test Suite”> <directory>./</directory> </testsuite> </testsuites> <php> <ini name=“date.timezone” value=“PRC”/> <env name=“APP_ENV” value=“DEV”/> </php> <logging> <log type=“coverage-html” target=“logs/html/” lowUpperBound=“35” highLowerBound=“70”/> <log type=“json” target=“logs/logfile.json”/> <log type=“tap” target=“logs/logfile.tap”/> <log type=“junit” target=“logs/logfile.xml” logIncompleteSkipped=“false”/> <log type=“testdox-html” target=“logs/testdox.html”/> <log type=“testdox-text” target=“logs/testdox.txt”/> </logging> <listeners> <!–<listener class="\Test\Cases\Headline\Listeners\MyListener" file="./Listeners/MyListener.php">–> <!–<arguments>–> <!–<array>–> <!–<element key=“0”>–> <!–<string>Sebastian</string>–> <!–</element>–> <!–</array>–> <!–<integer>22</integer>–> <!–<string>April</string>–> <!–<double>19.78</double>–> <!–<null/>–> <!–<object class=“stdClass”/>–> <!–</arguments>–> <!–</listener>–> <!–<listener class="\Test\Cases\Headline\Listeners\MyListener" file="./Listeners/MyListener.php">–> <!–<arguments>–> <!–<array>–> <!–<element key=“0”>–> <!–<string>Sebastian</string>–> <!–</element>–> <!–</array>–> <!–<integer>22</integer>–> <!–</arguments>–> <!–</listener>–> </listeners></phpunit>测试用例案例<?php/* * Created by PhpStorm. * User: qikailin * Date: 2019-01-29 * Time: 11:57 /namespace Test\Cases\Headline\Articles;use App\Http\Controllers\Headline\ArticleController;use App\Models\Headline\ArticleCategoryRelationModel;use App\Models\Headline\ArticleContentModel;use App\Models\Headline\ArticleKeywordsRelationModel;use App\Models\Headline\ArticlesModel;use Faker\Generator;use Illuminate\Http\Request;use Test\BaseCase;class ArticleTest extends BaseCase{ private static $model; public static function setUpBeforeClass() { parent::setUpBeforeClass(); self::$model = new ArticlesModel(); } /* * 生成factory faker 数据构建模型对象 * @codeCoverageIgnore / public function factory($factory) { $words = [“测试”, “文章”, “模糊”, “搜索”]; $id = 262; $factory->define(ArticlesModel::class, function (Generator $faker) use (&$id, $words) { $id++; return [ ‘id’ => $id, ‘uri’ => $faker->lexify(‘T???????????????????’), ’title’ => $id == 263 ? “搜索” : $words[rand(0, sizeof($words) - 1)], ‘authorId’ => 1, ‘state’ => 1, ‘isUpdated’ => 0, ]; }); } /* * 生成模拟的数据,需seederRollback 成对出现 / public function seeder() { $articles = factory(ArticlesModel::class, 10)->make(); foreach ($articles as $article) { // 注意: article为引用对象,不是copy if ($article->isRecommend) { $article->recommendTime = time(); } $article->save(); } } /* * getArticleList 测试数据 * @return array / public function getArticleListDataProvider() { return [ [1, “搜索”, 1, 10, 1], [2, “搜索”, 1, 10, 0], [2, null, 1, 10, 0], [3, “搜索”, 1, 10, 0], [1, null, 1, 10, 1], [2, null, 1, 10, 0], [3, null, 1, 10, 0], ]; } /* * @dataProvider getArticleListDataProvider / public function testGetArticleList($type, $searchText, $page, $pageSize, $expceted) { $rst = self::$model->getArticleList($type, $searchText, $page, $pageSize); $this->assertGreaterThanOrEqual($expceted, sizeof($rst)); $rst = self::$model->getArticleCount($type, $searchText); $this->assertGreaterThanOrEqual($expceted, $rst); } /* * addArticle 测试数据 * @return array / public function addArticleDataProvider() { return [ [ [ ‘id’ => 273, ‘uri’ => ‘dddddddddd0123’ ], ‘save’, 0 ], [ [ ‘id’ => 274, ‘uri’ => ‘dddddddddd123’ ], ‘publish’, 0 ], [ [ ‘id’ => 275, ‘uri’ => ‘dddddddddd456’ ], ‘preview’, 0 ], ]; } /* * @dataProvider addArticleDataProvider / public function testAdd($data, $action, $expected) { $rst = self::$model->addArticle($data, $action); if ($rst) { self::$model::where(‘id’, $rst)->delete(); } $this->assertGreaterThanOrEqual($expected, $rst); } public function testGetArticleInfo() { $rst = self::$model->getArticleInfo(263, 0); $this->assertGreaterThanOrEqual(1, sizeof($rst)); $rst = self::$model->getArticleInfo(2000, 1); $this->assertEquals(0, sizeof($rst)); } /* * 回滚模拟的数据到初始状态 */ public function seederRollback() { self::$model::where(‘id’, ‘>=’, 263)->where(‘id’, ‘<=’, 272)->delete(); }}运行测试cd {APPROOT}/tests/Cases/Headline# mv phpunit-debug-custom.xml -> phpunit-debug.xml../../../vendor/bin/phpunit –verbose -c phpunit-debug.xml参考PHPUnit 5.0 官方中文手册 ...

February 15, 2019 · 3 min · jiezi

PHPUnit实践一(初识)

本系列教程所有的PHPUnit测试基于PHPUnit6.5.9版本,Lumen 5.5框架前置日常我们的普通用到的测试:代码直接echo,debug等方法测试 -> 跟踪细节断点型测试log日志辅助测试 -> 跟踪细节断点型测试辅助工具,postman之类的做请求类测试->请求类测试浏览器直接测试->浏览器测试单元测试单元测试是针对程序的最小单元来进行正确性检验的测试工作,程序单元就是应用的最小可测试部件,一个单元可能是单个程序,类,对象,方法等单元测试是用来测试包或者程序的一部分代码或者一组代码的函数。测试的目的是确认目标代码在给定的场景下,有没有按照期望工作。一个场景是正向路经测试,就是在正常执行的情况下,保证代码不产生错误的测试。这种测试可以用来确认代码可以成功地向数据库中插入一条工作记录。另外一些单元测试可能会测试负向路径的场景,保证代码不仅会产生错误,而且是预期的错误。这种场景下的测试可能是对数据库进行查询时没有找到任何结果,或者对数据库做了无效的更新。在这两种情况下,测试都要验证确实产生了错误,且产生的是预期的错误。总之,不管如何调用或者执行代码,所写的代码行为都是可预期的优点或改善解决问题减少bug通过运行单元测试可以直接测试各个功能的正确性,有bug可以直接发现并解决,如果要等到跟其他的功能对接,进行连贯测试,测试比较麻烦,而且bug不能及早的发现并解决快速定位bug如果是web项目的某一个功能,平常我们定位bug可能是页面输入值,后台断点,一步一步的需要bug位置,如果有编写单元测试,则可以直接修改数据,运行单元测试即可,快速有限提高代码质量如果每一个部件都是完美的,那么组合起来肯定也是完美的。整体代码质量就得到了保障减少调试时间当不知问题所在的时候,可能需要各种调试与运行,而如果所有的都有编写单元测试,那么可以直接运行单元测试,就能定位问题所在位置。PHPUnitPHPUnit是一个面向PHP程序员的测试框架,这是一个xUnit的体系结构的单元测试框架。版本主版本初始版本PHP兼容性支持后台框架对应版本PHPUnit 82019年2月1日PHP 7.2, PHP 7.3, PHP 7.4在2021年2月5日结束支持 PHPUnit 72018年2月2日PHP 7.1, PHP 7.2, PHP 7.3在2020年2月7日结束支持 PHPUnit 62017年2月3日PHP 7.0, PHP 7.1, PHP 7.2在2019年2月1日结束支持*PHPUnit 52015年10月2日PHP 5.6, PHP 7.0, PHP 7.1在2018年2月2日结束支持 PHPUnit 42014年3月7日PHP 5.3, PHP 5.4, PHP 5.5, PHP 5.6在2017年2月3日结束支持 你的第一个单元测试demo目录结构tests├── ExampleTest.php 测试用例└── TestCase.php Lumen自带测试基类,继承PHPunit代码<?phpclass ExampleTest extends TestCase{ /** * 测试断言成功. * * @return void / public function testTrue() { $this->assertTrue(true); } /* * 测试断言失败 * * @return void / public function testFailure() { $this->assertTrue(false); } /* * 测试不加断言,risky. * * @return void */ public function testRisky() { }}运行../vendor/bin/phpunit ExampleTest.php输出PHPUnit 6.5.9 by Sebastian Bergmann and contributors..FR 3 / 3 (100%)Time: 902 ms, Memory: 10.00MBThere was 1 failure:1) ExampleTest::testFailureFailed asserting that false is true./web/www/wpt/gt-api/tests/ExampleTest.php:22–There was 1 risky test:1) ExampleTest::testRiskyThis test did not perform any assertionsFAILURES!Tests: 3, Assertions: 2, Failures: 1, Risky: 1.说明3个测试方法,2个断言 一个断言失败,一个测试方法无断言输出标识说明.当测试成功时输出。F当测试方法运行过程中一个断言失败时输出。E当测试方法运行过程中产生一个错误时输出。R当测试被标记为有风险时输出。S当测试被跳过时输出。I当测试被标记为不完整或未实现时输出。参考PHPUnit 6.5 官方文档 ...

January 30, 2019 · 1 min · jiezi

使用karma+mocha+chai+sinon+@vue/test-utils为你的组件库增加单元测试

项目github地址:https://github.com/yuanalina/installAsRequired这里必须要提前说明,前端项目的单元测试不是必须的,特别是业务型项目,增加单元测试反而会成为累赘,增加开发成本且无意义,业务型的项目需求常常变动,UI也经常更改,增加单元测试,需要在开发过程中不断更新开发测试用例,增加开发成本。但是,项目中的一些公共封装,比如公共的组件、公用的功能模块等是可以使用单元测试的。为什么组件库需要使用单元测试搭建完组件库的环境后,进入开发阶段,当开发完成组件,在说明文档中调试完毕后,到正式在项目中使用组件时,发现没有合适的项目或者说合适的方法去确保组件本身功能是没有问题了,再引用到项目中。毕竟组件是通过发布为npm包的形式为其他项目所使用,如果组件本身就有很多bug,那调试过程将是很繁琐的,需要不断的重复发布npm包,项目更新引用npm包,繁琐的操作浪费本就宝贵的开发时间。因此特为组件库引入单元测试,目的在于能减少组件的bug,避免重复的发布不必要的npm包。技术栈组件库单元测试,使用的各技术为:karma+mocha+chai+sinon+@vue/test-utils。下面做简单介绍,并贴上个人觉得简单有效的学习链接作为参考。karmakarma是一个测试运行器,为开发者提供高效的测试环境,主要作用是将项目运行在各种主流Web浏览器进行测试。关于karma的学习,建议看官方文档。组件库项目是通过vue-cli搭建的,项目生成时karma相关配置就已经设置好了,关于karma,可以先作为了解即可。mochamocha是一个测试框架,兼容多种断言库,mocha的学习可以看阮一峰老师的测试框架 Mocha 实例教程进行了解。chaichai是一个测试断言库,所谓断言,就是对组件做一些操作,并预言产生的结果。如果测试结果与断言相同则测试通过。chai的学习可以参阅Chai.js断言库API中文文档sinonsinon是一个测试工具,可以使用sinon来进行模拟http等异步请求操作,作为间谍监听回调函数调用等功能来帮助我们更轻松实现测试。sinon学习参阅:sinon入门,关于模拟http请求:利用SinonJS测试 AJAX 请求例子@vue/test-utils@vue/test-utils是vue官方推荐的vue测试工具,使用这个工具我们可以让我们更方便的测试vue项目。官方文档:vue-test-utils环境搭建在用vue-cli构建项目时,Set up unit test输入y(yes),Pick a test runner 选择Karma and mocha即可生成单元测试开发环境什么?你的项目生成时Set up unit test输入的是n(no)?别着急,跟着下面步骤来,搭建环境。1、首先安装全部单元测试需要的依赖npm i -D karma karma-webpack phantomjs-prebuilt karma-phantomjs-launcher karma-phantomjs-shim karma-chrome-launcher karma-sourcemap-loader mocha karma-mocha sinon chai sinon-chai karma-sinon-chai kbaocunrma-spec-reporter karma-coverage @vue/test-utils2、将vue-cli中关于单元测试的相关文件copy到项目相应位置3、修改package.json,增加单元测试启动命令"unit": “cross-env BABEL_ENV=test karma start test/unit/karma.conf.js –single-run"4、修改、增加chrome运行环境安装chrome相关依赖npm i -D chromedriver karma-chrome-launcher修改karma.conf.js文件到这里环境就搭建完毕了,在src/components目录中增加一个HelloWorld.vue,执行npm run unit命令就可以将单元测试跑起来啦目录结构:运行结果,看见一片飘绿就是成功了测试用例开发示例环境搭建完成就可以进行测试用例的开发了,这里以button组件为例示范测试用例如何开发在test/unit/specs目录中创建一个以.spec.js结尾的文件,在文件中引入需要测试的.vue文件即可运行结果:踩过的坑不得不说,从搭建单元测试到开发环境到完成测试用例开发,真的是踩坑无数。。。这里做一个小小汇总,希望当你开发中遇到类似问题能对你有所帮助,也作为我个人的一个记录1、karma.conf.js中的browsers参数需要改成Chrome,并安装chrome相关依赖;2、要测试的vue组件有依赖其他第三方插件,需要在@vue/test-utils中引入localVue,并将第三方插件注册到localVue中,mount挂载组件生成wrapper时,将localVue作为参数传递;3、要测试的组件引入element-ui,除了要在localVue中注册外,还需引入@vue/test-utils的config,并进行配置: config.stubs.transition = false config.stubs[’transition-group’] = false4、使用了element-ui的按钮等元素,绑定原生事件(比如点击事件)时,加上.native:@click.native=“click"5、有异步的内容,比如延时定时器,不要忘记done(),否则不会被捕获;还有很多不知为何会发生的错误,也许是我的打开方式不对?小伙伴们开发中有好的方法欢迎指正~~本文结束啦~希望对你有所帮助。。学无止境,与诸君共勉~~

January 21, 2019 · 1 min · jiezi

在Vue项目中使用snapshot测试

在Vue项目中使用snapshot测试snapshot介绍snapshot测试又称快照测试,可以直观地反映出组件UI是否发生了未预见到的变化。snapshot如字面上所示,直观描述出组件的样子。通过对比前后的快照,可以很快找出UI的变化之处。第一次运行快照测试时会生成一个快照文件。之后每次执行测试的时候,会生成一个快照,然后对比最初生成的快照文件,如果没有发生改变,则通过测试。否则测试不通过,同时会输出结果,对比不匹配的地方。jest中的快照文件以为snap拓展名结尾,格式如下(ps: 在没有了解之前,我还以为是快照文件是截图)。一个快照文件中可以包含多个快照,快照的格式其实是HTML字符串,对于UI组件,其HTML会反映出其内部的state。每次测试只需要对比字符串是否符合初始快照即可。exports[button 1] = "&lt;div&gt;&lt;span class=\\"count\\"&gt;1&lt;/span&gt; &lt;button&gt;Increment&lt;/button&gt; &lt;button class=\\"desc\\"&gt;Descrement&lt;/button&gt; &lt;button class=\\"custom\\"&gt;not emitted&lt;/button&gt;&lt;/div&gt;";snapshot测试不通过的原因有两个。一个原因是组件发生了未曾预见的变化,此时应检查代码。另一个原因是组件更新而快照文件并没有更新,此时要运行jest -u更新快照。› 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with -u to update them.结合Vue进行snapshot测试生成快照时需要渲染并挂载组件,在Vue中可以使用官方的单元测试实用工具Vue Test Utils。Vue Test Utils 提供了mount、shallowMount这两个方法,用于创建一个包含被挂载和渲染的 Vue 组件的 Wrapper。component是一个vue组件,options是实例化Vue时的配置,包括挂载选项和其他选项(非挂载选项,会将它们通过extend覆写到其组件选项),结果返回一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法的Wrapper实例。mount(component:{Component}, options:{Object})shallowMount与mount不同的是被存根的子组件,详细请戳文档。Wrapper上的丰富的属性和方法,足以应付本文中的测试需求。html()方法返回Wrapper DOM 节点的 HTML 字符串。find()和findAll()可以查找Wrapper里的DOM节点或Vue组件,可用于查找监听事件的元素。trigger可以在DOM节点/组件上触发一个事件。结合上述的方法,我们可以完成一个模拟事件触发的快照测试。细心的读者可能会发现,我们平时在使用Vue时,数据更新后视图并不会立即更新,需要在nextTick回调中处理更新完成后的任务。但在 Vue Test Utils 中,为简化用法,更新是同步的,所以无需在测试中使用 Vue.nextTick 来等待 DOM 更新。demo演示Vue Test Utils官方文档中提供了一个集成VTU和Jest的demo,不过这个demo比较旧,官方推荐用CLI3创建项目。执行vue create vue-snapshot-demo创建demo项目,创建时要选择单元测试,提供的库有Mocha + Chai及Jest,在这里选择Jest.安装完成之后运行npm run serve即可运行项目。本文中将用一个简单的Todo应用项目来演示。这个Todo应用有简单的添加、删除和修改Todo项状态的功能;Todo项的状态有已完成和未完成,已完成时不可删除,未完成时可删除;已完成的Todo项会用一条线横贯文本,未完成项会在鼠标悬浮时展示删除按钮。组件简单地划分为Todo和TodoItem。TodoItem在Todo项未完成且触发mouseover事件时会展示删除按钮,触发mouseleave时则隐藏按钮(这样可以在快照测试中模拟事件)。TodoItem中有一个checkbox,用于切换Todo项的状态。Todo项完成时会有一个todo-finished类,用于实现删除线效果。为方便这里只介绍TodoItem组件的代码和测试。<template> <li :class="[’todo-item’, item.finished?’todo-finished’:’’]" @mouseover=“handleItemMouseIn” @mouseleave=“handleItemMouseLeave” > <input type=“checkbox” v-model=“item.finished”> <span class=“content”>{{item.content}}</span> <button class=“del-btn” v-show="!item.finished&&hover" @click=“emitDelete”>delete</button> </li></template><script>export default { name: “TodoItem”, props: { item: Object }, data() { return { hover: false }; }, methods: { handleItemMouseIn() { this.hover = true; }, handleItemMouseLeave() { this.hover = false; }, emitDelete() { this.$emit(“delete”); } }};</script><style lang=“scss”>.todo-item { list-style: none; padding: 4px 16px; height: 22px; line-height: 22px; .content { margin-left: 16px; } .del-btn { margin-left: 16px; } &.todo-finished { text-decoration: line-through; }}</style>进行快照测试时,除了测试数据渲染是否正确外还可以模拟事件。这里只贴快照测试用例的代码,完整的代码戳我。describe(‘TodoItem snapshot test’, () => { it(‘first render’, () => { const wrapper = shallowMount(TodoItem, { propsData: { item: { finished: true, content: ’test TodoItem’ } } }) expect(wrapper.html()).toMatchSnapshot() }) it(’toggle checked’, () => { const renderer = createRenderer(); const wrapper = shallowMount(TodoItem, { propsData: { item: { finished: true, content: ’test TodoItem’ } } }) const checkbox = wrapper.find(‘input’); checkbox.trigger(‘click’); renderer.renderToString(wrapper.vm, (err, str) => { expect(str).toMatchSnapshot() }) }) it(‘mouseover’, () => { const renderer = createRenderer(); const wrapper = shallowMount(TodoItem, { propsData: { item: { finished: false, content: ’test TodoItem’ } } }) wrapper.trigger(‘mouseover’); renderer.renderToString(wrapper.vm, (err, str) => { expect(str).toMatchSnapshot() }) })})这里有三个测试。第二个测试模拟checkbox点击,将Todo项从已完成切换到未完成,期待类todo-finished会被移除。第三个测试在未完成Todo项上模拟鼠标悬浮,触发mouseover事件,期待删除按钮会展示。这里使用toMatchSnapshot()来进行匹配快照。这里生成快照文件所需的HTML字符串有wrapper.html()和Renderer.renderToString这两种方式,区别在于前者是同步获取,后者是异步获取。测试模拟事件时,最好以异步方式获取HTML字符串。同步方式获取的字符串并不一定是UI更新后的视图。尽管VTU文档中说所有的更新都是同步,但实际上在第二个快照测试中,如果使用expect(wrapper.html()).toMatchSnapshot(),生成的快照文件中Todo项仍有类todo-finished,期待的结果应该是没有类todo-finished,结果并非更新后的视图。而在第三个快照测试中,使用expect(wrapper.html()).toMatchSnapshot()生成的快照,按钮如期望展示,是UI更新后的视图。所以才不建议在DOM更新的情况下使用wrapper.html()获取HTML字符串。下面是两种对比的结果,1是使用wrapper.html()生成的快照,2是使用Renderer.renderToString生成的。exports[TodoItem snapshot test mouseover 1] = &lt;li class="todo-item"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn" style=""&gt;delete&lt;/button&gt;&lt;/li&gt;;exports[TodoItem snapshot test mouseover 2] = &lt;li class="todo-item"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn"&gt;delete&lt;/button&gt;&lt;/li&gt;;exports[TodoItem snapshot test toggle checked 1] = &lt;li class="todo-item todo-finished"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn" style="display: none;"&gt;delete&lt;/button&gt;&lt;/li&gt;;exports[TodoItem snapshot test toggle checked 2] = &lt;li class="todo-item"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn" style="display:none;"&gt;delete&lt;/button&gt;&lt;/li&gt;;这里使用vue-server-renderer提供的createRenderer来生成一个Renderer实例,实例方法renderToString来获取HTML字符串。这种是典型的回调风格,断言语句在回调中执行即可。 // … wrapper.trigger(‘mouseover’); renderer.renderToString(wrapper.vm, (err, str) => { expect(str).toMatchSnapshot() })如果不想使用这个库,也可以使用VTU中提供的异步案例。由于wrapper.html()是同步获取,所以获取操作及断言语句需要在Vue.nextTick()返回的Promise中执行。 // … wrapper.trigger(‘mouseover’); Vue.nextTick().then(()=>{ expect(wrapper.html()).toMatchSnapshot() })观察测试结果执行npm run test:unit或yarn test:unit运行测试。初次执行,终端输出会有Snapshots: 3 written, 3 total这一行,表示新增三个快照测试,并生成初始快照文件。 › 3 snapshots written.Snapshot Summary › 3 snapshots written from 1 test suite.Test Suites: 1 passed, 1 totalTests: 7 passed, 7 totalSnapshots: 3 written, 3 totalTime: 2.012sRan all test suites.Done in 3.13s.快照文件如下示:// Jest Snapshot v1, https://goo.gl/fbAQLPexports[`TodoItem snapshot test first render 1] = <li class=“todo-item todo-finished”><input type=“checkbox”> <span class=“content”>test TodoItem</span> <button class=“del-btn” style=“display: none;">delete</button></li>;exports[TodoItem snapshot test mouseover 1] = <li class=“todo-item”><input type=“checkbox”> <span class=“content”>test TodoItem</span> <button class=“del-btn”>delete</button></li>;exports[TodoItem snapshot test toggle checked 1] = <li class=“todo-item”><input type=“checkbox”> <span class=“content”>test TodoItem</span> <button class=“del-btn” style=“display:none;">delete</button></li>;第二次执行测试后,输出中有Snapshots: 3 passed, 3 total,表示有三个快照测试成功通过,总共有三个快照测试。Test Suites: 1 passed, 1 totalTests: 7 passed, 7 totalSnapshots: 3 passed, 3 totalTime: 2sRan all test suites.Done in 3.11s.修改第一个快照中传入的content,重新运行测试时,终端会输出不匹配的地方,输出数据的格式与Git类似,会标明哪一行是新增的,哪一行是被删除的,并提示不匹配代码所在行。 - Snapshot + Received - &lt;li class="todo-item todo-finished"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem&lt;/span&gt; &lt;button class="del-btn" style="display: none;"&gt;delete&lt;/button&gt;&lt;/li&gt; + &lt;li class="todo-item todo-finished"&gt;&lt;input type="checkbox"&gt; &lt;span class="content"&gt;test TodoItem content change&lt;/span&gt; &lt;button class="del-btn" style="display: none;"&gt;delete&lt;/button&gt;&lt;/li&gt; 88 | } 89 | }) &gt; 90 | expect(wrapper.html()).toMatchSnapshot() | ^ 91 | }) 92 | 93 | it('toggle checked', () =&gt; { at Object.toMatchSnapshot (tests/unit/TodoItem.spec.js:90:32)同时会提醒你检查代码是否错误或重新运行测试并提供参数-u以更新快照文件。Snapshot Summary › 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with -u` to update them.执行npm run test:unit – -u或yarn test:unit -u更新快照,输出如下示,可以发现有一个快照测试的输出更新了。下次快照测试对照的文件是这个更新后的文件。Test Suites: 1 passed, 1 totalTests: 7 passed, 7 totalSnapshots: 1 updated, 2 passed, 3 totalTime: 2.104s, estimated 3sRan all test suites.Done in 2.93s.其他除了使用toMatchSnapshot()外,还可以使用toMatchInlineSnapshot()。二者不同之处在于toMatchSnapshot()从快照文件中查找快照,而toMatchInlineSnapshot()则将传入的参数当成快照文件进行匹配。配置JestJest配置可以保存在jest.config.js文件里,可以保存在package.json里,用键名jest表示,同时也允许行内配置。介绍几个常用的配置。rootDir查找Jest配置的目录,默认是pwd。testMatchjest查找测试文件的匹配规则,默认是[ “/tests//.js?(x)”, “**/?(.)+(spec|test).js?(x)” ]。默认查找在__test__文件夹中的js/jsx文件和以.test/.spec结尾的js/jsx文件,同时包括test.js和spec.js。snapshotSerializers生成的快照文件中HTML文本没有换行,是否能进行换行美化呢?答案是肯定的。可以在配置中添加snapshotSerializers,接受一个数组,可以对匹配的快照文件做处理。jest-serializer-vue这个库做的就是这样任务。如果你想要实现这个自己的序列化任务,需要实现的方法有test和print。test用于筛选处理的快照,print返回处理后的结果。后记在未了解测试之前,我一直以为测试是枯燥无聊的。了解过快照测试后,我发现测试其实蛮有趣且实用,同时由衷地感叹快照测试的巧妙之处。如果这个简单的案例能让你了解快照测试的作用及使用方法,就是我最大的收获。如果有问题或错误之处,欢迎指出交流。参考链接vue-test-utils-jest-exampleJest - Snapshot TestingVue Test UtilsVue SSR 指南 ...

December 29, 2018 · 3 min · jiezi

使用testify和mockery库简化单元测试

前言2016年我写过一篇关于Go语言单元测试的文章,简单介绍了 testing 库的使用方法。后来发现 testify/require 和 testify/assert 可以大大简化单元测试的写法,完全可以替代 t.Fatalf 和 t.Errorf,而且代码实现更为简短、优雅。再后来,发现了 mockery 库,它可以为 Go interface 生成一个 mocks struct。通过 mocks struct,在单元测试中我们可以模拟所有 normal cases 和 corner cases,彻底消除细节实现上的bug。mocks 在测试无状态函数 (对应 FP 中的 pure function) 中意义不大,其应用场景主要在于处理不可控的第三方服务、数据库、磁盘读写等。如果这些服务的调用细节已经被封装到 interface 内部,调用方只看到了 interface 定义的一组方法,那么在测试中 mocks 就能控制第三方服务返回任意期望的结果,进而实现对调用方逻辑的全方位测试。关于 interface 的诸多用法,我会单独拎出来一篇文章来讲。本文中,我会通过两个例子展示 testify/require 和 mockery 的用法,分别是:使用 testify/require 简化 table driven test使用 mockery 和 testify/mock 为 lazy cache 写单元测试准备工作# download require, assert, mockgo get -u -v github.com/stretchr/testify# install mockery into GoBingo get -u -v github.com/vektra/mockery/…/testify/require首先,我们通过一个简单的例子看下 require 的用法。我们针对函数 Sqrt 进行测试,其实现为:// Sqrt calculate the square root of a non-negative float64 // number with max error of 10^-9. For simplicity, we don’t // discard the part with is smaller than 10^-9.func Sqrt(x float64) float64 { if x < 0 { panic(“cannot be negative”) } if x == 0 { return 0 } a := x / 2 b := (a + 2) / 2 erro := a - b for erro >= 0.000000001 || erro <= -0.000000001 { a = b b = (b + x/b) / 2 erro = a - b } return b}这里我们使用了一个常规的方法实现 Sqrt,该实现的最大精确度是到小数点后9位(为了方便演示,这里没有对超出9位的部分进行删除)。我们首先测试 x < 0 导致 panic 的情况,看 require 如何使用,测试代码如下:func TestSqrt_Panic(t *testing.T) { defer func() { r := recover() require.Equal(t, “cannot be negative”, r) }() _ = Sqrt(-1)}在上面的函数中,我们只使用 require.Equal 一行代码就实现了运行结果校验。如果使用 testing 来实现的话,变成了三行,并且需要手写一串描述:func TestSqrt_Panic(t *testing.T) { defer func() { r := recover() if r.(string) != “cannot be negative” { t.Fatalf(“expect to panic with message "cannot be negative", but got "%s"\n”, r) } }() _ = Sqrt(-1)}使用 require 之后,不仅使测试代码更易于编写,而且能够在测试运行失败时,格式化运行结果,方便定位和修改bug。这里你不妨把 -1 改成一个正数,运行 go test,查看运行结果。上面我们能够看到 require 库带来的编码和调试效率的上升。在 table driven test 中,我们会有更深刻的体会。Table Driven Test我们仍然以 Sqrt 为例,来看下如何在 table driven test 中使用 require。这里我们测试的传入常规参数的情况,代码实现如下:func TestSqrt(t *testing.T) { testcases := []struct { desc string input float64 expect float64 }{ { desc: “zero”, input: 0, expect: 0, }, { desc: “one”, input: 1, expect: 1, }, { desc: “a very small rational number”, input: 0.00000000000000000000000001, expect: 0.0, }, { desc: “rational number result: 2.56”, input: 2.56, expect: 1.6, }, { desc: “irrational number result: 2”, input: 2, expect: 1.414213562, }, } for _, ts := range testcases { got := Sqrt(ts.input) erro := got - ts.expect require.True(t, erro < 0.000000001 && erro > -0.000000001, ts.desc) }}在上面这个例子,有三点值得注意:匿名struct 允许我们填充任意类型的字段,非常方便于构建测试数据集;每个匿名struct都包含一个 desc string 字段,用于描述该测试要处理的状况。在测试运行失败时,非常有助于定位失败位置;使用 require 而不是 assert,因为使用 require 时,测试失败以后,所有测试都会停止执行。关于 require,除了本文中提到的 require.True, require.Equal,还有一个比较实用的方法是 require.EqualValues,它的应用场景在于处理 Go 的强类型问题,我们不妨看一段代码:func Test_Require_EqualValues(t *testing.T) { // tests will pass require.EqualValues(t, 12, 12.0, “compare int32 and float64”) require.EqualValues(t, 12, int64(12), “compare int32 and int64”) // tests will fail require.Equal(t, 12, 12.0, “compare int32 and float64”) require.Equal(t, 12, int64(12), “compare int32 and int64”)}更多 require 的方法参考 require’s godoc。mockerymockery 与 Go 指令(directive) 结合使用,我们可以为 interface 快速创建对应的 mock struct。即便没有具体实现,也可以被其他包调用。我们通过 LazyCache 的例子来看它的使用方法。假设有一个第三方服务,我们把它封装在 thirdpartyapi 包里,并加入 go directive,代码如下:package thirdpartyapi//go:generate mockery -name=Client// Client defines operations a third party service hastype Client interface { Get(key string) (data interface{}, err error)}我们在 thirdpartyapi 目录下执行 go generate,在 mocks 目录下生成对应的 mock struct。目录结构如下:~ $ tree thirdpartyapi/thirdpartyapi/├── client.go└── mocks └── Client.go1 directory, 2 files在执行 go generate 时,指令 //go:generate mockery -name=Client 被触发。它本质上是 mockery -name=Client 的快捷方式,优势是 go generate 可以批量执行多个目录下的多个指令(需要多加一个参数,具体可以参考文档)。此时,我们只有 interface,并没有具体的实现,但是不妨碍在 LazyCache 中调用它,也不妨碍在测试中调用 thirdpartyapi 的 mocks client。为了方便理解,这里把 LazyCache 的实现也贴出来 (忽略 import)://go:generate mockery -name=LazyCache// LazyCache defines the methods for the cachetype LazyCache interface { Get(key string) (data interface{}, err error)}// NewLazyCache instantiates a default lazy cache implementationfunc NewLazyCache(client thirdpartyapi.Client, timeout time.Duration) LazyCache { return &lazyCacheImpl{ cacheStore: make(map[string]cacheValueType), thirdPartyClient: client, timeout: timeout, }}type cacheValueType struct { data interface{} lastUpdated time.Time}type lazyCacheImpl struct { sync.RWMutex cacheStore map[string]cacheValueType thirdPartyClient thirdpartyapi.Client timeout time.Duration // cache would expire after timeout}// Get implements LazyCache interfacefunc (c *lazyCacheImpl) Get(key string) (data interface{}, err error) { c.RLock() val := c.cacheStore[key] c.RUnlock() timeNow := time.Now() if timeNow.After(val.lastUpdated.Add(c.timeout)) { // fetch data from third party service and update cache latest, err := c.thirdPartyClient.Get(key) if err != nil { return nil, err } val = cacheValueType{latest, timeNow} c.Lock() c.cacheStore[key] = val c.Unlock() } return val.data, nil}为了简单,我们暂时不考虑 cache miss 或 timeout 与cache被更新的时间间隙,大量请求直接打到 thirdpartyapi 可能导致的后果。介绍测试之前,我们首先了解一下 “控制变量法”,在自然科学中,它被广泛用于各类实验中。在智库百科,它被定义为 指把多因素的问题变成多个单因素的问题,而只改变其中的某一个因素,从而研究这个因素对事物影响,分别加以研究,最后再综合解决的方法。该方法同样适用于计算机科学,尤其是测试不同场景下程序是否能如期望般运行。我们将这种方法应用于本例中 Get 方法的测试。在 Get 方法中,可变因素有 cacheStore、thirdPartyClient 和 timeout。在测试中,cacheStore 和 timeout 是完全可控的,thirdPartyClient 的行为需要通过 mocks 自定义期望行为以覆盖默认实现。事实上,mocks 的功能要强大的多,下面我们用代码来看。为 LazyCache 写测试这里,我只拿出 Cache Miss Update Failure 一个case 来分析,覆盖所有 case 的代码查看 github repo。func TestGet_CacheMiss_Update_Failure(t *testing.T) { testKey := “test_key” errTest := errors.New(“test error”) mockThirdParty := &mocks.Client{} mockThirdParty.On(“Get”, testKey).Return(nil, errTest).Once() mockCache := &lazyCacheImpl{ memStore: map[string]cacheValueType{}, thirdPartyClient: mockThirdParty, timeout: testTimeout, } // test cache miss, fails to fetch from data source , gotErr := mockCache.Get(testKey) require.Equal(t, errTest, gotErr) mock.AssertExpectationsForObjects(t, mockThirdParty)}这里,我们只讨论 mockThirdParty,主要有三点:mockThirdParty.On(“Get”, testKey).Return(nil, errTest).Once() 用于定义该对象 Get 方法的行为:Get 方法接受 testKey 作为参数,当且仅当被调用一次时,会返回 errTest。如果同样的参数,被调用第二次,就会报错;, gotErr := mockCache.Get(testKey) 触发一次上一步中定义的行为;mock.AssertExpectationsForObjects 函数会对传入对象进行检查,保证预定义的期望行为完全被精确地触发;在 table driven test 中,我们可以通过 mockThirdParty.On 方法定义 Get 针对不同参数返回不同的结果。在上面的测试中 .Once() 等价于 .Times(1)。如果去掉 .Once(),意味着 mockThirdParty.Get 方法可以被调用任意次。更多 mockery 的使用方法参考 github小结在本文中,我们结合实例讲解了 testify 和 mockery 两个库在单元测试中的作用。最后分享一个图,希望大家能重视单元测试。相关链接示例代码testifymockeryThe Outrageous Cost of Skipping TDD & Code Reviews扫码关注微信公众号“深入Go语言” ...

November 4, 2018 · 4 min · jiezi

从0到1实现Promise

前言Promise大家一定都不陌生了,JavaScript异步流程从最初的Callback,到Promise,到Generator,再到目前使用最多的Async/Await(如果对于这些不熟悉的可以参考我另一篇文章《JavaScript异步编程》),这不仅仅是技术实现的发展,更是思想上对于如何控制异步的递进。Promise作为后续方案的基础,是重中之重,也是面试时候最常被问到的。今天我们就一起从0到1实现一个基于A+规范的Promise,过程中也会对Promise的异常处理,以及是否可手动终止做一些讨论,最后会对我们实现的Promise做单元测试。完整的代码已经上传到github,想直接看代码的可以点这里。虽然已经有很多带你实现Promise类的文章了,但每个人理解的程度不一样,也许不同的文章可以带给你不同的思考呢,那我们就开始吧。正文1. 基础框架new Promise()时接收一个executor函数作为参数,该函数会立即执行,函数中有两个参数,它们也是函数,分别是resolve和reject,函数同步执行一定要放在try…catch中,否则无法进行错误捕获。MyPromise.jsfunction MyPromise(executor) { function resolve(value) { } function reject(reason) { } try { executor(resolve, reject); } catch (reason) { reject(reason); }}module.exports = MyPromise;resolve()接收Promise成功值value,reject接收Promise失败原因reason。test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { resolve(123);})2. 添加状态机目前实现存在的问题:Promise是一个状态机的机制,初始状态为 pending,成功状态为 fulfilled,失败状态为 rejected。只能从 pending -> fulfilled,或者从 pending -> rejected,并且状态一旦转变,就永远不会再变了。所以,我们需要为Promise添加一个状态流转的机制。MyPromise.jsconst PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function MyPromise(executor) { let self = this; self.state = PENDING; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; } } try { executor(resolve, reject); } catch (reason) { reject(reason); }}module.exports = MyPromise;test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { resolve(123);});promise.then(function(value) { console.log(‘value’, value);}, function(reason) { console.log(‘reason’, reason);})3. 添加then方法Promise拥有一个then方法,接收两个函数 onFulfilled 和 onRejected,分别作为Promise成功和失败的回调。所以,在then方法中我们需要对状态state进行判断,如果是fulfilled,则执行onFulfilled(value)方法,如果是rejected,则执行onRejected(reason)方法。由于成功值value和失败原因reason是由用户在executor中通过resolve(value) 和 reject(reason)传入的,所以我们需要有一个全局的value和reason供后续方法获取。MyPromise.jsconst PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function MyPromise(executor) { let self = this; self.state = PENDING; self.value = null; self.reason = null; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; self.value = value; } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; self.reason = reason; } } try { executor(resolve, reject); } catch (reason) { reject(reason); }}MyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; if (self.state === FULFILLED) { onFuifilled(self.value); } if (self.state === REJECTED) { onRejected(self.reason); }};module.exports = MyPromise;4. 实现异步调用resolve目前实现存在的问题:同步调用resolve()没有问题,但如果是异步调用,比如放到setTimeout中,因为目前的代码在调用then()方法时,state仍是pending状态,当timer到时候调用resolve()把state修改为fulfilled状态,但是onFulfilled()函数已经没有时机调用了。针对上述问题,进行如下修改:MyPromise.jsconst PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function MyPromise(executor) { let self = this; self.state = PENDING; self.value = null; self.reason = null; self.onFulfilledCallbacks = []; self.onRejectedCallbacks = []; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; self.value = value; self.onFulfilledCallbacks.forEach(function(fulfilledCallback) { fulfilledCallback(); }); } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; self.reason = reason; self.onRejectedCallbacks.forEach(function(rejectedCallback) { rejectedCallback(); }); } } try { executor(resolve, reject); } catch (reason) { reject(reason); }}MyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; if (self.state === PENDING) { self.onFulfilledCallbacks.push(() => { onFuifilled(self.value); }); self.onRejectedCallbacks.push(() => { onRejected(self.reason); }); } if (self.state === FULFILLED) { onFuifilled(self.value); } if (self.state === REJECTED) { onRejected(self.reason); }};module.exports = MyPromise;我们添加了两个回调函数数组onFulfilledCallbacks和onRejectedCallbacks,用来存储then()方法中传入的成功和失败回调。然后,当用户调用resolve()或reject()的时候,修改state状态,并从相应的回调数组中依次取出回调函数执行。同时,通过这种方式我们也实现了可以注册多个then()函数,并且在成功或者失败时按照注册顺序依次执行。test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { setTimeout(function() { resolve(123); }, 1000);});promise.then(function(value) { console.log(‘value1’, value);}, function(reason) { console.log(‘reason1’, reason);});promise.then(function(value) { console.log(‘value2’, value);}, function(reason) { console.log(‘reason2’, reason);});5. then返回的仍是Promise读过PromiseA+规范的同学肯定知道,then()方法返回的仍是一个Promise,并且返回Promise的resolve的值是上一个Promise的onFulfilled()函数或onRejected()函数的返回值。如果在上一个Promise的then()方法回调函数的执行过程中发生了错误,那么会将其捕获到,并作为返回的Promise的onRejected函数的参数传入。比如:let promise = new Promise((resolve, reject) => { resolve(123);});promise.then((value) => { console.log(‘value1’, value); return 456;}).then((value) => { console.log(‘value2’, value);});let promise = new Promise((resolve, reject) => { resolve(123);});打印结果为:value1 123 value2 456let promise = new Promise((resolve, reject) => { resolve(123);});promise.then((value) => { console.log(‘value1’, value); a.b = 2; // 这里存在语法错误 return 456;}).then((value) => { console.log(‘value2’, value);}, (reason) => { console.log(‘reason2’, reason);});打印结果为:value1 123 reason2 ReferenceError: a is not defined可以看到,then()方法回调函数如果发生错误,会被捕获到,那么then()返回的Promise会自动变为onRejected,执行onRejected()回调函数。let promise = new Promise((resolve, reject) => { reject(123);});promise.then((value) => { console.log(‘value1’, value); return 456;}, (reason) => { console.log(‘reason1’, reason); return 456;}).then((value) => { console.log(‘value2’, value);}, (reason) => { console.log(‘reason2’, reason);});打印结果为:reason1 123 value2 456好啦,接下来我们就去实现then()方法依然返回一个Promise。MyPromise.jsMyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; let promise2 = null; promise2 = new MyPromise((resolve, reject) => { if (self.state === PENDING) { self.onFulfilledCallbacks.push(() => { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch(reason) { reject(reason); } }); self.onRejectedCallbacks.push(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch(reason) { reject(reason); } }); } if (self.state === FULFILLED) { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } } if (self.state === REJECTED) { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } } }); return promise2;};可以看到,我们新增了一个promise2作为then()方法的返回值。通过let x = onFuifilled(self.value) 或者 let x = onRejected(self.reason)拿到then()方法回调函数的返回值,然后调用self.resolvePromise(promise2, x, resolve, reject),将新增的promise2、x、promise2的resolve和reject传入到resolvePromise()中。所以,下面我们重点看一下resolvePromise()方法。MyPromise.jsMyPromise.prototype.resolvePromise = function(promise2, x, resolve, reject) { let self = this; let called = false; // called 防止多次调用 if (promise2 === x) { return reject(new TypeError(‘循环引用’)); } if (x !== null && (Object.prototype.toString.call(x) === ‘[object Object]’ || Object.prototype.toString.call(x) === ‘[object Function]’)) { // x是对象或者函数 try { let then = x.then; if (typeof then === ‘function’) { then.call(x, (y) => { // 别人的Promise的then方法可能设置了getter等,使用called防止多次调用then方法 if (called) return ; called = true; // 成功值y有可能还是promise或者是具有then方法等,再次resolvePromise,直到成功值为基本类型或者非thenable self.resolvePromise(promise2, y, resolve, reject); }, (reason) => { if (called) return ; called = true; reject(reason); }); } else { if (called) return ; called = true; resolve(x); } } catch (reason) { if (called) return ; called = true; reject(reason); } } else { // x是普通值,直接resolve resolve(x); }};resolvePromise()是用来解析then()回调函数中返回的仍是一个Promise,这个Promise有可能是我们自己的,有可能是别的库实现的,也有可能是一个具有then()方法的对象,所以这里靠resolvePromise()来实现统一处理。下面是翻译自PromiseA+规范关于resolvePromise()的要求:Promise 解决过程Promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来执行 promise 。这种 thenable 的特性使得 Promise 的实现更具有通用性:只要其暴露出一个遵循 Promise/A+ 协议的 then 方法即可;这同时也使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。运行 [[Resolve]](promise, x) 需遵循以下步骤:x 与 promise 相等如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promisex 为 Promise如果 x 为 Promise ,则使 promise 接受 x 的状态:- 如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝- 如果 x 处于执行态,用相同的值执行 promise- 如果 x 处于拒绝态,用相同的据因拒绝 promisex 为对象或函数如果 x 为对象或者函数:- 把 x.then 赋值给 then- 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise- 如果 then 是函数,将 x 作为函数的作用域 this 调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise: - 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y) - 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise - 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用 - 如果调用 then 方法抛出了异常 e: - 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之 - 否则以 e 为据因拒绝 promise - 如果 then 不是函数,以 x 为参数执行 promise- 如果 x 不为对象或者函数,以 x 为参数执行 promise如果一个 promise 被一个循环的 thenable 链中的对象解决,而 [[Resolve]](promise, thenable) 的递归性质又使得其被再次调用,根据上述的算法将会陷入无限递归之中。算法虽不强制要求,但也鼓励施者检测这样的递归是否存在,若检测到存在则以一个可识别的 TypeError 为据因来拒绝 promise。参考上述规范,结合代码中的注释,相信大家可以理解resolvePromise()的作用了。测试:test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { setTimeout(function() { resolve(123); }, 1000);});promise.then((value) => { console.log(‘value1’, value); return new MyPromise((resolve, reject) => { resolve(456); }).then((value) => { return new MyPromise((resolve, reject) => { resolve(789); }) });}, (reason) => { console.log(‘reason1’, reason);}).then((value) => { console.log(‘value2’, value);}, (reason) => { console.log(‘reason2’, reason);});打印结果:value1 123 value2 7896. 让then()方法的回调函数总是异步调用官方Promise实现的回调函数总是异步调用的:console.log(‘start’);let promise = new Promise((resolve, reject) => { console.log(‘step-’); resolve(123);});promise.then((value) => { console.log(‘step–’); console.log(‘value’, value);});console.log(’end’);打印结果:start step- end step– value1 123Promise属于微任务,这里我们为了方便用宏任务setTiemout来代替实现异步,具体关于宏任务、微任务以及Event Loop可以参考我的另一篇文章带你彻底弄懂Event Loop。MyPromise.jsMyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; let promise2 = null; promise2 = new MyPromise((resolve, reject) => { if (self.state === PENDING) { self.onFulfilledCallbacks.push(() => { setTimeout(() => { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); }); self.onRejectedCallbacks.push(() => { setTimeout(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); }); } if (self.state === FULFILLED) { setTimeout(() => { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); } if (self.state === REJECTED) { setTimeout(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); } }); return promise2;};测试:test.jslet MyPromise = require(’./MyPromise.js’);console.log(‘start’);let promise = new MyPromise((resolve, reject) => { console.log(‘step-’); setTimeout(() => { resolve(123); }, 1000);});promise.then((value) => { console.log(‘step–’); console.log(‘value’, value);});console.log(’end’);打印结果:start step- end step– value1 123经过以上步骤,一个最基本的Promise就已经实现完了,下面我们会实现一些不在PromiseA+规范的扩展方法。7. 实现catch()方法then()方法的onFulfilled和onRejected回调函数都不是必传项,如果不传,那么我们就无法接收reject(reason)中的错误,这时我们可以通过链式调用catch()方法用来接收错误。举例:let promise = new Promise((resolve, reject) => { reject(‘has error’);});promise.then((value) => { console.log(‘value’, value);}).catch((reason) => { console.log(‘reason’, reason);});打印结果:reason has error不仅如此,catch()可以作为Promise链式调用的最后一步,前面Promise发生的错误会冒泡到最后一个catch()中,从而捕获异常。举例:let promise = new Promise((resolve, reject) => { resolve(123);});promise.then((value) => { console.log(‘value’, value); return new Promise((resolve, reject) => { reject(‘has error1’); });}).then((value) => { console.log(‘value’, value); return new Promise((resolve, reject) => { reject(‘has error2’); });}).catch((reason) => { console.log(‘reason’, reason);});打印结果:reason has error reason has error1那么catch()方法到底是如何实现的呢?答案就是在Promise的实现中,onFulfilled和onRejected函数是有默认值的:MyPromise.jsMyPromise.prototype.then = function(onFuifilled, onRejected) { onFuifilled = typeof onFuifilled === ‘function’ ? onFuifilled : value => {return value;}; onRejected = typeof onRejected === ‘function’ ? onRejected : reason => {throw reason};};MyPromise.prototype.catch = function(onRejected) { return this.then(null, onRejected);};可以看到,onRejected的默认值是把错误reason通过throw抛出去。由于我们对于同步代码的执行都是在try…catch中的,所以如果Promise发生了错误,如果没传onRejected,默认的函数会把错误reason抛出,然后会被promise2捕捉到,作为reject(reason)决议。catch()实现就是调用this.then(null, onRejected),由于promise2被reject,所以会执行onRejected回调,于是就捕捉到了第一个promise的错误。总结来说,then()方法中不传onRejected回调,Promise内部会默认帮你写一个函数作为回调,作用就是throw抛出reject或者try…catch到的错误,然后错误reason会被promise2作为reject(reason)进行决议,于是会被下一个then()方法的onRejected回调函数调用,而catch只是写了一个特殊的then(null, onRejected)而已。所以,我们在写Promise的链式调用的时候,在then()中可以不传onRejected回调,只需要在链式调用的最末尾加一个catch()就可以了,这样在该链条中的Promise发生的错误都会被最后的catch捕获到。举例1:let promise = new Promise((resolve, reject) => { reject(123);});promise.then((value) => { // 注意,不会走这里,因为第一个promise是被reject的 console.log(‘value1’, value); return new Promise((resolve, reject) => { reject(‘has error1’); });}).then((value) => { console.log(‘value2’, value); return new Promise((resolve, reject) => { reject(‘has error2’); });}, (reason) => { // 注意,这个then有onRejected回调 console.log(‘reason2’, reason);}).catch((reason) => { // 错误在上一个then就被捕获了,所以不会走到这里 console.log(‘reason3’, reason);});打印结果:reason2 123举例2:let promise = new Promise((resolve, reject) => { reject(123);});promise.then((value) => { console.log(‘value1’, value); return new Promise((resolve, reject) => { reject(‘has error1’); });}).then((value) => { console.log(‘value2’, value); return new Promise((resolve, reject) => { reject(‘has error2’); });}).catch((reason) => { // 由于链条中的then都没有onRejected回调,所以会一直被冒泡到最后的catch这里 console.log(‘reason3’, reason);});catch和then一样都是返回一个新的Promise。有的同学可能会有疑问,如果catch中的回调执行也发生错误该怎么办呢,这个我们后续在Promise异常处理中再做讨论。打印结果:reason3 1238. 实现finally方法finally是某些库对Promise实现的一个扩展方法,无论是resolve还是reject,都会走finally方法。MyPromise.jsMyPromise.prototype.finally = function(fn) { return this.then(value => { fn(); return value; }, reason => { fn(); throw reason; });};9. 实现done方法done方法作为Promise链式调用的最后一步,用来向全局抛出没有被Promise内部捕获的错误,并且不再返回一个Promise。一般用来结束一个Promise链。MyPromise.jsMyPromise.prototype.done = function() { this.catch(reason => { console.log(‘done’, reason); throw reason; });};10. 实现Promise.all方法Promise.all()接收一个包含多个Promise的数组,当所有Promise均为fulfilled状态时,返回一个结果数组,数组中结果的顺序和传入的Promise顺序一一对应。如果有一个Promise为rejected状态,则整个Promise.all为rejected。MyPromise.jsMyPromise.all = function(promiseArr) { return new MyPromise((resolve, reject) => { let result = []; promiseArr.forEach((promise, index) => { promise.then((value) => { result[index] = value; if (result.length === promiseArr.length) { resolve(result); } }, reject); }); });};test.jslet MyPromise = require(’./MyPromise.js’);let promise1 = new MyPromise((resolve, reject) => { console.log(‘aaaa’); setTimeout(() => { resolve(1111); console.log(1111); }, 1000);});let promise2 = new MyPromise((resolve, reject) => { console.log(‘bbbb’); setTimeout(() => { reject(2222); console.log(2222); }, 2000);});let promise3 = new MyPromise((resolve, reject) => { console.log(‘cccc’); setTimeout(() => { resolve(3333); console.log(3333); }, 3000);});Promise.all([promise1, promise2, promise3]).then((value) => { console.log(‘all value’, value);}, (reason) => { console.log(‘all reason’, reason);})打印结果:aaaa bbbb cccc 1111 2222 all reason 2222 333311. 实现Promise.reace方法Promise.race()接收一个包含多个Promise的数组,当有一个Promise为fulfilled状态时,整个大的Promise为onfulfilled,并执行onFulfilled回调函数。如果有一个Promise为rejected状态,则整个Promise.race为rejected。MyPromise.jsMyPromise.race = function(promiseArr) { return new MyPromise((resolve, reject) => { promiseArr.forEach(promise => { promise.then((value) => { resolve(value); }, reject); }); });};test.jslet MyPromise = require(’./MyPromise.js’);let promise1 = new MyPromise((resolve, reject) => { console.log(‘aaaa’); setTimeout(() => { resolve(1111); console.log(1111); }, 1000);});let promise2 = new MyPromise((resolve, reject) => { console.log(‘bbbb’); setTimeout(() => { reject(2222); console.log(2222); }, 2000);});let promise3 = new MyPromise((resolve, reject) => { console.log(‘cccc’); setTimeout(() => { resolve(3333); console.log(3333); }, 3000);});Promise.all([promise1, promise2, promise3]).then((value) => { console.log(‘all value’, value);}, (reason) => { console.log(‘all reason’, reason);})打印结果:aaaa bbbb cccc 1111 all reason 1111 2222 333312. 实现Promise.resolve方法Promise.resolve用来生成一个fulfilled完成态的Promise,一般放在整个Promise链的开头,用来开始一个Promise链。MyPromise.jsMyPromise.resolve = function(value) { let promise; promise = new MyPromise((resolve, reject) => { this.prototype.resolvePromise(promise, value, resolve, reject); }); return promise;};test.jslet MyPromise = require(’./MyPromise.js’);MyPromise.resolve(1111).then((value) => { console.log(‘value1’, value); return new MyPromise((resolve, reject) => { resolve(2222); })}).then((value) => { console.log(‘value2’, value);})打印结果:value1 1111 value2 2222由于传入的value有可能是普通值,有可能是thenable,也有可能是另一个Promise,所以调用resolvePromise进行解析。12. 实现Promise.reject方法Promise.reject用来生成一个rejected失败态的Promise。MyPromise.jsMyPromise.reject = function(reason) { return new MyPromise((resolve, reject) => { reject(reason); });};test.jslet MyPromise = require(’./MyPromise.js’);MyPromise.reject(1111).then((value) => { console.log(‘value1’, value); return new MyPromise((resolve, reject) => { resolve(2222); })}).then((value) => { console.log(‘value2’, value);}).catch(reason => { console.log(‘reason’, reason);});打印结果:reason 111113. 实现Promise.deferred方法Promise.deferred可以用来延迟执行resolve和reject。MyPromise.jsMyPromise.deferred = function() { let dfd = {}; dfd.promies = new MyPromise((resolve, reject) => { dfd.resolve = resolve; dfd.rfeject = reject; }); return dfd;};这样,你就可以在外部通过调用dfd.resolve()和dfd.reject()来决议该Promise。13. 如何停止一个Promise链假设这样一个场景,我们有一个很长的Promise链式调用,这些Promise是依次依赖的关系,如果链条中的某个Promise出错了,就不需要再向下执行了,默认情况下,我们是无法实现这个需求的,因为Promise无论是then还是catch都会返回一个Promise,都会继续向下执行then或catch。举例:new Promise(function(resolve, reject) { resolve(1111)}).then(function(value) { // “ERROR!!!”}).catch() .then() .then() .catch() .then()有没有办法让这个链式调用在ERROR!!!的后面就停掉,完全不去执行链式调用后面所有回调函数呢?我们自己封装一个Promise.stop方法。MyPromise.jsMyPromise.stop = function() { return new Promise(function() {});};stop中返回一个永远不执行resolve或者reject的Promise,那么这个Promise永远处于pending状态,所以永远也不会向下执行then或catch了。这样我们就停止了一个Promise链。new MyPromise(function(resolve, reject) { resolve(1111)}).then(function(value) { // “ERROR!!!” MyPromise.stop();}).catch() .then() .then() .catch() .then()但是这样会有一个缺点,就是链式调用后面的所有回调函数都无法被垃圾回收器回收。14. 如何解决Promise链上返回的最后一个Promise出现错误看如下例子:new Promise(function(resolve) { resolve(42)}).then(function(value) { a.b = 2;});这里a不存在,所以给a.b赋值是一个语法错误,onFulfilled回调函数是包在try…catch中执行的,错误会被catch到,但是由于后面没有then或catch了,这个错误无法被处理,就会被Promise吃掉,没有任何异常,这就是常说的Promise有可能会吃掉错误。那么我们怎么处理这种情况呢?方法一就是我们前面已经实现过的done()。new Promise(function(resolve) { resolve(42)}).then(function(value) { a.b = 2;}).done();done()方法相当于一个catch,但是却不再返回Promise了,注意done()方法中不能出现语法错误,否则又无法捕获了。方法二普通错误监听window的error事件可以实现捕获window.addEventListener(’error’, error => { console.log(error); // 不会触发});Promise没有被onRejected()处理的错误需要监听unhandledrejection事件window.addEventListener(‘unhandledrejection’, error => { console.log(‘unhandledrejection’, error); // 可以触发,而且还可以直接拿到 promise 对象});14. 单元测试结束相关单元测试以及完整代码可以到我的github查看,如果对你有帮助的话,就来个star吧~参考文档PromiseA+规范 ...

September 29, 2018 · 8 min · jiezi