V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
• 请不要在回答技术问题时复制粘贴 AI 生成的内容
mizuhashi
V2EX  ›  程序员

我覺得 Ruby 最優秀的地方(RSpec)

  •  1
     
  •   mizuhashi · 13 天前 · 3063 次点击

    看隔壁關於 ORM 的討論說 Ruby/Rails 的長處都被其他語言學走了,但我覺得至少 RSpec 在 JS 世界還沒有替代。以下是一個用 RSpec 寫測試的例子:

    require 'rspec'
    require 'matrix'
    
    RSpec.describe 'numbers' do
      # 描述加法的性質
      shared_examples 'addition' do
        # 加法滿足交換律
        # a 和 b 會在實際的 context 裏注入
        it 'has commutativity' do
          expect(a + b).to eq(b + a)
        end
      end
    
      context 'for number' do
        # 令 a 和 b 為兩個隨機數,並調用加法的測試用例
        let(:a) { Random.rand }
        let(:b) { Random.rand }
    
        it_behaves_like 'addition'
      end
    
      context 'for vector' do
        # a 和 b 可以是向量,也滿足加法的性質
        let(:a) { Vector[Random.rand, Random.rand] }
        let(:b) { Vector[Random.rand, Random.rand] }
    
        it_behaves_like 'addition'
      end
    
    end
    

    如果要用 js 寫,至少所有的 a 和 b 都要換成類似context.a context.b,因為 ruby 有 self ,而 js 只有詞法作用域,這導致 js 的 dsl 表達力很受限。

    另外在 ruby 裏也可以很輕易做 mock ,例如這個例子修改了Date.today來返回我們測試用的日期:

    require 'rspec'
    require 'date'
    
    RSpec.describe 'Date' do
      # 選定一個測試用的日期,一般來說實際會使用隨機的
      let(:date) { Date.parse('1970-01-01') }
    
      before do
        # Date.today 本身是標準庫定義好的,但我們可以覆寫它來返回我們的日期
        allow(Date).to receive(:today).and_return(date)
      end
    
      describe '.today' do
        it 'is the mocked date' do
          # Date.today 現在會是我們 mock 的日期 1970-01-01
          expect(Date.today).to eq(date)
        end
      end
    end
    
    33 条回复    2025-08-22 22:36:01 +08:00
    dssxzuxc
        1
    dssxzuxc  
       13 天前   ❤️ 2
    ror 最优秀的地方在于极致地实现约定大于配置,以及魔法般的 DSL 。
    有什么好处?就是极致地少写代码。

    不同人有不同的喜好,就像我追求的是接近完美的类型安全,多写代码无所谓,反正现在大部分活是 AI 干的。而类型安全让我几乎杜绝各种傻逼运行时错误,可以安心地睡觉。你要说人菜那我无话可说,我本来就是一名普通的程序员,一定会犯错误,我只能希望能少犯错误。Ruby 很优秀与 Ruby 不适合团队协作并不冲突。
    ericguo
        2
    ericguo  
       13 天前   ❤️ 1
    @dssxzuxc 多写代码费 token 啊。。。能少写一点总是好的,只要没有歧义,我觉得 ruby 甚至可以当作以后所有其他语言的 LLM 转换的源语言用。没有明面上的类型干扰,更加容易让 LLM 关注逻辑,从逻辑推断出各个语言应该使用的类型或者需要导入的库。
    mizuhashi
        3
    mizuhashi  
    OP
       13 天前
    @dssxzuxc 那 idris 估計最符合,不僅類型安全,甚至能用類型證明邏輯正確😁
    NineTree
        4
    NineTree  
       13 天前
    看不懂,但是感觉很厉害
    qiumaoyuan
        5
    qiumaoyuan  
       13 天前
    @dssxzuxc 我觉得不是适合不适合团队合作,而是适合什么样的团队合作。我还是那句话,程序员写代码时的思维应该是主动的、清醒的、活跃的,而不是被动的、怠惰的。当然如果你习惯把一个方法的代码写得超过 10 行,那确实大脑容易超载。
    visper
        6
    visper  
       13 天前
    ruby 都已经没人用了吧。
    dddd1919
        7
    dddd1919  
       13 天前   ❤️ 2
    @dssxzuxc #1 除此之外觉得还有一点就是 all in one 的思路,通过这种 add-in 可以在一个工程内解决各种各样的问题。测试除了 Rspec ,还用过 Capybara 处理模拟 web 的测试,用 factory girl 构造铺底数据,用 waiter 去做自动化灰盒,这一切都是一个工程统统搞定,到现在其他生态很难找到这么完整的工具生态
    catamaran
        8
    catamaran  
       13 天前
    @qiumaoyuan 如果你的代码每个方法都不超过 10 行,我想可以称为神人了
    kakki
        9
    kakki  
       13 天前
    这种手工造 DSL 的语言根本不适合有一定规模的团队合作,对于代码民工来说,架构设计为做填空题就行,无论谁来拉翔都破坏力有限,Ruby 比较适合小规模专家级团队,尤其是独立开发.
    qiumaoyuan
        10
    qiumaoyuan  
       13 天前
    @catamaran 不要自己吓自己,也不要限制自己。我觉得主要是很多人遇到问题习惯性的选择应付,而不是死磕。看看多数人对“屎山”的态度,几乎没有人选择去清理干净,增强自己能力的,而是能跑就行。在“屎山”的基础上,遇到 bug 的态度是兵来将挡水来土掩,不考虑怎么系统性的解决,除非实在应付不了,才会逼自己努力思考一下。
    zpvip
        11
    zpvip  
       13 天前
    @kakki 我一直搞不明白很多团队的代码合并流程, 团队成员发 pull request 时不同时提交测试吗? pull request 合并前没有人审核代码吗?

    如果新代码能通过各种测试, 有两人以上 approved, 能有什么破坏力?
    Configuration
        12
    Configuration  
       13 天前
    实际上 RSpec 在 ruby 社区争议挺大的,喜欢的人很喜欢,讨厌的人很讨厌
    cloudzhou
        13
    cloudzhou  
       13 天前
    说起这个测试用例,接触 ai 后,平时补充代码不让我惊讶
    ai 来写测试代码,那真是又稳又细,修修改改就可以用了

    所以 ai 目前还没有到创造性的时候,擅长于已有的数据下,分析进行后续处理
    kakki
        14
    kakki  
       13 天前
    @zpvip 因为很多公司的要求 1. 出活 2. 低成本的出活 招来低水平的人也要能干活 完全不是你说的技术性问题, 这本质上是一个资源管理问题.
    cloudzhou
        15
    cloudzhou  
       13 天前
    @zpvip #11 按照我经历过的公司,没有哪家真的测试用例 80% 以上的,都是主流程走一走,甚至依靠一些白盒测试,而人日压缩越发严重,压力很大,每次接需求就是填坑而已

    之前我从 Java 世界到 Django ,简直蜜月期
    后来为什么去掉幻想呢?从我需要大批量修改某个变量开始

    比如说有一个广泛大量使用的表,我因为业务需求,需要字段重命名 nameXXX -> nameYYY
    如果是静态语言,那么 ide -> refactor/rename 等,一把搞定
    同理 看某个全局变量哪里引用,对应修改逻辑

    我不知道这么多年,脚本语言是否改进了,在当时的话,是依靠 grep + 人肉

    但是,我修改了好多次,发现总是漏了一些地方
    起码 Python 来说,是运行时解析,到对应代码,才抛出错误

    ror 不知道是否完善一些
    msg7086
        16
    msg7086  
       13 天前
    @kakki #9 #14
    所以当年湾区初创公司一堆用 Rails 的,反观国内一堆用 PHP 的。
    其实就是你说的小规模专家级团队 vs 招一堆兼职大学生或者低水平的人。
    lithium4010
        17
    lithium4010  
       13 天前
    jest-plugin-set
    zpvip
        18
    zpvip  
       13 天前
    @cloudzhou #15

    Rails 的标准 CRUD 的 DB Migration, Controller, Model, View 还有 RSpec / Minitest 都是命令行生成的, 如果就真是标准的 Restful, 那测试就是 100% 覆盖且通过, 此时你还没有写一行代码, 只运行了一行命令, 例如:

    bin/rails generate scaffold Post title:string body:text

    具体什么过程, 可以去这看视频: https://kamal-deploy.org, 从 9:54 开始.

    我一般会把 View 模板选做好, 不像 DHH 演示那样阳春. 当然实际情况不是这么简单, 各种一对多, 多对多关系, Post 嵌套 Comments 之类的, 补充一些测试用例也非常简单, 另外会提前安装测试相关的 Gems, 比如:
    gem "capybara"
    gem "selenium-webdriver"
    gem 'webmock', '~> 3.25'
    gem 'shoulda-matchers'
    gem 'rspec-rails'
    gem "factory_bot_rails",
    gem "faker", "~> 3.5"

    现在有了 AI, 它自己会发现这些 Gems 并用这些 Gems 去写 RSpecs. 所以覆盖率 95% 以上是不难的.

    一般 Rails 程序员都会有个启动模板, 也可以用别人现成的.

    https://github.com/nickjj/docker-rails-example

    字段重命名的事, IDE 批量替换就可以了吧, 如果测试覆盖率好, 一下就可以找出漏网之鱼吧. 当然, 最简单的方法就是不改旧代码, 加个别名就可以了, 李白, 字太白, 号诗仙, 几个名字同时可用, 以后有心情了也可以一天改一处, 发现一处改一处, 天天提交 Git commit 还可以混 KPI.

    class User < ApplicationRecord
    alias_attribute :name_老字段名, :name_新改的字段名
    end

    u = User.new
    u.name_老字段名 = "Alice"

    u.name_老字段名 # => "Alice"
    u.name_新改的字段名 # => "Alice"

    u.name_新改的字段名 = "Bob"
    u.name_老字段名 # => "Bob"


    我以前做了十多年 C++, 副业也用 PHP 开发过流量很大的系统, 后来尝试换成 Ruby on Rails, 发现这才是网站开发的正确方式, 再后来发现全职 RoR 岂不是更爽, C++ 又不会给更多钱.

    年纪大了为了能更适应就业市场, 也尝试用 Go/C#/Java/PHP/JS (React, NodeJs), 越用越气, 相比 Ruby on Rails 真是太麻烦了, 稍写两下就感叹, 在 Ruby on Rails 不就一句话的事吗, 各种测试写得头痛, 难怪覆盖率上不去. 语法方面, Java 的 Lamda 把我气笑了, 只有一个抽象方法的接口?! 真是老气横秋, 感觉有人按着我的头写代码, 的确是很好的防呆设计.

    AI 时代, 现在程序员一个顶十个, 全栈工程师应该更吃香, 希望我还能再战几年后再去卖红薯.
    razertory
        19
    razertory  
       13 天前
    是不是用 Scala 写出来也优雅

    import org.scalatest.flatspec.AnyFlatSpec
    import org.scalatest.matchers.should.Matchers
    import org.scalatest.{OneInstancePerTest, Outcome}
    import org.scalatest.wordspec.AnyWordSpec
    import scala.util.Random

    // 共享测试
    trait AdditionBehaviors { this: AnyWordSpec with Matchers =>

    // 用 def 而不是 val ,让子类可以延迟绑定
    def a: Double
    def b: Double

    def additionBehavior(): Unit = {
    "addition" should {
    "be commutative" in {
    (a + b) shouldEqual (b + a)
    }
    }
    }
    }

    // 针对 Double 的测试
    class DoubleAdditionSpec extends AnyWordSpec with Matchers with AdditionBehaviors {
    override def a: Double = Random.nextDouble()
    override def b: Double = Random.nextDouble()

    "Double" should {
    behave like additionBehavior()
    }
    }

    // 针对 Vector[Double] 的测试
    import breeze.linalg.{DenseVector => Vec}

    class VectorAdditionSpec extends AnyWordSpec with Matchers with AdditionBehaviors {
    override def a: Double = Random.nextDouble()
    override def b: Double = Random.nextDouble()

    // 这里把 a 、b 换成向量
    private val v1 = Vec(a, a)
    private val v2 = Vec(b, b)

    "Vector" should {
    behave like additionBehavior()
    }

    // 重新实现加法行为,因为这里是向量
    override def additionBehavior(): Unit = {
    "addition" should {
    "be commutative" in {
    (v1 + v2) shouldEqual (v2 + v1)
    }
    }
    }
    }
    charles0
        20
    charles0  
       13 天前
    QuickCheck 、Hypothesis 是不是都能实现这个?
    cloudzhou
        21
    cloudzhou  
       13 天前
    @zpvip 别名就算了,那肯定不是解决之道,清晰是第一位

    我在想,是否有个语言,有 ror 的开发舒适度,同时具备静态编译过程(可选),然后还能编译成为一个独立 bin (类似 Go )
    也就是你在开发阶段,ror 现有开发方式;又是严格类型和语法,ide 完全可解析,比如变量名方法名错误及时提示(加个运行参数)

    那就完美了
    xgdgsc
        22
    xgdgsc  
       13 天前 via Android
    @cloudzhou https://github.com/erikedin/Behavior.jl?tab=readme-ov-file 可以试试 Julia ,就是目前编译独立 bin 在实现进程中,编译出比较大的一坨 dll 目前就可以,1.12rc 在测试编译出小体积的 dll 了。
    doraemon0711
        23
    doraemon0711  
       12 天前   ❤️ 2
    我是力挺 rspec 的一派,因为我认为 rspec 完全发挥了 ruby 的高度定制 DSL 的能力,让写测试就像写文章一样顺畅
    但我也充分理解一个对 ruby 不熟悉的人,或者母语非英语的人看到这些代码会一脸懵的情况,因为我刚开始用 rspec 是就是这样的

    说回 ruby ,我认为其最显著的特点,是用**最简洁**的方式把面向对象发挥到了极致,当意识到 ruby 中任何定义的 class 都是 Class 的实例、`1+1`实际上是`1.+(1)`时,我才意识到什么叫完全面向对象的编程语言,你或许可以指责 ruby 性能不行,但了解过 ruby 的人一定不应该指责其语法不优雅
    flyqie
        24
    flyqie  
       12 天前 via Android
    说起来,有点好奇,ruby 很多设计确实不错,但现在用的似乎并不算多,为什么呢?
    mizuhashi
        25
    mizuhashi  
    OP
       12 天前
    @flyqie 我覺得這兩者是沒有關係的,像 react 的設計非常落伍但是用的人卻很多。從個人的角度我只知道自己很喜歡 ruby ,14 年的時候我要在 ruby python node 裏面選一個學,然後看了作者 matz 的書,很欣賞這個人,就堅定使用 ruby 了,後來也遇到了很多用 ruby 的有趣的人(當時在 qq 群裏的老師是一位寫 rpgmaker 腳本的)。我覺得技術背後的人/人文是很重要的,如果要為用的人少找個原因,大概就是有類似想法的人不是多數
    mizuhashi
        26
    mizuhashi  
    OP
       12 天前
    @razertory 應該是的,在 ruby 裏的實現方式是,給定一個代碼塊,如 { a },我們可以自由決定這個 a 的解讀方式。實際上這個 a 代表的 self.a ,然後 ruby 可以在執行每個代碼塊的時候指定 self 是什麼,再找到對應的 a 方法,只要具備類似的特性就能實現差不多的東西
    cloudzhou
        27
    cloudzhou  
       12 天前
    @flyqie 工业化和艺术品的区别,我是个艺术家,要把这个碗做的美的不可方物,耐摔实用一点不在乎;我要生产日用碗,耐摔可靠,最好一天批量生产 1 万个
    kneo
        28
    kneo  
       12 天前 via Android
    有没有可能,别人没有,是因为嫌弃?
    DOLLOR
        29
    DOLLOR  
       12 天前
    @mizuhashi
    a 自动绑定到 self.a ,JS 曾经有类似的语法,也就是 with 块。
    动态绑定 self ,在 JS 里也有类似的 call/apply/bind 。
    还有 eval 、new Function ,可以玩各种很骚的操作。
    但这些特性在 JS 里,通常不被视为优秀,而是糟粕,遭到唾弃。
    或许这就是从“精致的小众玩意”转变到“流水线的大众工具”的代价吧
    qiumaoyuan
        30
    qiumaoyuan  
       12 天前
    @flyqie 语言优秀不优秀,跟用的人多不多没有太大关系。我觉得以逻辑严谨著称的群体,应该要看得清楚这点。

    很多时候反而设计得很糟糕的东西更流行,每当有人谈到排行榜、流行程度,我就总是联想到 MooTools 和 jQuery 。MooTools 设计得十分优秀,无论是源码(喏,你真要看每个函数/方法 10 行左右的代码,MooTools 相当多时候都是),还是暴露出来的接口,都简洁得不像话,但受众就是少得可怜。

    而 jQuery 满世界闻名,流行程度我就不用多说了。但如果你想要复用代码,它就一个插件机制可选,写出来的代码总是又臭又长。反观 MooTools 本身基于对象的设计,OOP 当中那么多的复用手段,它都完全可以利用。

    jQuery 流行的原因是啥?我觉得就是看起来上手简单。但工具这东西,上手难度和往深了用之后的趁手程度往往是两回事。

    另外,如果看排行榜的话,以前 C 和 C++ 一直是老大,这两年 AI 火了,Python 一下窜到第一,所以 Python 突然变成最优秀的语言了吗?明显不是这个原因,对吧?
    qiumaoyuan
        31
    qiumaoyuan  
       12 天前
    说话 RSpec 这东西,我特别喜欢的是它的 context (以及它的各种别名:describe ,example_group 等),这玩意可以把测试场景划分得很细,可以无限层级地复用上下文,简直就是为测试时遇到的各种场景细微区别时,最大限度复用代码而生的。

    当然 TestUnit 和 MiniTest 本身利用好 OOP 也可以做到一样的效果,就是要求使用者熟练掌握 OOP 各项特性。
    mizuhashi
        32
    mizuhashi  
    OP
       12 天前 via iPhone
    @DOLLOR 有道理,用 with this 加 bind 可以實現類似的
    mizuhashi
        33
    mizuhashi  
    OP
       12 天前 via iPhone   ❤️ 1
    @qiumaoyuan rspec 寫起來很快樂,跑起來也很快樂,免費的快樂
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2802 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 15:02 · PVG 23:02 · LAX 08:02 · JFK 11:02
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.