1
/
5

Ruby 3.2でERBテンプレートのカバレッジが測れるようになるぞ!! (1) eval, ERB 編

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に関する面白い変更がマージされました。

この変更によりRubyのCoverageクラスにおいて eval 内もCoverage測定できるようになりました。

  • 出来るようになったこと
    • Ruby の eval 内での Coverage 測定できるようになった
  • 考えられる有力なユースケース
    • Rails で View Coverage が測定できるようになる

そこで今回は eval, ERB のカバレッジ測定を実際に検証しながら、最終的に実用的なユースケースとして Rails アプリケーションで View Template の Coverage が取れることを確認します。

検証

今回の検証では最新 master branch の ruby で、以下の4点を実際に確かめます。

  1. eval 内の Coverage が取れること (本記事)
  2. ERB テンプレート内の Coverage が取れること (本記事)
  3. Rails アプリケーションで View Coverage が取れること (次記事)
    1. ERB
    2. 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 masterBuilding 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 での検証も同時に行うことを考えています。

Invitation from Wantedly, Inc.
If this story triggered your interest, have a chat with the team?
Wantedly, Inc.'s job postings
1 Likes
1 Likes

Weekly ranking

Show other rankings
Like Sora Ichigo's Story
Let Sora Ichigo's company know you're interested in their content