💎

SorbetとMiniTestでRubyに入門する

2024/07/11に公開

GitHub

コードはこちらに載せています.docker compose up -dで立ち上がるので煮るなり焼くなりどうぞ.スター⭐️もらえるとモチベーションに繋がります!
https://github.com/shunsock/ruby_playground

TL;DR

知見だけまとめます.

  • Sorbetが良い.他の言語と同じ感覚で書ける
  • Sorbet Runtimeで実行時解析が効いて嬉しい
  • MiniTestでシンプルにテストを書ける

注意

Rubyガチ初心者です.普段はPython, Rust, PHPをメインに書いています.
https://github.com/shunsock

使用したツール

パッケージマネージャー: Bundler
静的解析ツール: Sorbet CLI, RoboCop
テストツール: MiniTest
実行時解析: Sorbet Runtime

※ Bundlerのリンク先がgemになっているのはBundlerのレポジトリがPublic Archiveになっていてgemのレポジトリに管理先が移行しているからです

Bundler is now maintained in the rubygems/rubygems repository.

ざっくりとした書き方

main.rbファイルはこのような感じになっています.

# typed: strict
require 'sorbet-runtime'
require_relative 'service/fizz_buzz_runner'

class Program
  extend T::Sig

  sig { void }
  def self.main
    loop = Loop.new(1, 30)
    fizz_buzz_runner = FizzBuzzRunner.new(loop: loop)
    fizz_buzz_runner.run
  end
end

Program.main

上から順番に説明すると

  • #typed: strict: 静的解析用のコメントアウトです.
    • typed: ignore, typed: false, typed: true, typed: strict, typed: strongの5段階あります.
    • 私も書いている時にstrongがあることを思い出しました.PHPだとdeclare(type=strict)と書くので,癖かもしれないです.
    • https://sorbet.org/docs/static
  • require 'sorbet-runtime': runtimeでの型チェックを有効化します.
  • extend T::Sig, sig { void }
    • Sorbetの機能でコードにsignatureをつけることができます
    • ここでは特に制御するものがなかったので void ですが,parametersreturnsといった入出力を制御することができます
  • fizz_buzz_runner.run
    • Rubyでは引数のない関数には括弧をつけないです.
    • 地味に他では経験したことのない文法でした.

Static Analysis

Sorbetを使っています.Sorbetを使うとRubyのコード上に型情報を付与できます.

SorbetはRBSとは異なり,サードパーティ製ツールです.しかし,開発元がStripeで,Shopify社やGitHub社といった「Rubyを使っている大企業」で想起されるような会社が採用をしています.そして,Repositoryを確認したところ,直近の更新(2024.07.11時点)もlast weekと開発も進んでいるようです.以上の理由から突然なくなることもないかなと判断し採用しました.

# frozen_string_literal: true
# typed: strict

require 'sorbet-runtime'
require_relative '../../app/domain/loop'
require_relative '../../app/domain/printer/fizz'
require_relative '../../app/domain/printer/buzz'
require_relative '../../app/domain/printer/fizz_buzz'
require_relative '../../app/infrastructure/message_provider'


class FizzBuzzRunner
  extend T::Sig

  sig { returns(Loop) }
  attr_reader :loop

  sig { returns(MessageProvider) }
  attr_reader :message_provider

  sig { params(loop: Loop, message_provider: MessageProvider).void }
  def initialize(
    loop: Loop.new(1, 100),
    message_provider: MessageProvider.new
  )
    @loop = T.let(loop, Loop)
    @message_provider = T.let(message_provider, MessageProvider)
  end

  sig { void }
  def run
    (@loop.start_with..@loop.end_with).each do |i|
      puts @message_provider.provide(i)
    end
  end
end

Test

MiniTestを採用しています.以下のように書くことでtestdir以下の_test postfixがついたファイルを実行できます.

bundle exec ruby -Itest -e 'Dir.glob("./test/**/*_test.rb") { |file| require file }'

MiniTestはRSpecと比較していますが,個人的な好みで採用したので,特別強い信念があるというわけではないです.

実装例

ここではFizzBuzzのiter変数ごとに出力を切り替えるプログラムを実装しています.入力はInteger型で出力はString型です.

# frozen_string_literal: true
# typed: strict

require 'sorbet-runtime'
require_relative '../domain/printer/fizz'
require_relative '../domain/printer/buzz'
require_relative '../domain/printer/fizz_buzz'


class MessageProvider
  extend T::Sig

  sig { void }
  def initialize
    @fizz = T.let(Fizz.new, Fizz)
    @buzz = T.let(Buzz.new, Buzz)
    @fizz_buzz = T.let(FizzBuzz.new, FizzBuzz)
  end

  sig { params(counter: Integer).returns(String) }
  def provide(counter)
    if (counter % 3).zero? && (counter % 5).zero?
      @fizz_buzz.provide_message
    elsif (counter % 3).zero?
      @fizz.provide_message
    elsif (counter % 5).zero?
      @buzz.provide_message
    else
      counter.to_s
    end
  end
end

テスト例

実装と同じように Sorbetを使ってSignatureを記載します.テストファイルにはminitest/autorunを追加します.assertなどは他の言語と同様に行います.

# frozen_string_literal: true
# typed: strict

require 'minitest/autorun'
require 'sorbet-runtime'
require_relative '../../app/infrastructure/message_provider'

class MessageProviderTest < Minitest::Test
  extend T::Sig

  sig { void }
  def setup
    @message_provider = T.let(MessageProvider.new, T.nilable(MessageProvider))
  end

  sig { void }
  def test_show_fizz_when_counter_is_three
    test_case = 3
    assert_equal('Fizz', T.must(@message_provider).provide(test_case))
  end

  sig { void }
  def test_show_buzz_when_counter_is_five
    test_case = 5
    assert_equal('Buzz', T.must(@message_provider).provide(test_case))
  end

  sig { void }
  def test_show_fizz_buzz_when_counter_is_fifteen
    test_case = 15
    assert_equal('FizzBuzz', T.must(@message_provider).provide(test_case))
  end

  sig { void }
  def test_show_fizz_buzz_when_counter_is_multiple_of_three_and_five
    test_case = 30
    assert_equal('FizzBuzz', T.must(@message_provider).provide(test_case))
  end

  sig { void }
  def test_show_counter_when_counter_is_not_divisible_by_three_or_five
    test_case = 1
    assert_equal('1', T.must(@message_provider).provide(test_case))
  end
end

Rubyのシンプルな文法のおかげでコードが読みやすいです.

書いてみた感想

初めてRubyを書いてみて,静的解析に従っていけばコードの一貫性を保ちながら高速にプログラムを作成できて楽しかったです.2スペースインデントも最初は慣れなかったですが,ネストが深くなるところでtabより読みやすいなあと感じました.以下,各ライブラリの感想です.

  • Sorbetの静的解析を通ったコードは一貫性を持っていて非常に読みやすい.Sorbet特有の書き方を理解するのが大変だが,頑張る価値はあると思う.
  • Sorbet Runtimeで実行時型解析が効いて嬉しい.PythonだとClassと関数で別のライブラリを使うので結構大変だったりする.
  • MiniTestでシンプルにテストを書ける.テストコードの複雑度が上がらないようになっていて非常に嬉しい.

めっちゃどうでもいいですが,ZennのアイキャッチでRubyと入れるとダイヤが出てくる仕様が地味にツボでしたw
"Blue" Ruby

Discussion