NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

“小众”之美——Ruby 在 QA 自动化中的应用

  • 2020-02-27
  • 本文字数:8305 字

    阅读完需:约 27 分钟

“小众”之美——Ruby在QA自动化中的应用

前言

关于测试领域的自动化,已有很多的文章做过介绍,“黑科技”也比比皆是,如通过 Java 字节码技术实现接口的录制,Fiddler 录制内容转 Python 脚本,App 中的插桩调试等,可见角度不同,对最佳实践的理解也不一样。这里想要阐述的是,外卖(上海)QA 团队应用相对“小众”的 Ruby,在资源有限的条件下实现自动化测试的一些实践与经验分享。

背景

加入外卖上海团队时,共 2 名 QA 同学,分别负责 App 与 M 站的功能测试,自动化测试停留在学习北京侧接口测试框架的阶段,实效上近乎为 0,能力结构上在代码这部分是明显薄弱的。而摆在面前的问题是,回归测试的工作量较大,特别是 M 站渠道众多(4 个渠道),移动端 API 的接口测试需区分多个版本,自动化测试的开展势在必行。在这样的条件下,如何快速且有效地搭建并推广自动化测试体系?在过去对自动化测试的多种尝试及实践的总结后,选择了 Ruby。

Why Ruby?

简单点说就是:并不聪明的大脑加上“好逸恶劳”的思想,促使我在这些年的自动化测试实践中,不断寻找更合适的解决方案。所谓技术,其本质都是站在别人的肩膀上,肩膀的高度也决定了实现目标的快慢,而 Ruby 正符合所需的一些特征:


  • 效率。自身应该算是“纯粹”的测试人员,在“测试开发”这重职业并不普及的年代,一直希望有种语言可以让测试的开发效率超过研发,Ruby 做到了;人性化的语法,各种糖。类似 1.day.ago,简单的表达不需要解释;

  • 强大的元编程能力。基于此,DHH 放弃了 PHP 而使用 Ruby 开发出了 Rails,DSL 也因此成为 Ruby 开发的框架中非常普通的特性,而这对于很多主流语言都是种奢望;

  • 对于测试来说足够充足的社区资源。不涉及科学计算,不涉及服务开发,在没有这些需求的情况下,Python 和 Java 不再是必需。


脱离了开发语言的平台,但在不关注白盒测试的情况下并无太多不妥。当 Ruby 用于测试开发,基本“屏蔽”了性能上的劣势,充分展现了敏捷、易用的特点,也是选择这一技术路线的主要因素。

接口自动化框架 Coral-API

框架思路

接口自动化测试方案众多,个人认为它们都有自己的适用的范围和优缺点。UI 类工具虽轻松实现无码 Case,但在处理接口变动和全链路接口流程上多少会显得有些繁琐(尤其在支持数据驱动需求下),过多的规则、变量设置和编码也相差无几;录制类型的方案,更多还是适合回归,对于较全面的接口测试也需要一定的开发量。基于这些权衡考虑,采用一种编码尽可能少、应用面更广的接口自动化框架实现方式,把它命名为 Coral-API,主要有以下特点:


  1. 测试数据处理独立

  2. 预先生成测试所需的最终数据,区分单接口测试数据(单接口数据驱动测试)与链路测试数据

  3. 通过命令行形式的语句解决了参数的多层嵌套及动态数据生成的问题

  4. Excel 中维护测试数据,最终转化为 YML 或存入 DB,折中解决了 JSON 形式的数据难维护问题

  5. 学习成本低

  6. 框架提供生成通用结构代码的功能,使测试人员更关注于业务逻辑处理

  7. DSL 的书写风格,即便没有 Ruby 的语言基础,也可以较快掌握基本的接口测试用例编写

  8. 扩展性

  9. 支持 Java 平台的扩展

  10. 支持 HTTP/RPC 接口,可根据开发框架扩展

  11. 框架基于 Rspec,支持多种验证方式(Build-In Matcher),及支持自定义 Matcher,目前实现了 JSON 去噪的 Diff,各种复合的条件比较


以单个接口测试编写为例,下图描述了具体流程:



coral-api 框架


从图中可以看到,安装了 Coral-API 的 gem 后,可通过命令行 “coral g {apiname}” ,通过模板来生成测试数据 XLS 及对应的数据处理文件(例如 ApiOne.rb 文件),修改并执行 ApiOne.rb 文件,则可以生成最终的测试数据(YML 文件)及测试类和 Case 文件。如果开发框架支持(有途径可解析出参数),则可以通过脚本直接生成整个服务下所有接口的测试代码,实现自动化 Case 的同步开发。这种处理过程主要是一并解决了以下几个问题:


  1. 复杂结构的测试数据构造

  2. 动态参数的赋值

  3. 测试数据的维护

  4. 测试数据的加载


假设有以下这样一个接口请求格式,包含一个 orderInfo 的子节点,及 payInfo 的 list,还需要解决一些变化值的问题,如各种 id 和 time(暂且称为动态字段)。一般框架中会以 JSON 格式来作为测试用例的请求格式,在代码中按变量处理动态字段值。JSON 作为请求数据的保存形式,存在一个很大的问题,就是后期维护,尤其是 Case 数量较多的时候。因此,考虑仍以 Excel 为数据维护的初始形式(使用上更直观),通过 Sheet 的嵌套来处理复杂结构,也便于后期接口参数变动后的 Case 维护。


userId: E000001requestId: '1938670097'orderInfo: orderId: '6778043386' count: '2' name: testgoodspayInfo:- transactionId: '510455433082284' payTime: '2017-04-04 13:03:34' payType: BOC- transactionId: '167338836018587' payTime: '2017-04-04 13:03:34' payType: WalletcreateTime: '2017-04-04 13:03:34'
复制代码


测试数据的 Excel 做如下设计,Main 中为第一层参数结构,预期响应另分一个 Sheet,子节点和 list 节点的内容写在对应的 Sheet 中,动态值均置为空,在接口数据类中处理,orderInfo 节点和 payInfo 节点均另写在新的 Sheet 中,用于单接口数据驱动的 Case 与链路回归用 Case 分开,当然这会增加一些 Case 维护的成本,可以选择是否区分。



示例的数据结构,通过以下语句即可实现,如果需要为后续接口测试提供前置步骤的数据,也可以同步实现,下例中为后续接口生成了 5 条请求数据。针对接口参数变动的情况,可以修改 Excel 和数据处理类文件,执行一遍即可,也提供了批量重新生成所有接口数据的脚本。


class Demo < ApiCaseBase
update self.request,:requestId=>'gen_randcode(10)',:createTime=>'get_datetime' add_node self.request,"orderInfo",:orderId=>'gen_randcode(10)' add_list self.request,"payInfo",:transactionId=>'gen_randcode(15)',:payTime=>'get_datetime' sheetData={'ForApiOther'=>5} generate_data self,sheetData do update_force @data,:orderId=>'gen_randcode(10)',:createTime=>'get_datetime' add_node_force @data,"orderInfo",:orderId=>'gen_randcode(10)' add_list_force @data,"payInfo",:transactionId=>'gen_randcode(15)',:payTime=>'get_datetime' endend
复制代码


Excel 作为 Case 的维护形式,缺点是 Case 较多情况下频繁读取比较影响时间。在这种情况下,考虑到把数据序列化到 YML 中,启动执行时接口测试类自动与测试数据进行绑定。在 Case 中可以直接使用形如 DemoTest.request[1]的请求数据,提高了速度,结构上也清晰了不少。


接口测试类文件(HTTP 接口调用为例)生成的模板如下,修改对应的接口信息即可,支持 DB 验证(代码块 p 这部分是目前唯一需要写 Ruby 代码的地方,当然这是非必需项)。


require 'apicasebase' class PreviewTest   include ApiTestBase   set_cookie   set_domain "Domain_takeaway"   set_port 80   set_path "/waimai/ajax/wxwallet/Preview"   set_method "get"   set_sql "select * from table"   p = proc do |dbres|    ## do something    ## return a hash  end   set_p p
end
复制代码


TestCase 文件如下,原则上无需修改,只需要在测试数据的 Excel 中编写匹配规则及预期输出,基本上实现了单个接口无编码的数据驱动测试。


require 'Preview_validate' RSpec.shared_examples "Preview Example" do |key,requestData,expData|     it 'CaseNo'+ key.to_s + ': '+expData['memo'] do       response = PreviewTest.response_of(key)            expect(response).to eval("#{expData['matcher']} '#{expData['expection']}'")     endend RSpec.describe "Preview接口测试",:project=>'api_m_auto',:author=>'Neil' do  PreviewTest.request.each{|key,parameter|include_examples "Preview Example",key,PreviewTest.request[key],PreviewTest.expect[key]}end
复制代码


接口流程 Case 编写就是各独立接口的业务逻辑串联,重点是 Case 的组织,把一些公用的 Steps 独立出 shared_examples,在主流程的 Case 中 include 这些 shared_examples 即可,关联的上下游参数


通过全局变量来传递。


RSpec.describe "业务流程测试" ,:project=>'api_m_auto',:author =>'Neil' do  let(:wm_b_client) { WmBClient.new('自配') }    before(:context) do    init_step  end    context "在线支付->商家接单->确认收货->评价" do    include_examples "OrderAndPay Example",1    include_examples "AcceptOrder Example"    include_examples "CommentStep Example"  end  end
复制代码


通过上面的介绍,可以看到,Case 的编写大部分可以通过代码生成实现(熟悉以后部分接口也可以根据需要进行操作步骤的取舍,如直接编写 YML)。实践下来的情况是,从各方面一无所有,17 个人日左右的时间,完成了 M 站 API 层接口自动化(业务流程 9 个,单个接口 10 个)及点评外卖移动端 API 的接口自动化(业务流程 9 个,单个接口 20 个),实现了外卖业务全链路接口回归,平均每个业务流 Case 步骤 9 个左右。期间也培养了一名之前未接触过 Ruby 的同学,在完成了第一版开发后,两名初级阶段的同学逐步承担起了框架的改进工作,实现了更多有效的验证 Matcher,并支持了移动端 API 多版本的测试。之后的回归测试不仅时间上缩减了 50%以上,也通过接口自动化 3 次发现了问题,其中一次 API 不同版本导致的 Bug 充分体现了自动化测试的效率。通过 ci_reporter,可以方便地将 Rspec 的报告格式转为 JUnit 的 XML 格式,在 Jenkins 中做对应的展示。



测试报告 jenkins 展示

解决接口多版本测试的例子

移动端 API 自动化中存在的问题就是,一个接口会存在多个版本并存的情况,有 header 中内容不同的,或 formdata 内容不同的情况,在接口回归中必须都要照顾到,在 Coral-API 中我们采用以下方式进行处理。


在 config.yml 中定义各版本的 header。


Domain_takeaway_header:        v926: '{"connection":"upgrade","x-forwarded-for":"172.24.121.32, 203.76.219.234","mkunionid":"-113876624192351423","pragma-apptype":"com.dianping.ba.dpscope","mktunneltype":"tcp","pragma-dpid":"-113876624192351423","pragma-token":"e7c10bf505535bfddeba94f5c050550adbd9855686816f58f0b5ca08eed6acc6","user-agent":"MApi 1.1 (dpscope 9.4.0 appstore; iPhone 10.0.1 iPhone9,1; a0d0)","pragma-device":"598f7d44120d0bf9eb7cf1d9774d3ac43faed266","pragma-os":"MApi 1.1 (dpscope 9.2.6 appstore; iPhone 10.0.1 iPhone9,1; a0d0)","mkscheme":"https","x-forwarded-for-port":"60779","X-CAT-TRACE-MODE":"true","network-type":"wifi","x-real-ip":"203.76.219.234","pragma-newtoken":"e7c10bf505535bfddeba94f5c050550adbd9855686816f58f0b5ca08eed6acc6","pragma-appid":"351091731","mkoriginhost":"mobile.dianping.com","pragma-unionid":"91d9c0e21aca4170bf97ab897e5151ae0000000000040786871"}'     v930: '{"connection":"upgrade","x-forwarded-for":"172.24.121.32, 203.76.219.234","mkunionid":"-113876624192351423","pragma-apptype":"com.dianping.ba.dpscope","mktunneltype":"tcp","pragma-dpid":"-113876624192351423","pragma-token":"e7c10bf505535bfddeba94f5c050550adbd9855686816f58f0b5ca08eed6acc6","user-agent":"MApi 1.1 (dpscope 9.4.0 appstore; iPhone 10.0.1 iPhone9,1; a0d0)","pragma-device":"598f7d44120d0bf9eb7cf1d9774d3ac43faed266","pragma-os":"MApi 1.1 (dpscope 9.3.0 appstore; iPhone 10.0.1 iPhone9,1; a0d0)","mkscheme":"https","x-forwarded-for-port":"60779","X-CAT-TRACE-MODE":"true","network-type":"wifi","x-real-ip":"203.76.219.234","pragma-newtoken":"e7c10bf505535bfddeba94f5c050550adbd9855686816f58f0b5ca08eed6acc6","pragma-appid":"351091731","mkoriginhost":"mobile.dianping.com","pragma-unionid":"91d9c0e21aca4170bf97ab897e5151ae0000000000040786871"}'    ......
复制代码


在接口测试类被加载时会进行全局变量赋值,同时替换 header 里对应节点的 token,测试数据 YML 文件中则做这样的描述,每条数据的 header 则较方便地被替换。


---Main:  1: &DEFAULT    headers: '<%= $v926 %>'    host: mobile.51ping.com    port: '80'    path: "/deliveryaddresslist.ta"    search: "?geotype=2&actuallat=31.217329&actuallng=121.415603&initiallat=31.22167778439444&initiallng=121.42671951083571"    method: GET    query: '{"geotype":"2","actuallat":"31.217329","actuallng":"121.415603","initiallat":"31.22167778439444","initiallng":"121.42671951083571"}'    formData: "{}"    scheme: 'http:'  2:    <<: *DEFAULT    headers: '<%= $v930 %>'  3:    <<: *DEFAULT    headers: '<%= $v940 %>'  4:    <<: *DEFAULT    headers: '<%= $v950 %>'  5:    <<: *DEFAULT    headers: '<%= $v990 %>'
复制代码

解决 RPC 接口测试

HTTP 接口的测试框架选择面还是比较多的,RPC 调用的框架如何测试呢?答案就是 JRuby + Java 的反射调用,在 Pigeon 接口中我们已经试点了这种方式,证明是可行的,针对不同的 RPC 框架实现不同的 Adapter(Jar 文件),Coral-API 传参(JSON 格式)给 Adapter,Adapter 通过解析参数进行反射调用,这样对于框架来说无需改动,只需对部分文件模板稍作调整,也无需在 Ruby 中混写 Java 代码,实现了最少的代码量—2 行。



rpc 调用

UI 自动化框架 Coral-APP

框架思想

App 的 UI 自动化,Ruby 的简便性更明显,尤其 Appium 提供了对 Ruby 良好的支持,各种 UI 框架的优劣就不在此赘述了。综合比较了 Appium 与 Calabash 后,选择了前者,测试框架选用了更适合业务流描述的 Cucumber,沿用了以前在 Web 自动化中使用的对象库概念,将页面元素存储在 CSV 中,包括了 Android 与 iOS 的页面对象描述,满足不同系统平台的测试需要。在针对微信 M 站的 UI 自动化方案中,还需解决微信 WebView 的切换,及多窗口的切换问题,appium_lib 都提供了较好的支持,下面介绍下结合了 Appium 及 Cucumber 的自动化框架 Coral-APP。


框架结构如下图:



coral-app


step_definitions 目录下为步骤实现,public_step.rb 定义了一些公共步骤,比如微信测试需要用到的上下文切换,Webview 里的页面切换功能,也可以通过 support 目录下的 global_method.rb 里新增的 Kernel 中的方法来实现。


support/native 目录下为 app 测试的配置文件,support/web 目录下为 h5 测试的配置文件。


support/env.rb 为启动文件,主要步骤如下:


$caps = Appium.load_appium_txt file: File.expand_path('../app/appium.txt', __FILE__), verbose: true $caps[:caps].store("chromeOptions",{"androidProcess":"com.tencent.mm:tools"}) $driver = Appium::Driver.new($caps,true) Elements.generate_all_objects Before{$driver.start_driver}
After{$driver.quit_driver}
复制代码


support/elements 下为对象库 CSV 文件,内容如下图:



对象库文件


support/elements.rb 为对象库实现,将 CSV 中的描述转换为 Elements 模块中对象的功能,这样在 Page 中就可以直接使用类似“Elements.微信我” 这样的对象描述了。


......  def self.define_ui_object(element)  case $caps[:caps][:platformName].downcase    when "android"      idempotently_define_singleton_method(element["OBJNAME"]){$driver.find_element(:"#{element["ATTRIBUTE"]}","#{element["ANDROID_IDENTITY"]}")}    else      idempotently_define_singleton_method(element["OBJNAME"]){$driver.find_element(:"#{element["ATTRIBUTE"]}","#{element["IOS_IDENTITY"]}")}  endend ......
复制代码


support/pages 为 Page 层,实现了每个页面下的操作,目前把它实现为 Kernel 中的方法,采用中文命名,便于阅读使用。


module Kernel  def 点击我    Elements.微信我.click  end   def 点击收藏按钮    Elements.微信收藏.click  end   def 点击收藏项    Elements.微信收藏链接.click  end   def 点击收藏中的美团外卖链接    Elements.微信收藏链接URL.click  endend
复制代码


step 里的步骤我们可以这样写,封装好足够的公共步骤或方法,Case 的编写就是这么简单。


When /^进入美团外卖M站首页$/ do   点击我   点击收藏按钮   点击收藏项   点击收藏中的美团外卖链接   等待 5   step "切换到微信Webview"   等待 15   step "切换到美团外卖window" end
复制代码


最终 Feature 内容如下:


Feature: 回归下单主流程  打开微信->进入首页->定位->进入自动化商户->下单->支付->订单详情  Scenario:    When 进入美团外卖M站首页
复制代码


相对于其他的 UI 测试框架,使用接近自然语言的描述,提高了 Case 可读性,编写上也没有其他框架那么复杂。当然 UI 自动化中还是有一些小难点的,尤其是 Hybrid 应用,Appium 目前还存在些对使用影响不大的 Bug,在框架试用完成的情况下,将在微信入口体验优化项目结束后的进一步使用中去总结与完善。

质量工作的自动化

都知道在美团点评,QA 还担负着质量控制的工作,当功能+自动化+性能+其他测试工作于一身,而且是 1:8 的测试开发比下,如何去关注质量的改进?答案只有:工具化、自动化。开发这样一个小系统,技术方案选择上考虑主要是效率和学习成本,符合敏捷开发的特点,基于这些因素,应用了被称为“Web 开发的最佳实践”的 Rails 框架。


Rails 的设计有些颠覆传统的编程理念,CRUD 的实现上不用说了,一行命令即可,数据库层的操作,通过 migration 搞定,在 Mail,Job 等功能的实现上也非常方便,框架都有对应的模块,并且提供了大量的组件,Session、Cookie、安全密码、邮件地址校验都有对应的 gem,感觉不像是在写代码,更像是在配置项目,不知不觉,一个系统雏形就完成了,整理了下项目中使用到的 gem,主要有以下这些。


前端相关:


  1. bootstrap-sass Bootstrap 框架

  2. jquery-rails jQuery 框架

  3. simple_form 优化的 form 组件

  4. chartkick 堪称一行代码即可的图表组件

  5. hightchart 图表组件


后端相关:


  1. validates_email_format_of 邮件地址校验

  2. has_secure_password 安全密码组件

  3. mysql2 MySQL 连接组件

  4. cancancan 权限管理组件

  5. sidekiq 队列中间件

  6. sidekiq-cron 定时 Job 组件

  7. rest-client Http And Rest Client For Ruby

  8. will_paginate 分页组件


从搭建开发环境、写 Demo,自己做产品、开发、测试、搭建生产环境、部署,边参阅文档边实现,总共 18 个人日左右,实现了平台基础功能、线上故障问题的管理及通知、测试报告的管理及通知、Sonar 数据的抽取(Job 及邮件)、Bug 数据的抽取(Job)、自动化测试项目的接入、质量数据的 Dashboard 各类数据图表展示等功能,以下为系统功能的两个示例:


后台管理界面



shwmqp manager


线下缺陷周趋势



shwmqp manager


应用 Rails,团队较快进入了可以通过数据进行质量分析的初级阶段,当然还有很长的路要走,在从 0 到 1 的这个过程中,还是较多地体会到了敏捷开发的特性,也充分感受到了 DRY 理念。

总结

以上为半年左右时间内,外卖上海 QA 团队在自动化工作上的一些实践,总的来说,达到一定预期效果,整理这篇文章分享一些心得。所谓的主流与小众并非绝对,主要从几个方面衡量:


  1. 应用领域。Ruby 因为性能问题,始终不太主流,但并不意味着它一无是处,用在测试领域,开发效率、DSL 的友好性、语言的粘合性、使用者的学习低成本,都能发挥很大的优势。

  2. 使用群体。不同的使用群体对于技能掌握的要求也是不同的,能达到同样效果甚至超过预期则就可以选择哪怕“小众”的方案。

  3. 环境背景。其实有很多初创公司选择 Ruby 作为初期的技术栈有一定的道理,而这与我们当初的情景有相似之处,实际效果也体现了语言的特性。


当然应用“小众”技术,必然要面对不少挑战:如何迅速培养能掌握相关技术的同学,与其他语言平台的衔接问题,面对团队的质疑等。尤其 Ruby 属于易学难精的那种,从脚本语言应用层次上升到动态语言设计层次还是需要一定的学习曲线的,也就是说对于使用者来说是简单的,对于设计者的能力要求较高,就像流传的 Ruby 程序员的进阶过程就是魔法师的养成史。


正因为有特色的技术,才值得去研究和学习,就像它的设计者所说,目的就是为了让开发人员觉得编程是件快乐的事情。做了这么些年的测试,还能够不停止写代码的脚步,也是因为几年前开始接触 Ruby。不论将来是否成为主流,它仍然是测试领域工具语言的不错选择,不管以后会出现什么样的技术,选型的标准也不会改变。技术的世界没有主流与小众,只有理解正确与否,应用得当与否。


2020-02-27 11:14532

评论

发布
暂无评论
发现更多内容

央行数字货币完成闭环测试 亮剑在即

CECBC

万吨黄金做后盾!央行数字货币正式拉开序幕

CECBC

架构实战营-模块六作业

无名

架构实战营 「架构实战营」

模块6作业

Asha

MySql优化:一条SQL语句的执行过程是怎样的?

秋水

MySQL性能优化 内容合集 签约计划第二季

2022北京展会专题

InfoQ_caf7dbb9aa8a

<<活法>>读后感

Tiger

28天写作

【入门教程直播第三期】如何完成一次高质量的 OceanBase 数据迁移

OceanBase 数据库

数据库 学习 开源 课程 oceanbase

在自己的服务器上部署hexo博客

为自己带盐

Hexo 28天写作 12月日更

注册了一个新域名(10/28)

赵新龙

28天写作

2022年企业采购多云管理软件就选行云管家!

行云管家

云计算 公有云 混合云 多云 云管理

数据库大赛50强之「中国人民大学」:培养工程性精英人才,共筑数据库美好未来

OceanBase 数据库

开源 新闻动态 oceanbase OceanBase 数据库大赛

2022北京-大数据-博览会

InfoQ_caf7dbb9aa8a

区块链技术何以成为中国数字化转型最新驱动力?

CECBC

netty系列之:一个价值上亿的网站速度优化方案

程序那些事

Java Netty nio 程序那些事 12月日更

Linux一学就会之文件系统结构-硬链接和软链接

学神来啦

Linux 运维 链接 linux云计算 硬盘

前端开发之Vue 技术栈编写表单组件

@零度

Vue 前端开发

大数据开发之传输组件Sqoop的功能介绍

@零度

大数据 sqoop

站在你领导的角度考虑问题

张老蔫

28天写作

30个类手写Spring核心原理之Ioc顶层架构设计(2)

Tom弹架构

Java spring 源码

2022世界物联网AIOTE博览会-北京开幕

InfoQ_caf7dbb9aa8a

2022北京-AI人工智能-主题展

InfoQ_caf7dbb9aa8a

2022第十五届北京国际物联网展览会

InfoQ_caf7dbb9aa8a

北京行动计划

Kyligence 智能数据服务与管理相关研究

Kyligence

技术专题合集

青岛等保测评机构有几家?咨询电话多少?在哪里?

行云管家

网络安全 等保 等级保护 等保测评 等保2.0

智慧园区系统建设助力智慧城市信息化建设

a13823115807

智慧城市 智慧园区 智慧园区系统平台建设

OceanBase 源码解读(七):一文读懂数据库索引实现原理

OceanBase 数据库

开源 oceanbase 技术解析

浅析流媒体CDN与WEB CDN的业务差异

郑州埃文科技

CDN加速 IP网络 在线并发

🏆【Alibaba中间件技术系列】「RocketMQ技术专题」Broker配置介绍及发送流程、异常(XX Busy)问题分析

洛神灬殇

RocketMQ 消息队列 Apache RocketMQ 12月日更 System Busy/Broker busy

Camtasia有哪些基本功能

淋雨

Camtasia 录屏软件

Linux常用命令速查手册

入门小站

Linux

“小众”之美——Ruby在QA自动化中的应用_文化 & 方法_美团技术团队_InfoQ精选文章