TL;DR(長いので先に結論)
こんなハッシュがあった場合、
# optionsの中身
{:title=>{:text=>nil},
# 略...
:yAxis=>
[{:min=>300,
:max=>800,
:tickInterval=>100,
:title=>
{:text=>"My Data 1", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}},
{:title=>
{:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:opposite=>true}],
:tooltip=>{:enabled=>true},
# 略...
:setStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}
普通だったらこういうふうにテストするけど、
it '"My Data 1"と"My Data 2"の2種類を出力する' do
options = do_something
y_axis = options[:yAxis]
expect(y_axis.size).to eq 2
expect(y_axis[0][:title][:text]).to eq 'My Data 1'
expect(y_axis[1][:title][:text]).to eq 'My Data 2'
expect(y_axis[1][:opposite]).to be true
end
パターンマッチを使えば、1行で検証できちゃいます!!
it '"My Data 1"と"My Data 2"の2種類を出力する' do
options = do_something
options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
end
詳細は本文にて👇👇👇
はじめに
この記事は「ソニックガーデン プログラマ - Qiita Advent Calendar 2024」17日目の記事です。
ところで、とある開発案件で、lazy_high_charts というgemを使ってチャート表示しているシステムがあります。
チャートを表示させるためにはさまざまなパラメータを作成してライブラリに渡す必要があります。
# copied from https://github.com/michelson/lazy_high_charts
@chart = LazyHighCharts::HighChart.new('graph') do |f|
f.title(text: "Population vs GDP For 5 Big Countries [2009]")
f.xAxis(categories: ["United States", "Japan", "China", "Germany", "France"])
f.series(name: "GDP in Billions", yAxis: 0, data: [14119, 5068, 4985, 3339, 2656])
f.series(name: "Population in Millions", yAxis: 1, data: [310, 127, 1340, 81, 65])
f.yAxis [
{title: {text: "GDP in Billions", margin: 70} },
{title: {text: "Population in Millions"}, opposite: true},
]
f.legend(align: 'right', verticalAlign: 'top', y: 75, x: -50, layout: 'vertical')
f.chart({defaultSeriesType: "column"})
end
渡した値はoptions
やseries
といったメソッドで取得できます。
# @chart.options の戻り値
{:title=>{:text=>"Population vs GDP For 5 Big Countries [2009]"},
:legend=>
{:align=>"right", :verticalAlign=>"top", :y=>75, :x=>-50, :layout=>"vertical"},
:xAxis=>{:categories=>["United States", "Japan", "China", "Germany", "France"]},
:yAxis=>
[{:title=>{:text=>"GDP in Billions", :margin=>70}},
{:title=>{:text=>"Population in Millions"}, :opposite=>true}],
:tooltip=>{:enabled=>true},
:credits=>{:enabled=>false},
:plotOptions=>{:areaspline=>{}},
:chart=>{:defaultSeriesType=>"column"},
:subtitle=>{}}
# @chart.series の戻り値
[{:name=>"GDP in Billions", :yAxis=>0, :data=>[14119, 5068, 4985, 3339, 2656]},
{:name=>"Population in Millions", :yAxis=>1, :data=>[310, 127, 1340, 81, 65]}]
上のサンプルコードだけでもそこそこ複雑なハッシュですが、実務ではさらにあれこれパラメータを渡さないといけません。
このデータをテストコード上でどう検証するか、というのがこの記事の主題です。
確認した動作環境
- Ruby 3.3.3
- rspec-core 3.13.0
実務で実際に使ったデータ(に近いもの)
上の例はlazy_high_chartsのREADMEから抜粋したものでしたが、実務で使ったデータはもうちょっとややこしいです。
たとえばoptionsはこんな感じ。
{:title=>{:text=>nil},
:legend=>
{:enabled=>true,
:vertical_align=>"middle",
:align=>"right",
:layout=>"vertical",
:item_style=>{:font_size=>12},
:item_margin_top=>10,
:item_margin_bottom=>10},
:xAxis=>
{:type=>"datetime",
:dateTimeLabelFormats=>
{:day=>"%Y/%m/%d",
:week=>"%Y/%m/%d",
:month=>"%Y/%m",
:year=>"%Y"},
:title=>{:text=>nil},
:labels=>{:style=>{:fontSize=>16}, :rotation=>-45},
:tickmark_placement=>"on"},
# :yAxisの中身をテストしたい
:yAxis=>
[{:min=>300,
:max=>800,
:tickInterval=>100,
:title=>
{:text=>"My Data 1", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}},
{:title=>
{:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:opposite=>true}],
:tooltip=>{:enabled=>true},
:credits=>{:enabled=>false},
:plotOptions=>{:series=>{:connectNulls=>true}},
:chart=>{:margin_top=>20},
:subtitle=>{},
:setStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}
:yAxis
の中のハッシュは、条件によっては"My Data 1"がなく、"My Data 2"だけを表示するケースがあります。
このとき:opposite
の値がtrueからfalseに変わります。
{:title=>{:text=>nil},
:legend=>
{:enabled=>true,
:vertical_align=>"middle",
:align=>"right",
:layout=>"vertical",
:item_style=>{:font_size=>12},
:item_margin_top=>10,
:item_margin_bottom=>10},
:xAxis=>
{:type=>"datetime",
:dateTimeLabelFormats=>
{:day=>"%Y/%m/%d",
:week=>"%Y/%m/%d",
:month=>"%Y/%m",
:year=>"%Y"},
:title=>{:text=>nil},
:labels=>{:style=>{:fontSize=>16}, :rotation=>-45},
:tickmark_placement=>"on"},
# My Data 1がなく、My Data 2のケースもある。このとき :opposite は false になる
:yAxis=>
[{:title=>
{:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:opposite=>false}],
:tooltip=>{:enabled=>true},
:credits=>{:enabled=>false},
:plotOptions=>{:series=>{:connectNulls=>true}},
:chart=>{:margin_top=>20},
:subtitle=>{},
:setStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}
一方、seriesはこんな感じです。
[{:type=>"line",
:name=>"My Data 1",
:data=>
[[1697932800000, 392],
[1700352000000, 530],
[1702771200000, 585],
[1705190400000, 388],
[1707609600000, 558],
[1710028800000, 432],
[1712448000000, 756],
[1714867200000, 479],
[1717286400000, 527],
[1719705600000, 523],
[1722124800000, 785],
[1724544000000, 376],
[1726963200000, 676],
[1729382400000, 781]]
:yAxis=>0,
:color=>"#013676",
:zIndex=>2},
{:type=>"line",
:name=>"My Data 2",
:data=>
[[1697932800000, 76],
[1700352000000, 76],
[1702771200000, 77],
[1705190400000, 74],
[1707609600000, 76],
[1710028800000, 76],
[1712448000000, 75],
[1714867200000, 76],
[1717286400000, 77],
[1719705600000, 76],
[1722124800000, 75],
[1724544000000, 74],
[1726963200000, 77],
[1729382400000, 77]],
:yAxis=>1,
:color=>"#ACC0D6",
:zIndex=>1}]
こちらもやはり"My Data 1"がなく、"My Data 2"だけになるケースがあります。
この場合、:yAxis
は1ではなく0になります。
# My Data 1がなく、My Data 2だけのケース。このとき :yAxis は 0 になる
[{:type=>"line",
:name=>"My Data 2",
:data=>
[[1697932800000, 76],
[1700352000000, 76],
[1702771200000, 77],
[1705190400000, 74],
[1707609600000, 76],
[1710028800000, 76],
[1712448000000, 75],
[1714867200000, 76],
[1717286400000, 77],
[1719705600000, 76],
[1722124800000, 75],
[1724544000000, 74],
[1726963200000, 77],
[1729382400000, 77]],
:yAxis=>0,
:color=>"#ACC0D6",
:zIndex=>1}]
愚直に検証する場合
optionsにはたくさんデータが含まれていますが、プログラムで動的に変更しているのは:yAxis
の部分だけなのでここが検証の対象になります。
:yAxis
について具体的に検証したいのは以下のような内容です。
- 通常の表示なら"My Data 1"と"My Data 2"の2種類を出力する。このとき、"My Data 2"には
:opposite=>true
のオプションが付く - "My Data 1"を表示しないケースでは、"My Data 2"だけを出力する。このとき、"My Data 2"には
:opposite=>false
のオプションが付く
# 通常の表示
:yAxis=>
[{:min=>300,
:max=>800,
:tickInterval=>100,
:title=>
{:text=>"My Data 1", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}},
{:title=>
{:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:opposite=>true}],
# My Data 1を表示しないケース
:yAxis=>
[{:title=>
{:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}},
:opposite=>false}],
これを愚直にRSpecで検証するとしたらこんな感じでしょうか。
context '通常の表示' do
it '"My Data 1"と"My Data 2"の2種類を出力する' do
options = do_something
y_axis = options[:yAxis]
expect(y_axis.size).to eq 2
expect(y_axis[0][:title][:text]).to eq 'My Data 1'
expect(y_axis[1][:title][:text]).to eq 'My Data 2'
expect(y_axis[1][:opposite]).to be true
end
end
context '"My Data 1"を表示しないケース' do
it '"My Data 2”だけを出力する' do
options = do_something
y_axis = options[:yAxis]
expect(y_axis.size).to eq 1
expect(y_axis[0][:title][:text]).to eq 'My Data 2'
expect(y_axis[0][:opposite]).to be false
end
end
まあ、別にこれでもいいと言えばいいんですが、ちょっと泥臭い感じが否めません。
1行パターンマッチを使うとこうなる
そこでRuby 3.0から導入された1行パターンマッチを使って書き換えてみましょう。
先ほどのテストはこんなふうになります。
context '通常の表示' do
it '"My Data 1"と"My Data 2"の2種類を出力する' do
options = do_something
options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
end
end
context '"My Data 1"を表示しないケース' do
it '"My Data 2”だけを出力する' do
options = do_something
options => {yAxis: [{title: {text: 'My Data 2'}, opposite: false}]}
end
end
なんと、それぞれ検証が1行で済んでしまいました!
パターンマッチの文法解説
とはいえ、パターンマッチには不慣れな人も多いと思うので簡単に文法を説明していきましょう。
まず大枠は以下のようになっています。
options => {...}
これはcase/inを使わない、1行パターンマッチの構文です。
optionsがハッシュであることを検証しています。(optionsがハッシュ以外ならエラー)
さらに、{}
の中身はこうなっています。
options => {yAxis: [...]}
これはoptionsに:yAxis
というキーが含まれ、その値が配列になっていることを検証しています。
このとき、optionsには:yAxis
以外のキーが含まれていても構いません。
配列の中身はこうなっています。
options => {yAxis: [{...}, {...}]}
これは要素が2個あり、どちらもハッシュであることを検証しています。
配列の要素は2個ぴったりじゃないとエラーになります。
1個目の要素の中身はこうなっています。
options => {yAxis: [{title: {text: 'My Data 1'}, {...}]}
これはハッシュの中に:title
というキーがあり、その値がさらにハッシュであることを検証しています。
そして値のハッシュには:text
というキーがあり、その値が"My Data 1"という文字列であることを検証しています。
ハッシュは部分一致で検証されるため、:title
や:text
以外のキーが検証対象のハッシュに含まれていてもエラーになりません。
一方、2個目の要素の中身はこうなっています。
options => {yAxis: [{...}, {title: {text: 'My Data 2'}, opposite: true}]}
title: {text: 'My Data 2'}
の部分の考え方は先ほどと同じです。
これに加えて、ここではさらにopposite: true
というキーと値が含まれていることも検証しています。
上の説明をまとめるとこうなります。
# optionsは、
# - :yAxisというキーがあり、その値は配列である
# - 配列には2つのハッシュが要素として含まれる
# - 1つめの要素のハッシュは、:titleがキー、その値はtext: 'My Data 1'が含まれるハッシュ
# - 2つめの要素のハッシュは、:titleがキー、その値はtext: 'My Data 2'が含まれるハッシュ
# - さらに2つめの要素のハッシュには、opposite: trueというキーと値も含まれる
options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
ここまで理解できれば、"My Data 1"を表示しないケースのパターンマッチもすぐに理解できるはずです。
以下の説明を読んで、自分の理解が合っているかどうか確認してみてください。
# optionsは、
# - :yAxisというキーがあり、その値は配列である
# - 配列には1つのハッシュが要素として含まれる
# - 1つめの要素のハッシュは、:titleがキー、その値はtext: 'My Data 2'が含まれるハッシュ
# - さらに1つめの要素のハッシュには、opposite: falseというキーと値も含まれる
options => {yAxis: [{title: {text: 'My Data 2'}, opposite: false}]}
もし検証に失敗したらどうなるのか?
optionsの中身が指定したパターンにマッチしない場合は NoMatchingPatternError が発生してテストが落ちます。
ためしに「通常の表示」で使っていた"My Data 1"を"My Data 100"に変えてテストを実行してみましょう。
options = do_something
# わざとMy Data 1をMy Data 100に変えてみる
options => {yAxis: [{title: {text: 'My Data 100'}}, {title: {text: 'My Data 2'}, opposite: true}]}
するとテスト実行時に次のようなエラーが発生します。
NoMatchingPatternError: {:title=>{:text=>nil}, :legend=>{:enabled=>true, :vertical_align=>"middle", :align=>"right", :layout=>"vertical", :item_style=>{:font_size=>12}, :item_margin_top=>10, :item_margin_bottom=>10}, :xAxis=>{:type=>"datetime", :dateTimeLabelFormats=>{:day=>"%Y/%m/%d", :week=>"%Y/%m/%d", :month=>"%Y/%m", :year=>"%Y"}, :title=>{:text=>nil}, :labels=>{:style=>{:fontSize=>16}, :rotation=>-45}, :tickmark_placement=>"on"}, :yAxis=>[{:min=>300, :max=>800, :tickInterval=>100, :title=>{:text=>"My Data 1", :style=>{:fontSize=>16, :color=>"#ACC0D6"}}, :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}}, {:title=>{:text=>"My Data 2", :style=>{:fontSize=>16, :color=>"#ACC0D6"}}, :labels=>{:style=>{:fontSize=>16, :color=>"#ACC0D6"}}, :opposite=>true}], :tooltip=>{:enabled=>true}, :credits=>{:enabled=>false}, :plotOptions=>{:series=>{:connectNulls=>true}}, :chart=>{:margin_top=>20, :renderTo=>"chart-multiple-timeline"}, :subtitle=>{}, :setStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}: "My Data 100" === "My Data 1" does not return true
一番最後に "My Data 100" === "My Data 1" does not return true
というメッセージが見えるので、どのポイントでマッチしなかったのかがわかります。
ですが、残念ながら検証に失敗したときのエラーメッセージは冗長でちょっとわかりづらいですね😅
応用: expectで囲んで検証していることを明示的に示す
先ほどはパターンマッチを使って以下のようなテストコードを書きました。
it '"My Data 1"と"My Data 2"の2種類を出力する' do
options = do_something
options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
end
が、これだとexpectが1つもなく、どこで何を検証しているのかわかりづらいです。
この問題を避けるために、あえてexpect{...}.not_to raise_error
で囲んであげると良いかもしれません。
it '"My Data 1"と"My Data 2"の2種類を出力する' do
options = do_something
expect {
options => {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
}.not_to raise_error
end
この場合、もしパターンマッチに失敗して NoMatchingPatternError が発生した場合は以下のような実行結果になります。
expected no Exception, got #<NoMatchingPatternError: {:title=>{:text=>nil}, :legend=>{:enabled=>true, :vertical_align=>"middle",...etStyle=>{:style=>{:fontFamily=>"Open Sans Condensed"}}}: "My Data 100" === "My Data 1" does not return true> with backtrace:
改行させるのもアリ
1行パターンマッチという名前ですが、=>
の右辺は改行して読みやすくするのもOKです。
options => {
yAxis: [
{title: {text: 'My Data 1'}},
{title: {text: 'My Data 2'}, opposite: true}
]
}
パターンマッチを使いつつ、値を変数にも格納する
続いて、seriesの値も検証してみましょう。
seriesはこんなデータになっていました。
# 通常の表示
[{:type=>"line",
:name=>"My Data 1",
:data=>
[[1697932800000, 392],
[1700352000000, 530],
[1702771200000, 585],
[1705190400000, 388],
[1707609600000, 558],
[1710028800000, 432],
[1712448000000, 756],
[1714867200000, 479],
[1717286400000, 527],
[1719705600000, 523],
[1722124800000, 785],
[1724544000000, 376],
[1726963200000, 676],
[1729382400000, 781]]
:yAxis=>0,
:color=>"#013676",
:zIndex=>2},
{:type=>"line",
:name=>"My Data 2",
:data=>
[[1697932800000, 76],
[1700352000000, 76],
[1702771200000, 77],
[1705190400000, 74],
[1707609600000, 76],
[1710028800000, 76],
[1712448000000, 75],
[1714867200000, 76],
[1717286400000, 77],
[1719705600000, 76],
[1722124800000, 75],
[1724544000000, 74],
[1726963200000, 77],
[1729382400000, 77]],
:yAxis=>1,
:color=>"#ACC0D6",
:zIndex=>1}]
# "My Data 1"を表示しないケース
[{:type=>"line",
:name=>"My Data 2",
:data=>
[[1697932800000, 76],
[1700352000000, 76],
[1702771200000, 77],
[1705190400000, 74],
[1707609600000, 76],
[1710028800000, 76],
[1712448000000, 75],
[1714867200000, 76],
[1717286400000, 77],
[1719705600000, 76],
[1722124800000, 75],
[1724544000000, 74],
[1726963200000, 77],
[1729382400000, 77]],
:yAxis=>0,
:color=>"#ACC0D6",
:zIndex=>1}]
検証したいポイントは以下の通りです。
- 通常の表示では"My Data 1"と"My Data 2"のデータが出力される。このとき、"My Data 2"の
:yAxis
は1になる - "My Data 1"を表示しないケースでは"My Data 2"のデータだけが出力される。このとき、"My Data 2"の
:yAxis
は0になる - "My Data 1"も"My Data 2"も、
:data
には14件のデータ(配列)が含まれる
この内容を検証するために、1行パターンマッチを利用して以下のようなテストコードを書きました。
context '通常の表示' do
it '"My Data 1"と"My Data 2"の2種類を出力する' do
series = do_something
series => [{name: 'My Data 1', data: Array => data_1, yAxis: 0}, {name: 'My Data 2', data: Array => data_2, yAxis: 1}]
expect(data_1.size).to eq 14
expect(data_2.size).to eq 14
end
end
context '"My Data 1"を表示しないケース' do
it '"My Data 2”だけを出力する' do
series = do_something
series => [{name: 'My Data 2', data: Array => data_2, yAxis: 0}]
expect(data_2.size).to eq 14
end
end
基本的にoptionsを検証したときと考え方は同じなのですが、一箇所だけ新しいテクニックを使っています。
それは以下の部分です。
data: Array => data_1
これはdata: Array
と=> data_1
の部分に分かれます。
-
data: Array
では、:data
に対応する値が配列(Array)であることを検証しています -
=> data_1
では、上でマッチした配列をdata_1
というローカル変数に格納しています(as パターンと呼ばれるパターンマッチの記法です)
つまり、data_1
には以下の値が格納されます。
# data_1の中身
[[1697932800000, 392],
[1700352000000, 530],
[1702771200000, 585],
[1705190400000, 388],
[1707609600000, 558],
[1710028800000, 432],
[1712448000000, 756],
[1714867200000, 479],
[1717286400000, 527],
[1719705600000, 523],
[1722124800000, 785],
[1724544000000, 376],
[1726963200000, 676],
[1729382400000, 781]]
同様に、data_2
には以下の値が格納されます。
# data_2の中身
[[1697932800000, 76],
[1700352000000, 76],
[1702771200000, 77],
[1705190400000, 74],
[1707609600000, 76],
[1710028800000, 76],
[1712448000000, 75],
[1714867200000, 76],
[1717286400000, 77],
[1719705600000, 76],
[1722124800000, 75],
[1724544000000, 74],
[1726963200000, 77],
[1729382400000, 77]]
data_1
とdata_2
はただの変数(ただの配列)なので、普通にRSpecで検証できます。
expect(data_1.size).to eq 14
expect(data_2.size).to eq 14
上の例では単純に要素数を検証しただけですが、必要に応じて各要素の中身を詳しく検証することもできます。
また、ローカル変数に格納する必要がなければ=> data_1
や=> data_2
は省略して構いません。
it '"My Data 1"と"My Data 2"の2種類を出力する' do
series = do_something
# dataの値が配列であることを検証する(配列の中身は任意。変数にも格納しない)
series => [{name: 'My Data 1', data: Array, yAxis: 0}, {name: 'My Data 2', data: Array, yAxis: 1}]
end
応用:暗黙的にキーと同名のローカル変数に代入する(ただし条件あり)
先ほどの例で使った以下のコードは、
context '"My Data 1"を表示しないケース' do
it '"My Data 2”だけを出力する' do
series = do_something
series => [{name: 'My Data 2', data: Array => data_2, yAxis: 0}]
expect(data_2.size).to eq 14
end
end
こんなふうに書き替えることもできます。
context '"My Data 1"を表示しないケース' do
it '"My Data 2”だけを出力する' do
series = do_something
series => [{name: 'My Data 2', data:, yAxis: 0}]
expect(data.size).to eq 14
end
end
変わったのは以下の部分です。
data: Array => data_2
# ↓
data:
これにあわせてexpectの部分も変わっています。
expect(data_2.size).to eq 14
# ↓
expect(data.size).to eq 14
ハッシュのパターンマッチで値を省略してキーだけにすると、キーに対応する値がキーと同名のローカル変数に格納されます。
つまり、ここではdata
という変数に14個の配列が格納されます。
ただし、このテクニックは:data
が1回しか登場しない場合に限り有効です。
:data
が2回登場するとローカル変数名が重複してしまうのでエラーが発生します。
# 以下の場合は:dataが2回登場するので、明示的に変数名を指定しないと構文エラー
series => [{name: 'My Data 1', data:, yAxis: 0}, {name: 'My Data 2', data:, yAxis: 1}]
#=> duplicated variable name (SyntaxError)
注意:ハッシュのキーはシンボルであることが必須
パターンマッチでハッシュを扱う場合、ハッシュのキーはシンボルでなければならない点に注意してください。
# これはOK
{ab: 1, cd: 2} => {ab: 1}
# ハッシュのキーが文字列だとマッチしない
{'ab' => 1, 'cd' => 2} => {ab: 1}
#=> {"ab"=>1, "cd"=>2}: key not found: :ab (NoMatchingPatternKeyError)
# パターンマッチのパターンは構文的にシンボルのキーしか書けない
{'ab' => 1, 'cd' => 2} => {'ab' => 1}
#=> syntax error, unexpected terminator, expecting literal content or tSTRING_DBEG or tSTRING_DVAR or tLABEL_END (SyntaxError)
Rails環境であれば symbolize_keys
メソッドでキーをシンボルに変換することができます。
# キーをシンボルに変換しておけばOK
{'ab' => 1, 'cd' => 2}.symbolize_keys => {ab: 1}
おまけ:=>の代わりにinを使う?
1行パターンマッチには2種類の書き方があります。
ひとつはこの記事で紹介した=>
を使う方法で、もうひとつはin
を使う方法です。
foo => {...}
foo in {...}
それぞれ以下のような違いがあります。
-
=>
はマッチに成功するとnil
が返り、失敗するとNoMatchingPatternKeyErrorが発生する -
in
はマッチに成功するとtrueが返り、失敗するとfalseが返る
なので、この記事で紹介したテストはin
を使って以下のように書くこともできます。
it '"My Data 1"と"My Data 2"の2種類を出力する' do
options = do_something
# 1行パターンマッチがtrueを返すことを検証する
expect((
options in {yAxis: [{title: {text: 'My Data 1'}}, {title: {text: 'My Data 2'}, opposite: true}]}
)).to be true
end
in
を使ったパターンマッチはtrue/falseを返すので一見テストコード向きに思えますが、パターンマッチに失敗したときに原因を教えてくれないため(なぜならfalseを返すだけだから)、=>
を使った方が実用的だと思います。
ちなみに、上のコードでは expect(( ... )).to
のように丸かっこが二重になっていますが、これは記述ミスではありません。
in
を使った1行パターンマッチをメソッドの引数に渡すときは、パターンマッチ自体を()
で囲まないと構文エラーになります。
puts([1] in [n])
#=> syntax error, unexpected `in', expecting ')' (SyntaxError)
puts(([1] in [n]))
#=> true
in
を使った1行パターンマッチは、Rubyを長年書いている筆者でも戸惑うようなちょっと変わった挙動になるので要注意です。
# メソッド呼び出しの丸かっこは省略できるが、パターンマッチの丸かっこは省略できない
# (省略すると [1] in [n] より puts [1] が優先的に評価される)
puts ([1] in [n])
#=> true
# 変数に代入するときも丸かっこが必要
# (省略すると [1] in [n] より ret = [1] が優先的に評価される)
ret = ([1] in [n])
puts ret
#=> true
# if文では丸かっこ不要
puts "matched." if [1] in [n]
#=> matched.
以下のissueでも同じような質問が挙がっていました。
まとめ
というわけで、この記事では1行パターンマッチを使って複雑なハッシュの中身を検証するコード例を紹介してみました。
複雑に入り組んだハッシュの中身を検証する場合はここで紹介したパターンマッチのテクニックが使えないか検討してみてください!
それでは明日の「ソニックガーデン プログラマ - Qiita Advent Calendar 2024」もお楽しみに〜
【PR】Rubyのパターンマッチを学ぶならこれ!
Rubyのパターンマッチはうまく使いこなせると非常に強力な武器になるのですが、かなり高機能なぶん、仕様(構文)が複雑です。
拙著「プロを目指す人のためのRuby入門 改訂2版」では、1章ぶんまるごと使ってパターンマッチを詳しく、丁寧に説明しています。
パターンマッチ、名前は知ってるけど全然わからない😣という人は、ぜひ「プロを目指す人のためのRuby入門 改訂2版」を読んでみてください!
ちなみに、パターンマッチが解説されているのは、さくらんぼが2つの「改訂2版」の方なので、間違って第1版を買わないように注意してくださいね!