发布在即!企业 AIGC 应用程度测评,3 步定制专属评估报告。抢首批测评权益>>> 了解详情
写点什么

“小众”之美——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:14521

评论

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

小程序开发实战案例之三 | 小程序底部导航栏如何设置

盐焗代码虾

前端 支付宝 小程序开发 导航栏

鸿蒙原生应用开发——分布式数据对象

HarmonyOS开发者

HarmonyOS

如何将自己的项目打包成whl文件分享

IT蜗壳-Tango

2023 总结对AI的总结和展望

i查拉图斯特拉如是说

AI 大模型

17 | 跳表:为什么Redis一定要用跳表来实现有序集合

鲁米

[译]你应该知道的多个HTML属性

南城FE

html html5 前端

喜报!乘云数字获浙江省“专精特新”企业认定

乘云 DataBuff

OpenTiny Vue 3.12.0 发布:文档大优化!增加水印和二维码两个新组件🎈

Kagol

写作训练营打卡2--工作后最有成就感的事

Avril

最全最详细的Java异常处理机制

是月月啊2023

Java 面试题

极狐GitLab 和 ArgoCD 集成实现 GitOps

极狐GitLab

DevOps gitlab CD workflow ArgoCD

直播预告|StarRocks 3.2 全面解读:易用性和性能的双重提升

StarRocks

数据库 数据分析 OLAP StarRocks

谷歌史上最强大模型Gemini抢鲜知!!!

江湖修行

AI Google openai #人工智能 #大模型

app开发

Geek_8da502

聚首江城,共创开源未来 | 2023 OpenHarmony城市推介会•武汉市圆满落幕

科技热闻

代码混淆技术探究与工具选择

雪奈椰子

现在甘肃等保测评机构有几家?分别是哪几家?

行云管家

等级保护 等保测评 甘肃

系统分析师还是架构师?

escray

技术人写作 21 天技术人写作行动营

全面高压化与全面超快充,破解新能源汽车的时代难题

脑极体

数字能源

ETL+BI结合的数据集成工具

RestCloud

BI ETL 数据集成

Amazon CodeWhisperer 开箱初体验

亚马逊云科技 (Amazon Web Services)

人工智能 云上探索实验室 Amazon CodeWhisperer

SQL CREATE INDEX 语句- 提高数据库检索效率的关键步骤

小万哥

MySQL 数据库 sql 程序员 后端开发

一篇读懂Volatile关键字

是月月啊2023

Java 面试题

低代码实现探索(六十一)字段隔离解耦

零道云-混合式低代码平台

一起学Elasticsearch系列-模糊搜索

Java随想录

Java 大数据 elastic

一款带键扫的LED驱动电路

攻城狮Wayne

避坑指南之财务共享中心的服务管理

用友BIP

财务共享

华秋喜获“2023深圳行业领袖企业100强”称号

华秋电子

2024营销日历新鲜出炉!提前规划,赢在起跑线!

彭宏豪95

互联网 营销 在线白板 活动策划 效率软件

华秋DFM软件荣获2023年度电子信息行业可靠性质量提升典型案例

华秋电子

软件

写作行动营学员打卡-Day3-工作笔记

Luke

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