ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

BDD实践误区与Cucumber工程化落地全解析

BDD实践误区与Cucumber工程化落地全解析 说到行为驱动测试很多团队的第一反应是不就是把用例写得像人话嘛然后匆匆忙忙接上Cucumber写完几个Feature文件就觉得已经实践了BDD。我见过太多项目最后变成了用Given/When/Then语法写的普通自动化脚本业务人员从不看代码维护成本反而比普通单元测试还高。真正的问题从来不是工具而是对BDD到底在解决什么问题缺乏共识。这篇文章我会围绕Cucumber这条主线把行为驱动测试从理念、工程结构、Gherkin编写、Step Definitions实现到报告集成和CI落地完整过一遍。内容适合三类人看计划在团队里引入BDD但还没动手的测试开发写了一阵子Cucumber但觉得维护成本失控的自动化测试工程师以及想搞清楚BDD和普通接口/UI自动化到底差在哪的开发者。1. 先搞清楚BDD到底在解决什么问题很多人把BDD理解成一种测试框架的用法这其实是本末倒置。BDD的本质是一种需求沟通方式它的全称是Behavior-Driven Development核心在于让业务人员、开发、测试用同一种语言描述系统行为再把这些描述直接变成可执行的测试。Cucumber只是把这种沟通结果落地成自动化测试的工具之一。1.1 从测试反模式说起我先说一个几乎所有团队都会踩的坑需求文档一份测试用例一份代码一份三份东西各自演进最后没人知道哪一份是准的。需求改了一行字测试用例可能隔了两周才同步代码更不用说等发现对不上时联调已经炸了。这背后的问题是翻译损耗。BA把业务需求翻译成需求文档测试把需求文档翻译成测试用例开发把需求文档翻译成代码。每一次翻译都引入歧义而BDD做的事情是取消中间的多次翻译让需求描述本身就变成可执行的测试。这就好比原来做菜要经过厨师手写菜谱-传菜员誊写-后厨照做三个环节现在直接用语音告诉后厨中间少了两层信息损耗。所以BDD的第一个价值不是自动化而是形成活文档Living Documentation。Feature文件里写的每一个场景既是需求说明又是验收标准还是自动化用例。需求变更时改Feature文件跑一遍测试所有影响一目了然。1.2 Cucumber在BDD实践中的定位Cucumber是BDD实践里最知名的执行引擎它识别的语言叫Gherkin。Gherkin是一种接近自然语言的结构化描述语法通过Given/When/Then这类关键词描述前置条件、操作行为和预期结果。Cucumber本身不关心你的业务逻辑它只负责做两件事解析Gherkin语句然后找到对应的代码步骤去执行。这里有一个关键区分Cucumber不是BDD本身它是BDD的载体。你完全可以不用Cucumber做BDD比如JBehave、SpecFlow都是同类工具。但Cucumber生态最成熟对Java、JS、Python、Ruby、Go都有支持而且它能方便地输出各种测试报告。另一个容易混淆的边界是BDD不等于自动化测试人员用Cucumber写脚本。真正的BDD强调需求的三种视角——业务视角描述价值用户视角描述行为开发/测试视角描述实现约束。在Feature文件里这三类内容会混在一起但写的时候脑子里要清楚当前这句话是在表达业务规则还是在表述技术细节。一个判断标准是这句话拿给不懂技术的业务人员看他能不能读懂并确认对这就是我要的。2. 工程结构设计与环境准备我第一次搭Cucumber工程时走了不少弯路比较典型的是把所有Feature文件和Step Definitions塞在一个包下跑了三个月之后项目里充斥着互相重复的步骤定义改一个公共步骤能引发十几个场景的连锁报错。后来重构时才意识到Cucumber工程的结构从一开始就要按业务模块来划分而不是按技术层次来划分。2.1 依赖引入与版本选择我以Java生态为例因为这是Cucumber使用最广泛的语言环境。目前稳定主流是Cucumber 7.x底层基于JUnit 5也兼容JUnit 4运行器。Maven坐标需要引入三块dependency groupIdio.cucumber/groupId artifactIdcucumber-java/artifactId version7.15.0/version scopetest/scope /dependency dependency groupIdio.cucumber/groupId artifactIdcucumber-junit-platform-engine/artifactId version7.15.0/version scopetest/scope /dependency dependency groupIdorg.junit.platform/groupId artifactIdjunit-platform-suite/artifactId version1.10.2/version scopetest/scope /dependency这里最需要注意的是cucumber-java和cucumber-junit-platform-engine版本必须完全一致否则会出现找不到步骤定义这类莫名其妙的问题。我见过太多人踩这个坑两个坐标版本不一样Cucumber运行时解析注解的类升级了但代码还是老版本定位问题浪费半天。另外注意不要混用cucumber-junit和cucumber-junit-platform-engine二者底层机制不同混用会导致测试用例被重复执行。选了JUnit Platform就一路用到底。2.2 目录结构与资源约定Cucumber有两个默认约定Feature文件放在src/test/resources/features/目录下Java步骤代码放在src/test/java对应包中。这两条路径可以通过CucumberOptions的features和glue参数修改但我不建议改遵循默认约定能省掉很多配置心智负担。从工程组织上看推荐按业务能力划分目录而不是按底层对象划分。比如一个电商项目可以这样分src/test/resources/features/ ├── 订单模块/ │ ├── 创建订单.feature │ └── 取消订单.feature ├── 支付模块/ │ └── 退款.feature └── 用户模块/ └── 登录.feature对应的Java包结构com/example/steps/ ├── 订单模块/ │ ├── 创建订单Steps.java │ └── 取消订单Steps.java ├── 支付模块/ │ └── 退款Steps.java └── 用户模块/ └── 登录Steps.java这样做的好处是当订单模块的步骤频繁变动时影响范围被限制在同一个目录不会像公共步骤包那样牵一发动全身。很多Cucumber教程喜欢把所有Step Definitions放在一个Stepdefs.java里这种写法只适合演示不适合工程。2.3 第一个最小可运行的骨架入门时我建议先用一条最简单的场景跑通全链路再去想复杂的东西。拿经典的计算器来说是最直观的但更贴合实际业务的是登录场景Feature: 用户登录 作为注册用户 我想要登录系统 以便访问个人中心 Scenario: 输入正确的账号密码 Given 我打开登录页面 When 我输入用户名admin和密码123456 And 我点击登录按钮 Then 我应该看到个人中心对应的Java步骤定义public class 登录Steps { Given(我打开登录页面) public void 打开登录页面() { // 初始化浏览器或调用接口 } When(我输入用户名{string}和密码{string}) public void 输入用户信息(String username, String password) { // 填充表单 } And(我点击登录按钮) public void 点击登录() { // 点击操作 } Then(我应该看到个人中心) public void 验证登录成功() { // 断言 } }这里有一个容易忽略的点中文步骤是可以作为方法名的但Java类名和方法名用中文在部分公司有编码规范冲突。稳妥的做法是步骤文本保持中文因为要贴近业务人员方法名用英文。我习惯这么做When(我输入用户名{string}和密码{string}) public void inputCredentials(String username, String password) { }这样Gherkin侧可读性不受影响Java侧也符合团队编码规范。3. 用Gherkin编写可读的场景Gherkin是整个BDD实践的需求语言它的质量直接决定了这套体系能走多远。我见过太多团队把Gherkin写成了伪装的代码到处都是技术细节、步骤之间的隐式依赖最后业务方完全看不懂沦为测试人员自娱自乐。3.1 语法基础与最小要素一个Feature文件由Feature功能描述和若干Scenario场景组成。每个场景有四个基础关键词Given描述前置条件表达系统处于什么状态When描述触发动作用户做了什么操作Then描述预期结果系统应该返回什么And/But补充上述三类的并列步骤这里有一个非常关键的原则避免在Gherkin里写具体的UI操作。比如Given 我打开Chrome浏览器 When 我在用户名输入框中键入admin这种写法是典型的拿Gherkin当自动化脚本业务人员看了毫无感觉他们只会问你为什么不直接说输入用户名更糟糕的是这种写法把UI改动直接暴露给了业务场景登录页从Web改到AppFeature文件全要改。正确的做法是抽象成业务行为Given 我是一个已注册用户 When 我用admin账号登录系统 Then 我应该看到个人中心我是已注册用户这个步骤在底层既可以直接造数据库用户也可以走注册接口还可以填充表单。UI怎么变都不影响Gherkin这一层。这个抽象层级是BDD实践中最难把握的一点我的判断标准很简单这句话描述的是系统的业务行为而不是用户与页面的交互方式。3.2 进阶语法Background、Scenario Outline与Data Table当多个场景拥有相同前置条件时用Background来提取公共上下文Feature: 购物车结算 Background: Given 我已经登录系统 And 我的购物车中有以下商品 | 商品名 | 数量 | 单价 | | 苹果 | 2 | 5.00 | | 香蕉 | 3 | 3.50 | Scenario: 正常结算 When 我点击结算按钮 Then 我应该看到订单总额为20.50元这里Data Table数据表很有用它以|分隔行和列Cucumber会自动转成列表结构传给步骤定义。Background极大的好处是把重复的Given挪到公共区场景读起来干净。但它也有代价新增场景时容易被Background的前置条件绑架如果某个新场景不需要登录就必须单独抽离。我的习惯是当Background超过4行时就要审视是否有多个不同上下文混在一起。Scenario Outline用于同一逻辑、多组数据的场景。这是BDD里最有测试味儿的语法Scenario Outline: 根据年龄段判断票价 Given 我的年龄是年龄 When 我购买门票 Then 我应该支付票价元 Examples: | 年龄 | 票价 | | 3 | 0 | | 12 | 50 | | 65 | 80 |换成测试术语这相当于数据驱动测试。年龄和票价是占位符Examples里的每一行都会生成一个独立场景。要注意的是这些场景在测试报告里会显示为根据年龄段判断票价加上Examples表格中的行号测试报告里如果看不出是哪组数据失败了排查时还得自己去翻Examples表——所以Examples里每一行尽量写清楚业务含义比如加一列描述Examples: | 场景描述 | 年龄 | 票价 | | 学龄前儿童 | 3 | 0 |这样报告里每个场景都有明确的业务可读性。3.3 场景编写的三个常见误区误区一一个场景里塞了太多步骤超过10行。场景太长说明你没有真正拆分用户行为流而是在写用户操作脚本。一个规范的业务场景应当控制在4-8个步骤以内超过就该拆成多个场景或者用Background抽取。误区二步骤之间隐式依赖。比如Given 我创建了一个订单 When 我取消该订单 Then 该订单状态为已取消看起来没问题但如果两个步骤之间用共享变量悄悄传订单号后续维护就会很痛苦。规范做法是在Given步骤里返回一个业务对象或编号通过Cucumber依赖注入在场景内部传递而不是用静态变量。后面讲Step Definitions时详聊。误区三把多条件断言揉进一个Then。比如Then 页面应显示创建成功且列表中出现新记录且数据库状态为已支付一个Then只做一种断言多条件就拆成多个And。这样失败了能够在报告里精确定位是哪一层断言挂了。4. Step Definitions的工程化实现如果Gherkin是BDD的需求层Step Definitions就是实现层。这一层的工程质量决定了整个套件的稳定性。我把Step Definitions当成普通生产代码来要求而不是测试代码就凑合写。4.1 从Gherkin到Java代码的映射Cucumber通过注解来绑定Gherkin步骤和Java方法Given(表达式)When(表达式)Then(表达式)And(表达式)/But(表达式)匹配支持两种方式Cucumber Expressions默认和正则表达式。Cucumber Expressions更简洁内置了{string}、{int}、{float}、{word}等类型占位符。比如When 我用用户名admin和密码123456登录对应步骤When(我用用户名{string}和密码{string}登录) public void login(String username, String password) { }Cucumber Expressions会自动把admin解析为字符串把123解析为Integer。曾经有很长一段时期大家习惯用正则^我用用户名(.)和密码(.)登录$但Cucumber 7建议直接用新语法正则仅在需要复杂匹配时才用。这里有个容易踩坑的点{string}默认匹配双引号包裹的内容如果你在Gherkin里写我输入账号admin不带引号就得用{word}。我见过很多刚上手的人写了{string}却忘了在Feature文件里加引号导致步骤一直匹配不上报undefined step。4.2 参数化与类型转换Cucumber内置类型不能解决所有业务问题比如日期Given 当前日期是2024-06-01如果用{string}接收再手动解析代码看着就繁琐。更优雅的方式是自定义参数类型DataTableType public LocalDate localDateEntry(String date) { return LocalDate.parse(date); }或者在方法参数上用Transformer注解ParameterType(\\d{4}-\\d{2}-\\d{2}) public LocalDate 日期(String date) { return LocalDate.parse(date); } Given(当前日期是{日期}) public void setCurrentDate(LocalDate date) { }这一步能极大提升Step Definitions的可读性。Data Table也可以直接映射成List对象Given(系统中有以下用户) public void createUsers(ListUser users) { }前提是User类有对应的构造函数或注解字段映射。这种表驱动的方式非常适合批量造数据比一个个步骤拼要利索得多。4.3 状态共享与HooksCucumber场景之间默认是隔离的这保证了每个场景的独立性。但场景内部经常需要在多个步骤之间共享数据比如Given创建订单得到订单号Then要用这个订单号校验。不要用static变量去共享Cucumber官方支持通过依赖注入来共享状态。常用方案是使用cucumber-picocontainer或cucumber-springdependency groupIdio.cucumber/groupId artifactIdcucumber-picocontainer/artifactId version7.15.0/version scopetest/scope /dependency依赖注入的核心是每个场景创建一个全新的Step Definitions实例而共享的测试上下文对象按场景隔离。比如public class TestContext { private String orderId; private Order order; // getter/setter } public class 订单Steps { private TestContext context; public 订单Steps(TestContext context) { this.context context; } }关键点来了不在步骤定义里new TestContext()而是通过构造函数注入。picocontainer会为每个场景实例化一个TestContext保证场景之间不串数据。用过Spring的同学可能不习惯但这套机制简单直接没有Spring依赖也能用。Hooks类以Before和After注解标记在场景之前和之后执行。注意这几个容易翻车的地方Before在Cucumber 7中优先于任何Given执行用于Web自动化时在这里初始化浏览器驱动After里关闭浏览器、清理数据。如果某个场景不需要浏览器比如纯接口测试可以用Before(web)加标签过滤Cucumber支持按Feature文件标签选择性执行Hooks。Hooks里不要做具体业务操作只做环境准备与清理。业务逻辑封装到Step Definitions否则你会在Hooks里攒出一堆无法被业务方理解的魔法行为。5. 测试报告与CI流水线集成Cucumber跑通了、功能都实现后下一步是让测试结果真正反馈给团队。一个BDD实践的成功标准是业务人员即使不打开IDE也能通过报告知道哪些业务行为是好的、哪些坏了。所以报告这事不能糊弄。5.1 多格式报告配置Cucumber 7使用CucumberOptions注解配置报告输出。最直接的方式Suite IncludeEngines(cucumber) SelectClasspathResource(features) ConfigurationParameter(key PLUGIN_PROPERTY_NAME, value pretty, html:target/cucumber/index.html, json:target/cucumber/cucumber.json) public class RunCucumberTest { }三种常用的报告格式侧重点不一样pretty控制台输出步骤执行详情适合本地调试html可视化网页报告含步骤耗时、失败堆栈适合团队审阅json结构化数据方便CI插件或后续工具加工实际项目中我还会加一个reruntarget/rerun.txt失败场景会写入这个文件。配合失败重跑插件解决偶发失败时不用全量重跑效率高不少value pretty, html:target/cucumber/index.html, json:target/cucumber/cucumber.json, rerun:target/rerun.txt这里有个心得报告路径和构建目录最好纳入版本管理忽略规则不然每次构建都会产生垃圾文件被提交。如果团队用Allure那就更容易了。Allure对Cucumber有官方适配配置方式是把plugin设为io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm生成的xml结果文件会被Allure收集并渲染成历史趋势、缺陷分类、步骤时间轴等丰富图表。我在团队里用Allure比较多因为它的场景层级Feature-Scenario-Step与Gherkin结构天然对应业务人员看着不晕。5.2 在Jenkins中稳定运行有了Cucumber测试套件接下来面临一个很实际的问题怎么在CI里稳定跑。Jenkins接入时我一般这么干第一步在Jenkins中创建Maven项目构建命令设置成mvn clean test -Dcucumber.filter.tagssmokesmoke是Feature文件上打的标签这样可以在不同流水线里跑不同范围的场景冒烟测试只跑smoke全量回归不传标签参数。第二步配置构建后操作发布HTML报告。如果用的Jenkins原生HTML Publisher插件有一个小坑报告页面默认禁止引用外部资源必须勾选Wrap HTML report using simple CSS或者改CSP配置否则报告会一片惨白。我第一次接入时就卡在这里后来发现是Jenkins的安全策略拦截了HTML内部的样式和脚本。第三步给失败重跑留后路。对于Web UI自动化这类容易偶发失败的场景建议在超时设置上多留余地。但更重要的策略是场景里不要做过多依赖时间的同步等待该用显式等待的用显式等待不然flaky测试会让你每天被报警邮件轰炸。6. 常见问题与排查技巧实录这部分都是项目里真实踩过的坑我整理成速查表按症状给排查方向和避坑经验。6.1 典型症状与排查速查症状最常见原因排查方式与解决办法报错undefined step步骤文本与Step Definitions表达式不匹配看报错里给出的步骤原文对照注解重点检查引号、参数类型是否一致报错ambiguous step同一个步骤被两个方法匹配Cucumber允许重复定义但它分不清时就会报歧义。全项目搜索该步骤文本删掉冗余定义场景内变量串值用了static变量跨场景传数据换成picocontainer或Spring的Context对象按场景隔离Chrome/Firefox自动跑时闪退浏览器驱动版本与浏览器版本不匹配检查WebDriverManager或手动下载的驱动版本是否匹配浏览器大版本时间等待失效隐式等待和显式等待混用统一用显式等待混用会加长等待且行为不可预测Maven测试执行时Cucumber用例没跑没有配置Suite类或运行器类检查是否有IncludeEngines(cucumber)的入口类以及feature文件路径是否在classpath下JSON报告生成但Allure没有数据插件没配置到plugin参数或者classpath冲突确认Allure的plugin配置与Allure命令版本匹配检查target/allure-results是否生成第一条undefined step几乎每个新手都会遇到。有一个排查技巧在控制台用pretty插件跑Cucumber会用黄色列出一串模糊匹配候选告诉你你是不是想找这个方法——对照候选和你的表达式很快就能发现问题。6.2 三个踩坑后的习惯第一个习惯强制步骤唯一。我后来在代码审查中加入了一条规则——同一条步骤文本含参数在全项目里只允许对应一个Java方法。寻求复用没错但复用要提成公共步骤类而不是在多个类里复制粘贴同样的方法签名。第二个习惯每个Feature文件配一个Owner。这个Owner负责维护该文件对应的步骤定义、数据准备、场景变更。当别的模块改接口影响了他负责的场景时由Owner来决策如何调整。BDD实践在大团队里失败往往不是因为工具而是因为Feature文件集体共有人人不管。第三个习惯定期清理废弃场景。开会讨论过的老场景、产品形态变更后不再适用的场景必须在一周内删除或标记为ignore。留着不跑的死场景会让测试套件膨胀最终跑一次要一两个小时团队就会失去跑它的意愿而没人愿意跑的BDD测试比没有测试更糟糕。7. 一些具体的实操场景展开前几节把BDD与Cucumber的骨架搭了起来这一节我想把内容铺得更细。用两个真实项目里的例子来展示一个偏接口层的BDD怎么落地一个偏UI层的BDD怎么处理。7.1 接口服务层的BDD实现第一个项目是支付网关的对接服务核心逻辑是接收第三方支付回调、验签、更新订单状态。如果按传统写法就是写几十个JUnit方法做各种回调验证。团队决定引入Cucumber后我们拿退款回调来做试点。Feature文件大概是这样Feature: 退款回调处理 作为支付网关对接服务 我要正确处理第三方退款回调 以便订单状态保持一致 Background: Given 存在一笔已支付的订单 And 该订单金额为100元 And 第三方支付平台配置了密钥 Scenario Outline: 退款回调验签失败 When 收到退款回调请求且签名错误 Then 响应码为响应码 And 订单退款状态不变 Examples: | 场景描述 | 响应码 | | 缺少签名字段 | 400 | | 签名时间戳超时 | 400 | | 签名算法不匹配 | 401 |编写时牵涉到一个技术点如何在Step里构造签名错误的回调签名是由密钥和参数拼接后算HMAC-SHA256的要构造错误签名很简单——在加密前篡改一个参数值。我们用了一个MockThirdPartyClient组件在测试中替换真实客户端的签名逻辑When(收到退款回调请求且签名错误) public void sendCallbackWithInvalidSignature() { String payload buildRefundCallbackPayload(); // 篡改orderId保持时间戳合法 payload payload.replace(\orderId\:\1001\, \orderId\:\9999\); String invalidSignature sign(payload tampered); // 用mock客户端发送请求到被测服务 }这个案例的启示是Step Definitions里可以把造数据的细节隐藏起来Gherkin层面只描述签名错误这个业务现象测试代码层面则忠实地构造出对应现象。业务方看Feature文件时不需要理解HMAC和时间戳窗口但测试人员看到Step实现时又能复现每一个细节。7.2 Web UI层的BDD实现第二个项目是后台管理系统的权限控制。用Cucumber做UI自动化时最大的痛点是稳定性。我们的解法是UI层的Gherkin只保留用户视角HTML操作完全隔离在步骤定义内部并且统一封装页面操作API。比如Scenario: 普通管理员无法查看用户详情 Given 我用管理员A账号登录后台 And 我进入用户管理页面 When 我点击查看详情按钮 Then 系统提示无权限对应步骤里用了Page Object模式And(我进入用户管理页面) public void navigateToUserManagementPage() { LoginPage loginPage new LoginPage(driver); UserManagePage userPage loginPage.loginAs(adminUser); userPage.waitUntilLoaded(); }这里有个容易忽略的细节Page对象里的loginAs方法返回的是目标页面对象而不是void。这样步骤定义里可以接着调用目标页面的行为代码更流畅也方便在页面切换时做显式等待。UI BDD一个高质量的评判标准是换一种UI实现比如从Vue换到React从Bootstrap换到Element UIFeature文件是否可以一行不改以我经验只要Gherkin里没出现点击按钮输入框这类UI感知词汇大概率能做到。如果做不到说明抽象层级还不够高。7.3 BDD与现有自动化框架的融合有些团队已经有了一套成熟的接口自动化框架底层是RestAssured或HttpClient封装的。这时候没必要推倒重来Cucumber完全可以作为需求描述层架在现有框架之上。做法是第一步把现有框架里根据场景造数据、调接口、断言结果的方法整理成可以复用的工具类。第二步在Step Definitions里调用这些工具类Gherkin只描述业务。第三步现有测试继续保留不强制迁移新需求优先用BDD写跑通之后再逐步把老用例翻译成Feature文件。这套渐进式落地策略比一次性大迁移稳得多。我们在支付网关项目里就是这么做的第一批只有6个场景跑了两周团队觉得报告清楚、排查方便才逐步扩大覆盖。8. 一个避不开的议题教练与团队文化BDD实践写到这里我想花一点篇幅聊聊非技术因素。很多团队引进Cucumber失败不是因为不会写代码而是因为三个角色业务/开发/测试没有真的坐下来开实例化需求的会。Cucumber官方倡导的三步流程是业务方描述期望行为讲需求团队把它提炼成具体例子写Examples自动化工程师把例子转成可执行测试写Gherkin步骤这三步里最常见的节奏问题是业务方讲完需求就走了剩下开发和测试自己猜Examples。猜出来的场景业务方事后又不认账。所以我在项目里坚持每个新需求至少安排一次需求实例化讨论会。测试人员带着Feature文件的草稿去逐条向BA确认如果我这么写场景符合你的预期吗BA确认过的场景写进Feature文件等于签了字。如果你们团队暂时做不到让业务方参与可以先做另一个折中方案让产品经理参加两周一次的测试场景评审只评审Feature文件里Scenario列表不去看代码。一次评审半小时就能把BDD的共享语言价值落地一半。9. 我最后想说的一点如果让我给准备落地BDD的团队一个最实在的建议那就是不要一上来就追求所有用例都BDD化。选定一个核心业务模块写好10个以内的Feature文件把流程跑顺把报告跑通让团队亲眼看到一份活文档带来的价值再逐步扩展。任何测试技术只有让团队觉得有帮助而不是增加负担才能真正存活下来。在这几年的BDD实践里我最大的体会是Cucumber最强大的地方不是把自然语言变成代码而是逼着团队在写代码之前把业务语言统一一遍。当你发现BA、开发、测试在讨论同一个场景时用词一致、没有歧义这套体系的价值就已经显形了。工具带来的是一次性的学习成本而好的需求沟通方式却是每天都在复利。
返回列表