RSpecでsubjectを使用して配列を検査時、配列の長さを取得する

RSpecでsubjectを使用して、配列を

subject { user.errors[:name] }  # user.errors[:name] は配列

このように検査時、配列の長さを取得する方法が分からなかったので調べました。

結論

rspec-itsitsを使用する。

its(:size) { is_expected.to eq 3 }

のように書くとsubjectの検査対象の配列の長さを取得できます。

経緯

モデルのテストを書きました。

modelのコード↓

class User < ApplicationRecord
  validates :nickname, presence: true, length: { maximum: 32 },
                       format: { with: /\A[-0-9a-zA-Z#$%&()._]*\z/,
                                 message: 'には半角英数字と#$%&()-._のみ使用できます。' }
end

テストは以下のようになります。(全て書くと長いので一部のみ抜粋)

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#nickname' do
    context '空白のとき'do
      it 'エラーが出ること' do
        user = User.new(nickname: '')
        user.valid?
        expect(user.errors[:nickname]).to include("can't be blank")
      end
    end

    context '許可される文字の場合' do
      context '英大文字小文字数字#$%&()-._' do
        it 'エラーが出ないこと' do
          user = User.new(nickname: 'ARZatz809(_#-$%.&)')
          user.valid?
          expect(user.errors[:nickname]).to be_blank
        end
      end
    end

    context '許可されない文字の場合' do
      context '許可されていない文字"?"が含まれている場合' do
        it 'エラーが出ること' do
          user = User.new(nickname: 'correct?')
          user.valid?
          expect(user.errors[:nickname]).to include('には半角英数字と#$%&()-._のみ使用できます。')
        end
      end
    end
  end
end

これを綺麗にすると以下のようになります。(参考: 使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」 - Qiita )

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#nickname' do
    subject { user.errors[:nickname] }

    let(:user) { User.new(params) }
    let(:params) { {nickname: nickname} }

    before do
      user.valid?
    end

    context '空白のとき'do
      let(:nickname) { '' }
      it { is_expected.to include("can't be blank") }
    end

    context '許可される文字の場合' do
      context '英大文字小文字数字#$%&()-._' do
        let(:nickname) { 'ARZatz809(_#-$%.&)' }
        it { is_expected.to be_blank }
      end
    end

    context '許可されない文字の場合' do
      context '許可されていない文字"?"が含まれている場合' do
        let(:nickname) { 'correct?' }
        it { is_expected.to include('には半角英数字と#$%&()-._のみ使用できます。') }
      end
    end
  end
end

繰り返しがなくなってすっきりしました。

ここでふと思いました。 このテストコード、対象のエラーが含まれることはチェックできるけど、想定外のエラーが(間違って)含まれることはテストできていないじゃん、と。

ここでの方針としては、

  • includeでなく配列同士の比較を行う
  • エラーの数をチェックする

などが思いつきます。今回は

  • エラーの数をチェックする

方法にします。この方法をとることで、

  • エラーの順番は問わず
  • 想定外のエラーが間違って入っていることを防ぐ

ことができます。

さて、テストの対象はsubjectに書かれています。これのサイズを取得したい。 これにはitsを使用します。

RSpec3からitsはgemに分離されているのでインストールします。

Gemfile

group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'rspec-rails'
  gem 'rspec-its'  # この行を追加
end

bundle installします。

テストを追加します。

its(:size) { is_expected.to eq 1 }のように書くとuser.errors[:nickname].sizeが1かどうかをテストすることができます。

RSpec.describe User, type: :model do
  describe '#nickname' do
    subject {user.errors[:nickname]}

    # 略

    context '空白のとき'do
      let(:nickname) { '' }
      it { is_expected.to include("can't be blank") }
      its(:size) { is_expected.to eq 1 }  # 追加
    end

    context '許可される文字の場合' do
      context '英大文字小文字数字#$%&()-._' do
        let(:nickname) {'ARZatz809(_#-$%.&)'}
        it { is_expected.to be_blank}
      end
    end

    context '許可されない文字の場合' do
      context '許可されていない文字"?"が含まれている場合' do
        let(:nickname) {'correct?'}
        it { is_expected.to include('には半角英数字と#$%&()-._のみ使用できます。')}
        its(:size) { is_expected.to eq 1 }  # 追加
      end
    end
  end
end

以上でsubjectに指定した配列のサイズ(長さ)をテストすることができました。

環境

参考


正規表現間違い探しクイズ その3

正規表現間違い探しクイズシリーズです。

正規表現単体テストを書いている場合でもバグを発見しづらいものです。そのため、自身での検証が欠かせません。 今回は仕事中に見つけたものでなく、個人開発中にネットで見つけた間違いのうち印象的だったものを少し変えて紹介します。

問題編

仕様

  • 行頭及び行末の#記号を取り除くString#strip_hashを定義する
  • #記号は連続している可能性がある
  • #記号がない場合もある

動作イメージ

  • ###本文本文
  • #大切#大切
  • 普通普通

ソースコード

ソースコードです。 今回はRubyで実装します。

class String
  def strip_hash
    sub(/^#*([^#]+)#*$/, '\1')
  end
end

正規表現^#*([^#]+)#*$は、行頭に続き#の0回以上の繰り返し、続いて#以外の文字の繰り返し、#の0回以上の繰り返し、行末、を意味します。subメソッドを使用して、1つ目のグループ([^#]+)を抜き出しています。

f:id:yucatio:20190107194148p:plain

さて、この正規表現には明らかな間違いがあります。どのような間違いでしょうか。また、どのように修正するべきでしょうか。

テスト

以下のテストはパスしています。RSpecで書いています。

describe '#strip_hash' do
  subject { str.strip_hash }

  context '#が含まれているとき' do
    context '行頭に#がある場合' do
      let(:str) { '###文字列' }
      it { is_expected.to eq '文字列' }
    end

    context '行末に#がある場合' do
      let(:str) { '文字列###' }
      it { is_expected.to eq '文字列' }
    end

    context '行頭と行末に#がある場合' do
      let(:str) { '###文字列###' }
      it { is_expected.to eq '文字列' }
    end
  end

  context '#が含まれていないとき' do
    let(:str) { '文字列' }
    it { is_expected.to eq '文字列' }
  end
end

解答編

少し考えてから解答編を見てください

続きを読む

正規表現間違い探しクイズ その2

正規表現間違い探しクイズシリーズです。

正規表現単体テストを書いている場合でもバグを発見しづらいものです。 そのためレビューの時には注意深く見るようにしています。そんな中見つけた間違いのうち印象的だったものを紹介します。

問題編

仕様

Linkモデルにはurlを持ちます。urlは以下の条件を満たします。

  • 入力必須
  • 長さは256文字以下
  • URLの形式を満たす必要がある
  • プロトコルhttpまたはhttpsのみ許可される
  • ドメインwww.example.comまたはblog.example.comのみ許可される

ソースコード

ソースコードです。 今回はRuby on Railsで実装します。

class Link < ApplicationRecord
  validates :url, presence: true, length: {maximum: 256}
  validate :valid_url

  private

  def valid_url
    return unless url

    if ! url?(url) then
      # URLの形式を満たしていない
      errors.add(:url, 'は有効なURLではありません。')
      return
    end

    if ! %r{\Ahttp(s)?://}.match?(url) then
      # httpまたはhttpsで始まっていない
      errors.add(:url, 'のプロトコルはhttpまたはhttpsのみ指定できます。')
    end

    if ! /((www)|(blog))\.example\.com/.match?(url) then
      # ドメインがwww.example.comまたはblog.example.comでない
      errors.add(:url, 'のドメインはwww.example.comまたはblog.example.comである必要があります。')
    end
  end

  def url?(url)
    # 今回の本題ではないので省略
  end
end

さて、上記ソースコードには明らかな間違いがあります。どのような間違いでしょうか。また、どのように修正するべきでしょうか。(ちなみに正規表現のはじめの\Aは文字列の先頭を指します。^と似ていますが、違いは参考のリンクを参照してください)

テスト

以下のテストはパスしています。

require 'rails_helper'

RSpec.describe Link, type: :model do
  describe '#url' do
    subject { link.errors[:url] }

    let(:link) { Link.new(params) }
    let(:params) { {url: url} }

    before do
      link.valid?
    end

    context '許可されるURLの場合' do
      context 'プロトコルがhttpの場合' do
        context 'ドメインがwww.example.comの場合' do
          let(:url) {'http://www.example.com/path/to/something'}
          it { is_expected.to be_blank}
        end
        context 'ドメインがblog.example.comの場合' do
          let(:url) {'http://blog.example.com/path/to/something'}
          it { is_expected.to be_blank}
        end
      end

      context 'プロトコルがhttpsの場合' do
        context 'ドメインがwww.example.comの場合' do
          let(:url) {'https://www.example.com/path/to/something'}
          it { is_expected.to be_blank}
        end
        context 'ドメインがblog.example.comの場合' do
          let(:url) {'https://blog.example.com/path/to/something'}
          it { is_expected.to be_blank}
        end
      end
    end

    context '許可されないURLの場合' do
      context '許可されないプロトコルの場合' do
        let(:url) {'ftp://www.example.com/path/to/something'}
        it { is_expected.to include('のプロトコルはhttpまたはhttpsのみ指定できます。')}
      end

      context '許可されないドメインの場合' do
        let(:url) {'http://www.hatenablog.com/path/to/something'}
        it { is_expected.to include('のドメインはwww.example.comまたはblog.example.comである必要があります。')}
      end
    end
  end
end

RSpec知らない人だと何書いてあるかわからないかと思いますが、重要な部分は、入力がhttp://www.example.com/path/to/somethingだとエラーが出なくて(be_blank)、入力がftp://www.example.com/path/to/somethingだとのプロトコルはhttpまたはhttpsのみ指定できます。というエラーが出るという部分です。

解答編

少し考えてから解答編を見てください

続きを読む

正規表現間違い探しクイズ その1

正規表現間違い探しクイズシリーズです。

正規表現のバグは単体テストを書いている場合でも発見しづらいものです。 そのためレビューの時には注意深く見るようにしています。そんな中見つけた間違いのうち印象的だったものを紹介します。

問題編

仕様

ユーザモデルはニックネームを持ちます。ニックネームは以下の条件を満たします。

  • 入力必須
  • 32文字以下
  • 使用できる文字列は半角英数字と#$%&()-._

ソースコード

ソースコードです。 今回はRuby on Railsで実装します。

class User < ApplicationRecord
  validates :nickname, presence: true, length: { maximum: 32 },
                       format: { with: /\A[0-9a-zA-Z#$%&()-._]*\z/,
                                 message: 'には半角英数字と#$%&()-._のみ使用できます。' }
end

さて、上記ソースコード正規表現(\A[0-9a-zA-Z#$%&()-._]*\z)には明らかな間違いがあります。どのような間違いでしょうか。また、どのように修正するべきでしょうか。(ちなみに正規表現のはじめの\Aは文字列の先頭、\zは文字列の末尾を指します。それぞれ^$と似ていますが、違いは参考のリンクを参照してください)

テスト

以下のテストはパスしています。

require 'rails_helper'

RSpec.describe User, type: :model do
  describe '#nickname' do
    subject { user.errors[:nickname] }

    let(:user) { User.new(params) }
    let(:params) { {nickname: nickname} }

    before do
      user.valid?
    end

    context '許可される文字の場合' do
      context '英大文字小文字数字#$%&()-._' do
        let(:nickname) { 'ARZatz809(_#-$%.&)' }
        it { is_expected.to be_blank }
      end
    end

    context '許可されない文字の場合' do
      context '許可されていない文字"?"と"""が含まれている場合' do
        let(:nickname) { '?baerg"' }
        it { is_expected.to include('には半角英数字と#$%&()-._のみ使用できます。') }
      end
    end
  end
end

RSpec知らない人だと何書いてあるかわからないかと思いますが、重要な部分は、入力がARZatz809(_#-$%.&)だとエラーが出なくて(be_blank)、入力が?baerg"だとには半角英数字と#$%&()-._のみ使用できます。というエラーが出るという部分です。

解答編

少し考えてから解答編を見てください

続きを読む

ソフトウェアのテストを書くのが嫌になる瞬間 第5位

ソフトウェアのテストを書くのが嫌になる瞬間、今回は第5位です。

以下、単に”テスト”と書いた場合は、テストコードのことを指し、CIツールなどで自動実行されるものを指します。また、単に"コード"と書いた場合には、プロダクションコードを指します。 また、単体テストよりもエンドtoエンドのテストを厚く書く職場で働いています。

テストを書くのが嫌になる瞬間 第5位 : テストを書いたにも関わらずリリース後にバグが発見されたとき

辛い。

今回は漫画なしです。

テストを書くとバグが減ると思ってましたが、大して減りません。

バグが発生した原因としては、大きく分けると

  • 自動テストのケース漏れ
  • 手動でしか確認できない部分の漏れ(副作用系)

に分けられます。

リリース後に発見される(大抵はユーザ問い合わせで発覚する)バグで多いのは、"手動でしか確認できない部分の漏れ(副作用系)"です。このバグは自動テスト導入しても(当然ながら)一向に減ることはありません。

手動でしか確認できない部分の漏れ(副作用系)の具体的な内容としては、以下のようなものです。

  • ファイルの書き出しが正しくされていない
  • 他のAPIに通知されるはずが通知されていない
  • メールが送られるはずが送られていない
  • 親子関係のあるデータで、親を削除した時に子のデータも一緒に削除されるはずが、されていない

また、現在システムが複雑化していて、同じ機能を持ったアプリが複数ある状況で、片方で登録した内容がもう片方で正しく表示されない、などの問題もありました。

あとは、これはリリース前に気づいたことですが、運用ログが出力されていないということもありました。(参照する変数が間違っていて毎回ID:nullと出ていたり、文字列だけ組み立てて出力するコードを書き忘れていたり)。

個人的な感想ですが、自動テストを導入してそちらのテストに気を取られて手動でしか確認できないテストに手と気が回らなくなってる気がします。ツールの問題より人間の気力というのが難しいところです。

また、テストが通ること=バグがないこと、と思い、手動テストが必要だという意識がないメンバーもいて、頭を悩ませているところです。 テストを書くことによる恩恵も受けているので、テストを書かなくするという対応策も取れず。

第5位は以上です。その他のソフトウェアのテストを書くのが嫌になる瞬間は以下にまとめています。

yucatio.hatenablog.com

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

テスト駆動開発 [ Kent Beck ]
価格:3024円(税込、送料無料) (2018/12/31時点)


ソフトウェアのテストを書くのが嫌になる瞬間 第4位

ソフトウェアのテストを書くのが嫌になる瞬間、今回は第4位です。

テストを書くのが嫌になる瞬間 第4位 : テストの文脈を見ないで落ちたテストのみ修正しているのをみたとき

今回は10コマ漫画でお送りします。

なお、以下単に”テスト”と書いた場合は、テストコードのことを指し、CIツールなどで自動実行されるものを指します。また、単に"コード"と書いた場合には、プロダクションコードを指します。 また、単体テストよりもエンドtoエンドのテストを厚く書く職場で働いています。

f:id:yucatio:20181230165209p:plain:w300

f:id:yucatio:20181230165731p:plain

f:id:yucatio:20181230164527p:plain:w300

大体こうなるよね。

とにかく落ちたテストしか見ないのです。落ちたテストが書いてあるファイルは見てほしいし、少なくとも前後のテストケースくらいは確認してもらいたいものですが、見ないのです。ちゃんと元のテストコードには

説明 期待値
最小値 - 1 2 エラーが出る
最小値 3 登録できる
最大値 10 登録できる
最大値 + 1 11 エラーが出る

のように説明も書いてあるんですよ(上記だと期待値が適当ですが実際はちゃんと書いてあります)。全然読まれてない。

念のため書いておくと、下記のように直すのが正しいです。

説明 期待値
最小値 - 1 2 エラーが出る
最小値 3 登録できる
最大値 20 登録できる
最大値 + 1 21 エラーが出る

上記漫画の重症例はソフトウェアのテストを書くのが嫌になる瞬間 第1位と同じメンタリティですね。とにかくテストが通ればよいという考えのようです。

誰ですかテストは仕様書だと言ったのは。修正漏れたままマージされてしまったら、

説明 期待値
最小値 - 1 2 エラーが出る
最小値 3 登録できる
最大値 10 登録できる
最大値 + 1 21 エラーが出る

もしくは

説明 期待値
最小値 - 1 2 エラーが出る
最小値 3 登録できる
最大値 10 登録できる
最大値 + 1 11 登録できる

になりますよ。これ、"仕様分からないからソース見なきゃわかんない"に陥るパターンではないですか。

仕様変更での修正漏れは、レビューでもうっかりすると見逃すパターンです。書き換えられた部分は差分に出てくるのですが、書き換え忘れた部分は差分として出てこないからです。

とにかく私の周りでこのパターン多すぎてレビュー来るたびにげんなりします。どうすればよいのですか。教えてえらいひと。

第4位は以上です。その他のソフトウェアのテストを書くのが嫌になる瞬間は以下にまとめています。

yucatio.hatenablog.com

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

テスト駆動開発 [ Kent Beck ]
価格:3024円(税込、送料無料) (2018/12/28時点)


ソフトウェアのテストを書くのが嫌になる瞬間 第3位

ソフトウェアのテストを書くのが嫌になる瞬間、今回は第3位です。

テストを書くのが嫌になる瞬間 第3位 : コードもテストも間違っているのをみたとき

今回は7コマ漫画2本でお送りします。

なお、以下単に”テスト”と書いた場合は、テストコードのことを指し、CIツールなどで自動実行されるものを指します。また、単に"コード"と書いた場合には、プロダクションコードを指します。 また、単体テストよりもエンドtoエンドのテストを厚く書く職場で働いています。

ケース1

f:id:yucatio:20181226144733p:plain:w300

f:id:yucatio:20181226144750p:plain:w300

あると思います。

人間だからミスはします。レビューに出す時点でバグがないことを求めてもしょうがないです。 ただ、直す手間が2重にかかることを考えると悲しい気持ちになります。

ケース2

f:id:yucatio:20181226145525p:plain:w300

f:id:yucatio:20181226145537p:plain:w300

ないと思います。実際にあった話ですが(コード間違っていて項目Fが出力されていなかった)。

テストケース作成の理想と現実

理想としては、テストもコードも仕様から作成するのが望ましいのです。

f:id:yucatio:20181227085748p:plain:w350

しかし、しばしばコードからテストケースの期待値を作成する例が見られます。

f:id:yucatio:20181227085710p:plain:w350

1人の人間がコードとテストを作成すると、仕様を勘違いしたままコードとテストを作成してしまうケースが発生しやすいです。コードを書いた後にテストを書くなら、もう一度新たな気持ちで仕様を読んでほしいですが、それが難しいことは想像に難くないです。

ところでTDDはこうだと思うのですが、

f:id:yucatio:20181227085725p:plain:w350

仕様からテストを作成する時に仕様を勘違いしてしまったらその勘違いに気づくのは結構後のフェーズだと思うのですがどうしているのでしょうか。

TDDは個人開発(仕様を作る人とテスト書く人が同一人物)か、ビジネスの仕様に左右されない部分(共通で使用される関数など)をテストする時に有用だとは思いますが、仕様を作る人とテストを書く人が別の場合、バグを防ぐという目的の達成はあまり期待しないほうがよい気がします。TDDを職場でやったことがないので想像ですが。

第3位は以上です。その他のソフトウェアのテストを書くのが嫌になる瞬間は以下にまとめています。

yucatio.hatenablog.com

[商品価格に関しましては、リンクが作成された時点と現時点で情報が変更されている場合がございます。]

テスト駆動開発 [ Kent Beck ]
価格:3024円(税込、送料無料) (2018/12/26時点)