资讯动态

用 RSpec 践行 TDD:以 Ruby 构建命令行版 Connect Four 的完整实战

发布时间:2026/9/16 15:48:27 来源:尧图企业网站定制
用 RSpec 践行 TDD以 Ruby 构建命令行版 Connect Four 的完整实战【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculumTDD测试驱动开发的核心是把先写代码、后补测试的习惯反转过来先写一个必然失败的测试再写恰好让测试通过的代码最后重构。本篇指南以当前开源课程仓库中的 project_connect_four.md 为骨架完整还原该课程的训练路径——先为旧的 Caesar Cipher、Tic Tac Toe 项目补写 RSpec 测试再以 TDD 流程从零构建命令行版 Connect Four。读完本文你将掌握 RSpec 的describe/it/expect语法、常用匹配器、mock/double 隔离技巧、RuboCop RSpec 的接入方法以及红-绿-重构循环的完整落地方式。为什么为旧项目补写测试是最佳热身原文档开篇就点明了一个反直觉但极其有效的学习方法想要快速熟悉一个新项目并开始为其贡献最好的方式就是先给它写测试。这与你入职后接手陌生代码库的体验完全一致——写测试迫使你逐行阅读源码、厘清每个公开方法的输入输出、搞清楚类与类之间的依赖关系远比通读一遍更有效。原文档还给出一个非常现实的提醒测试代码的行数往往是项目业务代码的两倍。这不是夸张。一个完整的测试套件通常要为每个方法覆盖正常路径、边界情况和异常路径再加上大量describe/context分组与let变量代码量自然水涨船高。所以写测试时感到代码越写越多是正常现象恰恰说明你的覆盖在走向完备。在开始任何测试之前请先确认你已经掌握了 RSpec 的基础语法。本仓库的 RSpec 基础入门 与 RSpec 代码共享机制 两课完整讲解了describe示例组、it单个示例、expect(...).to期望、匹配器、Arrange-Act-Assert 三段式、before/after钩子、let与subject的用法而 测试驱动开发 一课则系统阐述了 TDD 的红-绿-重构循环及其价值。如果这些概念还不牢固建议先通读这三篇再动手做本项目的练习。热身任务一在 ruby_testing 仓库中完成 spec 练习原文档给出的第一个任务是克隆官方配套的ruby testing 练习仓库TheOdinProject/ruby_testing并完成其中spec文件夹内的课程练习。这类练习仓库的典型结构是ruby_testing |__lib # 待测试的业务代码 |__spec # 课程配套的测试任务部分留空等你补全 |__.rspec练习的设计思路通常是仓库里已经给出部分测试骨架和对应实现你需要模仿已有测试的风格为尚未覆盖的方法补写测试。这是成本最低的 RSpec 上手方式——因为答案是否正确可以立刻通过rspec命令验证而且你能在读别人写好的测试与自己写测试之间来回切换快速建立语法手感。原文档同时建议观看 Sandi Metz 关于 Ruby 测试的讲解视频。Sandi Metz 的核心主张与本文主题高度一致测试关注的是消息messages与行为behaviors而非内部实现细节——这直接呼应了后面 Tic Tac Toe 任务中用 mock 隔离方法的要求。热身任务二为 Caesar Cipher 补写测试在 Caesar Cipher 项目 中你已经实现过一个凯撒密码加密函数。现在回头为它写测试原文档给出的建议是不应该超过半打约 6 个测试。凯撒密码的输入空间虽然大但测试只需覆盖几类典型用例# spec/caesar_cipher_spec.rb示例 require ./lib/caesar_cipher RSpec.describe Caesar Cipher do it shifts letters by the given factor do expect(caesar_cipher(hello, 3)).to eql(khoor) end it wraps around from z to a do expect(caesar_cipher(z, 1)).to eql(a) end it preserves case do expect(caesar_cipher(Hello, 1)).to eql(Ifmmp) end it keeps non-alphabetic characters unchanged do expect(caesar_cipher(a!b, 1)).to eql(b!c) end it handles negative shifts do expect(caesar_cipher(b, -1)).to eql(a) end it returns the original string when shift is a multiple of 26 do expect(caesar_cipher(hello, 26)).to eql(hello) end end原文档特别强调做这个练习时要使用完整的 git 工作流——像 Revisiting Rock Paper Scissors 一课中学到的那样为新增测试与实现创建独立的功能分支feature branch避免直接在主线分支上修改代码。这既是版本控制练习也是在安全环境里大胆重构的前提。让 RuboCop RSpec 开始工作在这个任务中原文档建议你正式引入RuboCop RSpec扩展。此前的 Linting and RuboCop 一课已经介绍过 RuboCop 本身——它通过 Style、Lint、Metrics 等部门检查 Ruby 代码质量而rubocop-rspec是官方扩展专门针对 RSpec 测试代码的常见陷阱给出告警例如单个it块中出现多个expect应拆分成独立示例使用实例变量user而非let/subject共享测试数据过度使用should旧语法等。接入流程与普通 RuboCop 扩展一致先在Gemfile中加入gem rubocop-rspec, require: false然后在项目根目录的.rubocop.yml中声明插件plugins: - rubocop-rspec之后运行bundle exec rubocop即可同时检查业务代码与 spec 代码。原文档的立场非常明确从此刻起所有涉及 RSpec 的项目都建议安装并使用 RuboCop RSpec——让机器替你记住测试规范把注意力留给测试本身的设计。热身任务三为 Tic Tac Toe 补写测试前一个 OOP 项目 是命令行版 Tic Tac Toe井字棋。为它写测试比 Caesar Cipher 复杂得多测试不再是给几个输入、断言返回值那么简单因为胜负判定依赖棋盘整体状态。原文档给出了三步测试策略1. 先测胜负判定最关键的条件分支以#game_over或你实现中对应的方法为例测试目标非常明确当棋盘上出现胜利局面时方法必须返回真。例如棋盘第一行全部为 X 时#game_over应触发RSpec.describe Game do describe #game_over do it returns true when the top row is all X do board [ %w[X X X], %w[O _ _], %w[_ _ _] ] game Game.new(board) expect(game.game_over?).to be(true) end it returns true when a column is all O do board [ %w[X O _], %w[X O _], %w[_ O _] ] game Game.new(board) expect(game.game_over?).to be(true) end it returns false when no one has won yet do board [ %w[X _ _], %w[_ O _], %w[_ _ _] ] game Game.new(board) expect(game.game_over?).to be(false) end end end注意这里使用了be(true)/be(false)匹配器——对于返回布尔值的谓词方法以?结尾be匹配器比eq更贴切、可读性更好。行、列、对角线三类胜利路径都要覆盖这正是玩家该赢时必须赢的语义。2. 逐一测试关键方法并覆盖边界情况除了#game_over还应对棋盘展示、落子合法性如占用格子不可再落子、平局判定等关键方法逐一写测试并刻意构造边界输入。网格类游戏最容易出 bug 的地方恰恰是边界棋盘已满但无人获胜、最后一格落子恰好形成胜利、在非空位置落子等。3. 用 mock/double 隔离方法依赖原文档要求的最后一步是使用 mocks/doubles 隔离方法确保它们返回正确的输出。这是 RSpec 中极具价值的一环当被测方法与另一个复杂对象协作时你并不想为每次测试都真实构造那个对象尤其当它依赖随机数、用户输入或 IO 时。这时可以注入一个 doubleRSpec.describe Game do describe #switch_player do it passes the current board to the next player do fake_player double(Player) board instance_double(Board, to_display: ...) game Game.new(board: board, players: [fake_player]) # 验证 game 确实向玩家发送了消息 expect(fake_player).to receive(:take_turn).with(board) game.switch_player end end enddouble创建的是一个替身对象你只关心被测方法是否以正确的参数向它发送了正确的消息message而不关心替身内部如何实现。这正是 Sandi Metz 所强调的面向行为而非实现的测试哲学。RSpec 提供了从double完全替身到instance_double校验方法签名与真实类一致的严格替身的一整套工具具体写法可以在本仓库的 RSpec 基础入门 中结合示例研习。主项目以 TDD 构建命令行版 Connect Four完成上述热身之后正式项目登场用 TDD 方式构建命令行版 Connect Four四子棋。游戏规则与设计要点Connect Four 的规则足够简单两名玩家轮流把棋子从顶部投放到一个竖直的网格cage中棋子落到底部或已有棋子之上谁先在水平、垂直或对角线方向连成 4 颗棋子谁就获胜。你之前已经构建过多个命令行游戏井字棋、石头剪刀布等纯 Ruby 实现这部分对你不构成挑战——真正的新挑战是流程而不是逻辑。原文档给了两个锦上添花的建议想让棋子在终端里更好看可以查阅Unicode 杂项符号表用●、○、等特殊字符代替普通的X/O整个开发过程必须全程 TDD。TDD 循环一次只走一小步原文档把 TDD 的节奏描述得非常具体每一步的思维过程大致是思考需要发生什么——例如玩家把棋子放进第 3 列棋子应落到该列最低的空位写一个必然失败的测试——此时甚至还没有对应方法运行rspec会得到 NameError 或失败写恰好能让测试通过的代码——不多写一行思考能否重构——在不改变行为的前提下改善代码结构然后重新跑测试确认仍然是绿的。这段循环对应 RSpec 输出中的红 → 绿 → 重构# 红失败的测试 F Failures: 1) Board#drop_token drops a token to the lowest empty slot in the column Failure/Error: expect(board.drop_token(0)).to eql(5) expected: 5 got: nil原文档特别提醒初学者不要急于求成一个方法往往需要两个测试才能让它真正有用。例如#drop_token的第一个测试让它能返回行索引第二个测试则验证连续投两次时第二次落在上一行——这看起来像是过度设计但正是 TDD 的意义所在每个行为都有测试兜底后续重构才敢放手去做。# 建议的测试设计示例 RSpec.describe Board do describe #drop_token do it drops the first token to the bottom row do board Board.new expect(board.drop_token(0)).to eql(5) # 6 行网格的最底行 end it drops the second token above the first one do board Board.new board.drop_token(0) expect(board.drop_token(0)).to eql(4) end end describe #winning_combination? do it detects four tokens in a horizontal row do # 构造第 5 行 0-3 列均为同色棋子的棋盘 board Board.new # ... expect(board.winning_combination?(player: X)).to be(true) end it detects four tokens in a vertical column do # 构造第 3 列 2-5 行均为同色棋子的棋盘 # ... expect(board.winning_combination?(player: X)).to be(true) end it detects four tokens along a diagonal do # 构造左上到右下方向的连续四子 # ... expect(board.winning_combination?(player: X)).to be(true) end it returns false when there is no winner do # 空棋盘或未形成四连的局面 # ... expect(board.winning_combination?(player: X)).to be(false) end end end建议的类拆分与测试顺序参考你在 井字棋项目 中已经养成的 OOP 习惯Connect Four 至少可以拆出三类每类对应一组独立的 specBoard管理 6×7 网格提供#drop_token、#winning_combination?、#full?、#display等方法——这一层与 IO 无关最容易用纯测试覆盖Player持有名称与棋子符号行为极简Game负责主循环轮流落子、判断胜负、处理平局、渲染棋盘——这一层与命令行交互耦合测试时多用 double 替身隔离输入输出。推荐的测试顺序恰好就是 TDD 的自然顺序先 Board纯逻辑测试最好写再 Player最后 Game 的流程编排。每写完一个失败测试就实现对应方法保持测试套件始终处于绿色状态。真正在学的不是 Ruby而是 RSpec原文档有一段非常诚恳的话值得反复咀嚼做这个项目时你会花大量时间 Google如何测试某个特定功能这完全正常——因为你真正在学的是 RSpec而不是 Ruby。它需要一些时间去适应。常见的困惑包括测试应该断言什么——断言行为返回值、发送的消息不断言实现细节一个方法该拆成几个测试——一个行为一个测试边界情况单独成例double 该用多严格——测试内部协作用double需要校验签名用instance_double。当你想清楚我要让这件事发生那我该怎么测试它时你就已经掌握了 TDD 的精髓测试在驱动设计而不是在事后验证。附RSpec 快速参考卡为了让项目推进更顺畅这里把本课程此前学到的核心语法浓缩成一张参考卡完整讲解见 RSpec 基础入门语法作用RSpec.describe Class do ... end定义顶层示例组通常以被测类为参数describe #method/describe .class_method嵌套示例组#表示实例方法.表示类方法context when ...按状态/条件分组是describe的别名用于语义区分it should ... do ... end定义单个测试示例expect(actual).to eq(expected)相等匹配器expect(actual).to eql(expected)严格相等匹配器eql?expect(actual).to be(true)/be(false)布尔匹配器适合谓词方法expect(collection).to include(x)包含匹配器适合集合断言expect(...).not_to ...反向断言let(:name) { ... }/subject(:obj) { ... }惰性共享变量/被测对象before { ... }/after { ... }每个示例执行前后的钩子double(name)/instance_double(Class)替身对象用于隔离协作对象expect(dbl).to receive(:msg).with(args)消息期望验证方法向替身发送了正确消息另外记得三个工程化习惯测试文件统一放在spec/目录并以_spec.rb结尾每个测试尽量只保留一个expect断言在.rspec中开启--format documentation与--order rand让测试输出可读且随机排序执行从而暴露测试间的状态泄漏。附加资源原文档为深入读者提供了两份补充材料关于RSpec Mock Object替身对象的实战示例——当你需要验证方法是否以正确参数调用了协作对象时这是最直接的学习素材可与本文 Tic Tac Toe 部分的 double 示例互相印证关于RSpec 编写 Spec的入门文章——适合对describe/it/expect结构还想再巩固一遍的读者。两份材料都指向同一目标把测试写得像文档一样清晰、像代码一样可靠。回到开篇那句话——测试代码最终会占据整个项目代码量的很大比重而你现在做的每一组it块都是在为未来接手更大代码库、以及为他人理解你的代码铺路。保持红绿循环保持小步前进Connect Four 值得你为它写满一整页测试。【免费下载链接】curriculumThe open curriculum for learning web development项目地址: https://gitcode.com/GitHub_Trending/cu/curriculum创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价