Ruby 3.2でERBテンプレートのカバレッジが測れるようになるぞ!! (1) eval, ERB 編
Photo by Tobias Rademacher on Unsplash
2022/09/22, 29 にruby 本体の master branch へ Code Coverage に関する面白い変更が入りました。具体的には今まで取れなかったeval内のCode Coverageが取れるようになったようです。
eval内と聞くとピンとこないかもしれませんが、実はERBテンプレートやHamlテンプレートはevalを使っているため、この変更の恩恵を受けることができます。
今回はそれらを実際に検証してみます。
前提知識
Code Coverage
Code Coverage とはどのコードが実行されたかの記録です。よくあるユースケースとしてテスト網羅性の指標(Test Coverage)への利用が挙げられます。またそれ以外でも一部の言語で実装されている oneshot coverage は不要なコードの検出(dead code)に役立ちます。
Ruby はこの Code Coverage 機能を Coverage という標準クラスで提供しています。
eval 内の Code Coverage
現在、前述のCoverageクラスはeval内のコードのCoverageを測定することができません。
ruby 3.1.0 で eval プログラムのCoverageを取ってみます。
1 def foo(n)
2 if n <= 10
3 p "n < 10"
4 else
5 p "n >= 10"
6 end
7 end
8
9 eval <<~RUBY, nil, __FILE__, __LINE__ + 1
10 def bar(n)
11 if n <= 10
12 p "n < 10"
13 else
14 p "n >= 10"
15 end
16 end
17 RUBY
18
19 foo(1)
20 foo(2)
21 bar(1)
22 bar(2)
require "coverage"
Coverage.start(lines: true)
load "lib/foo.rb"
p Coverage.result
# 実行結果
"n < 10"
"n < 10"
"n < 10"
"n < 10"
{"lib/boo.rb"=>{:lines=>[1, 2, 2, nil, 0, nil, nil, nil, 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 1, 1]}}
# 10~17行目までが取れていない
Rails の View について
Rails の View Template(ERB/HAML/...) はレンダリング時に内部的に eval を利用しています。
また、そこでも前節で述べたような eval 内の Coverage の性質により View Template 内の Coverage が測定できない、という問題が存在しています。
概要
今週 2022/09/22 に ruby 本体にCoverageに関する面白い変更がマージされました。
- 2022/9/22 https://github.com/ruby/ruby/pull/6396
- 2022/9/29 https://github.com/ruby/ruby/commit/9dd902b83186ad6f9d0a553da2ca114bac6ab7b5
この変更によりRubyのCoverageクラスにおいて eval 内もCoverage測定できるようになりました。
- 出来るようになったこと
- Ruby の eval 内での Coverage 測定できるようになった
- 考えられる有力なユースケース
- Rails で View Coverage が測定できるようになる
そこで今回は eval, ERB のカバレッジ測定を実際に検証しながら、最終的に実用的なユースケースとして Rails アプリケーションで View Template の Coverage が取れることを確認します。
検証
今回の検証では最新 master branch の ruby で、以下の4点を実際に確かめます。
- eval 内の Coverage が取れること (本記事)
- ERB テンプレート内の Coverage が取れること (本記事)
- Rails アプリケーションで View Coverage が取れること (次記事)
- ERB
- HAML
なお本記事では 1, 2 の紹介を行い、3 については次記事で詳しく説明しようと思います。
事前準備
今回の検証では比較のため ver 3.1.0
と ver 2022-09-28 master
のそれぞれで Coverage 測定を行います。
- ver 3.1.0
$ ruby -v
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [arm64-darwin21]
- ver 2022-09-28 master
$ ruby -v
ruby 3.2.0dev (2022-09-28T20:44:14Z master e7ddb6b182) [aarch64-linux]
なお ver 2022-09-28 master
は Building Ruby を参考に自身でビルドしています。
1. eval 内の Coverageが取れること
はじめに冒頭でも触れたシンプルな eval プ ロ グラムです。
利用するコード
1 def foo(n)
2 if n <= 10
3 p "n < 10"
4 else
5 p "n >= 10"
6 end
7 end
8
9 eval <<~RUBY, nil, __FILE__, __LINE__ + 1
10 def bar(n)
11 if n <= 10
12 p "n < 10"
13 else
14 p "n >= 10" #
15 end
16 end
17 RUBY
18
19 foo(1)
20 foo(2)
21 bar(1)
22 bar(2)
require "coverage"
Coverage.start(lines: true, eval: true)
load "lib/foo.rb"
p Coverage.result
9dd902b の変更により、Coverage Setup 時に eval: true
を指定すると eval 内の Coverage が取れるようになっています。
実行結果
- ver 3.1.0
"n < 10"
"n < 10"
"n < 10"
"n < 10"
{"lib/boo.rb"=>{:lines=>[1, 2, 2, nil, 0, nil, nil, nil, 1, nil, nil, nil, nil, nil, nil, nil, nil, nil, 1, 1, 1, 1]}}
# 10~17行目までが取れていない
- ver 2022-09-28 master
"n < 10"
"n < 10"
"n < 10"
"n < 10"
{"lib/boo.rb"=>{:lines=>[1, 2, 2, nil, 0, nil, nil, nil, 1, 1, 2, 2, nil, 0, nil, nil, nil, nil, 1, 1, 1, 1]}}
# 10~17行目までが取れている
ver 3.1.0 と比較して
ver 2022-09-28 master
では、10, 11, 12行目の Coverage が取得できていることがわかります。
2. ERBテンプレート内のCoverageが取れること
続いてERBクラスを利用したプログラムでCoverageが取れるかどうかを確認してみます。ERBクラスはテンプレートのRender時にevalを利用しているので、検証1と同様にERBテンプレート内のCoverageが取れることが予想されます。
利用するコード
https://github.com/ruby/erb#ruby-in-html のソースコードを利用
require "erb"
# Build template data class.
class Product
def initialize( code, name, desc, cost )
@code = code
@name = name
@desc = desc
@cost = cost
@features = [ ]
end
def add_feature( feature )
@features << feature
end
# Support templating of member data.
def get_binding
binding
end
# ...
end
# Create template.
template = %{
<html>
<head><title>Ruby Toys -- <%= @name %></title></head>
<body>
<h1><%= @name %> (<%= @code %>)</h1>
<p><%= @desc %></p>
<ul>
<% @features.each do |f| %>
<li><b><%= f %></b></li>
<% end %>
</ul>
<p>
<% if @cost < 10 %>
<b>Only <%= @cost %>!!!</b>
<% else %>
Call for a price, today!
<% end %>
</p>
</body>
</html>
}.gsub(/^ /, '')
rhtml = ERB.new(template)
# Set up template data.
toy = Product.new( "TZ-1002",
"Rubysapien",
"Geek's Best Friend! Responds to Ruby commands...",
999.95 )
toy.add_feature("Listens for verbal commands in the Ruby language!")
toy.add_feature("Ignores Perl, Java, and all C variants.")
toy.add_feature("Karate-Chop Action!!!")
toy.add_feature("Matz signature on left leg.")
toy.add_feature("Gem studded eyes... Rubies, of course!")
# Produce result.
rhtml.run(toy.get_binding)
require "coverage"
Coverage.start(oneshot_lines: true, eval: true)
load "lib/erb.rb"
p Coverage.result
実行結果
- ver 3.1.0
{"lib/erb.rb"=>{:oneshot_lines=>[3, 6, 7, 16, 21, 29, 55, 58, 8, 9, 10, 11, 13, 62, 17, 63, 64, 65,
66, 69, 22]}, "/usr/lib/ruby/2.7.0/erb.rb"=>{:oneshot_lines=>[15, 258, 259, 262, 269, 340, 341, 342, 345,
346, 349, 350, 351, 352, 355, 358, 362, 367, 368, 369, 375, 376, 378, 381, 382, 401, 413, 426, 435, 449,
471, 489, 490, 495, 359, 498, 501, 502, 513, 353, 515, 516, 536, 539, 540, 550, 552, 556, 562, 572, 576,
582, 605, 629, 642, 659, 688, 694, 701, 704, 707, 710, 713, 715, 718, 720, 736, 744, 809, 830, 831, 832,
833, 838, 843, 846, 850, 854, 871, 881, 889, 901, 910, 922, 932, 941, 958, 977, 986, 988, 989, 1002,
1005,1006, 1007, 1021, 1026, 1027, 1028, 1034, 1063, 1064, 1067, 1077, 811, 814, 818, 823, 839, 695, 660,
666, 696, 697, 698, 699, 824, 882, 883, 884, 885, 825, 583, 584, 585, 586, 721, 722, 723, 733, 587, 541,
542, 543, 544, 545, 546, 547, 553, 589, 590, 689, 363, 364, 370, 371, 372, 373, 591, 503, 504, 505, 506,
507, 508, 592, 593, 594, 595, 606, 625, 509, 615, 616, 573, 617, 597, 630, 638, 632, 643, 653, 577, 633,
634, 645, 650, 600, 601, 563, 564, 565, 567, 568, 602, 826, 827, 828, 890, 902, 905]},
"/usr/lib/ruby/2.7.0/cgi/util.rb"=>{:oneshot_lines=>[2, 3, 4, 5, 7, 8, 12, 22, 30, 41, 58, 65, 122, 125,
140, 160, 172, 175, 178, 181, 187, 211, 222]}}
- ver 2022-09-28 master
{"lib/erb.rb"=>{:oneshot_lines=>[3, 6, 7, 16, 21, 29, 55, 58, 8, 9, 10, 11, 13, 62, 17, 63, 64, 65, 66, 69,
22]}, "/root/.rubies/ruby-master/lib/ruby/3.2.0+2/erb.rb"=>{:oneshot_lines=>[15, 16, 259, 260, 261, 264,
271, 342, 343, 344, 347, 348, 351, 352, 353, 354, 357, 360, 364, 369, 370, 371, 377, 378, 380, 383, 384,
403, 415, 428, 437, 451, 473, 491, 492, 497, 361, 500, 503, 504, 515, 355, 517, 518, 538, 541, 542, 552,
554, 558, 564, 574, 578, 584, 607, 631, 644, 661, 690, 696, 703, 706, 709, 712, 715, 717, 720, 722, 738,
746, 811, 832, 833, 838, 843, 846, 850, 854, 871, 881, 889, 901, 910, 922, 932, 941, 958, 977, 986, 988,
989, 1002, 1005, 1006, 1007, 1021, 1026, 1027, 1028, 1034, 1063, 1064, 1067, 1077, 813, 816, 820, 825, 839,
697, 662, 668, 698, 699, 700, 701, 826, 882, 883, 884, 885, 827, 585, 586, 587, 588, 723, 724, 725, 735,
589, 543, 544, 545, 546, 547, 548, 549, 555, 591, 592, 691, 365, 366, 372, 373, 374, 375, 593, 505, 506,
507, 508, 509, 510, 594, 595, 596, 597, 608, 627, 511, 617, 618, 575, 619, 599, 632, 640, 634, 645, 655,
579, 635, 636, 647, 652, 602, 603, 565, 566, 567, 569, 570, 604, 828, 829, 830, 890, 902, 905]},
"/root/.rubies/ruby-master/lib/ruby/3.2.0+2/cgi/util.rb"=>{:oneshot_lines=>[2, 3, 4, 5, 7, 8, 14, 27, 41,
53, 63, 74, 94, 101, 160, 163, 178, 198, 210, 213, 219, 240, 251]}, "/root/.rubies/ruby-
master/lib/ruby/3.2.0+2/erb/version.rb"=>{:oneshot_lines=>[2, 3, 4]},
"(erb)"=>{:oneshot_lines=>[1, 3, 6, 7, 10, 10, 11, 12, 16, 18, 20, 25]
少しわかりにくいですが、ver 3.1.0 と比較して ver 2022-09-28 master
では、ERB テンプレート部のCoverageが取れていることがわかります。
# ERB テンプレート部のCoverage
"(erb)"=>{:oneshot_lines=>[1, 3, 6, 7, 10, 10, 11, 12, 16, 18, 20, 25]
本記事では ERB での検証のみを行いましたが、次記事では HAML を用いた検証も行おうと思います。
まとめ
この記事では最新の ruby ビルド環境で eval, ERB のカバレッジ測定を実際に検証しました。両者ともに今まで取れなかったCoverageが取れるようになっていました。
次の記事では実用的なユースケースとして Rails アプリケーションで View Template の Coverage が取れることを確認します。またその際には simplecov での検証も同時に行うことを考えています。