woshidan's blog

そんなことよりコードにダイブ。

いまさらRSpecを触ってみて最初のうち戸惑っていた部分の話

この記事はRuby on Rails Advent Calendarの1日目の記事です。

実は、この夏からはじめて仕事で本格的に RSpec を触ることになり、少し慣れてきたところで最初に感じたことをメモしておきます。

RSpecの構文は慣れない人にはこんな風に映ることもあるんだーと笑っていただけますと幸いです。

TL;DR

  • subjectlet でテストで利用する変数の宣言と初期化をする記法に慣れるまで時間がかかった
  • it .. do ... end のブロックを example ということ、 example の中にテストコードを書いていくことがわからなかった
  • describecontext に文法上の違いがあるかと思って身構えていた

慣れないうちに戸惑った話

テストコードやテスト対象をどこにどうやって記述したらいいかわからない

なかなか斬新な話なんですが、どういうことかというと、たとえば、自分がこれまで一番テストを書いてきたのはAndroidなので、Java用のテストフレームワーク JUnit5を例に挙げると

// https://junit.org/junit5/docs/current/user-guide/#writing-tests-nested より
@DisplayName("A stack")
class TestingAStackDemo {

    Stack<Object> stack;

    @BeforeEach
      void createNewStack() {
      stack = new Stack<>();
    }

    @Test
    @DisplayName("is instantiated with new Stack()")
    void isInstantiatedWithNew() {
        new Stack<>();
    }

    @Nested
    @DisplayName("when new")
    class WhenNew {

        @BeforeEach
        void createNewStack() {
            stack = new Stack<>();
        }

        @Test
        @DisplayName("is empty")
        void isEmpty() {
            assertTrue(stack.isEmpty());
        }

        // ...

        @Nested
        @DisplayName("after pushing an element")
        class AfterPushing {

            String anElement = "an element";

            @BeforeEach
            void pushAnElement() {
                stack.push(anElement);
            }

            @Test
            @DisplayName("it is no longer empty")
            void isNotEmpty() {
                assertFalse(stack.isEmpty());
            }

            // ...
        }
    }
}

のような感じなんですが、

  • 基本的にテストはクラスの単位でまとめられていて
  • インスタンス変数などの定義は通常のクラスと同様に行ってよく
  • テストの各ケースも通常のメソッドと補足情報をアノテーションで指定したりするが、同様に書いていけばよい
  • 各テストケース実行前の変数の初期化は BeforeEach などのアノテーションがついた箇所で宣言とは別の箇所で行う

んですね。一方同じ内容をRSpecで書くと以下のようになります。

describe "A stack" do
  let(:stack) { Stack.new }

  describe "when new" do
    it "is empty" do
      expect(stack.empty?).to eq true
    end

    # ...

    describe "after pushing an element" do
      let(:an_element) { "an element" }

      it "is no longer empty" do
        expect(stack.empty?).to eq false
      end

      # ...
    end
  end
end
  • まず、テストのためにクラスを定義しない
    • 内部実装をよくわかってないので違うのかもしれませんが、少なくともそういう書き方をしない
  • let で変数の宣言と初期化の方法を一緒に記載してしまう。変数の初期化だけのために前処理のブロックを用意していない
  • テストの内容を書いていく箇所もメソッドではなくてブロックの中

という具合で、かなり戸惑いが大きかったです。

特に3番目について、RSpecでは it ... do ... end のブロックで囲まれた部分にテストのコードを書いていき、その単位を example といって、その中にexampleなどを書いてくのですが、最初は

describe ブロックや context ブロックの中にどうして処理を書いていけないのか( example のそれと同じブロックやんけ)」

みたいな感じであわあわしてました。

また、テスト対象が1つに決まっている場合は subject という機能を使うと

  • テスト対象が1つに決まっていることを明確化され
  • expect(Hogehoge).to みたいにテスト対象の指定をしない is_expected.to という書き方が可能となる

のですが、これも最初「テスト対象どこに行った??」という感じでした。

# subject を使わない場合
describe "A stack" do
  let(:stack) { Stack.new }

  describe "when new" do
    it "is empty" do
      expect(stack.empty?).to eq true
    end
# subject を使う場合
describe "A stack" do
  let(:stack) { Stack.new }
  subject { stack.empty? }
  # 特にテスト対象がインスタンスそのものでなく、
  # インスタンスのメソッドの結果だったりする場合は
  # 直感的に理解できるようになるまで時間がかかった

  describe "when new" do
    it "is empty" do
      is_expected.to eq true
    end

エイリアスがたくさんある

また、テストコードを読んでいると

describe "..." do
  describe "..." do
    it "..." do
      ...
    end
  end

  context "..." do
    it "..." do
    end
  end
end

みたいな同じ文法の位置に違う単語があって、最初は文法的に何か意味があるんだろうか、と思ったんですが、

  • it / specify/ example は同じ意味で1つのテストケースのコードをグループ化するために用いられる
  • describe / context は同じ意味で example をグループ化するために用いられる
  • これらのエイリアスは自然な英語でテストが記述できるように用意されている

そうです。

まとめ

黙って一回チュートリアル。謙虚な気持ち、大事。

現場からは以上です。

慣れてきて覚えたことの話も書こうかと思ったのですが、長くなったので別記事にします。

参考

JUnit 5 User Guide

qiita.com