こんにちは。Tomoyuki(@tomoyuki65)です。
「Rubyの入門は一通り学んだけど、この後何を学べばいいのかな?」
って方いるんじゃないでしょうか。
今回はそんな方向けに、テストコードを書きながらクラスを作って遊んでみたいと思います。
特にWeb業界のプログラマであれば、テストコードを書くのは必須なので覚えましょう。
この記事では、クラスをより実践的な開発方法で作りながら、クラスやオブジェクトの関係性について解説します。
目次
テストコードとは?
テストコードとは、書いたプログラムのロジックが、想定通りの動きをしているかを確認するためのプログラムです。
例えば以下のメソッドがあったとします。
# 引数xとyを足した値を返す def x_add_y(x, y) x + y end
この場合は、メソッドの引数xに1、yに2を渡した場合の戻り値が「3」であればOKということを、コードベースで確認します。
また、テストコードは書いたプログラムの仕様についての側面も満たしており、開発者が想定しているパターンのテストが反映されています。
つまり、テストコードは開発者が想定しているコードの使われ方を表しています。
テストコードを書くメリット・デメリット
テストコードを書くことには、メリットやデメリットがあります。
よって、開発現場で必ず書いているものかといえばそうではありません。
「Web開発の仕事しているけど、今まで書いたことないよ」
って方も中にはいるでしょう。
ただし、Web開発の現場では必ず書いた方がいいと覚えておきましょう。
メリット
テストコードを書くメリットはいくつかありますが、以下の点が挙げられます。
- リファクタリングしやすい
- 特定の条件で起こる不具合をテストコード上で再現させて確認できる
- テストコードを実行することで、どんな機能か概ねわかる
尚、リファクタリングというのは、プログラムの外部から見た動作を変えずに、ソースコードの内部を整理することです。
中のロジックが変わらなければ、結果は同じになるようにプログラムを書くことで、内部変化に気付きやすくなります。(バグを見つけやすい)
また、ソフトウェアの開発が終わって保守している際に、機能改修などでプログラムの中身を綺麗に修正したいということが多々あります。
ただし、本番環境で動いているプログラムを修正するのはリスクが高く、修正後の検証も大変なため基本的には行いません。
(機能改修などで一緒に修正したいコードがあっても、その必要がなければ修正しないことがザラです。)
しかし、テストコードを書いていれば、テストコードの結果が変わらなければ、ロジックが変わっていないことを比較的容易に確認することが可能となります。
特に、仕様変更が多々あるようなWeb開発の現場では、リファクタリングがしやすいのは大きなメリットになるため、必ず書くべきでしょう。
デメリット
デメリットとしては、以下の点が挙げられます。
- テストコードを書くのは、開発と同じくらい時間がかかる
- メンテナンスをし続けないとゴミになる
時間がかかるだけならいいですが、それはつまり開発コストが上がるということです。
プロジェクトの内容次第では、逆にテストコードを書かない方がいい場合もあるでしょう。
まあ一番のデメリットは、テストコードを書くのは圧倒的にめんどくさいということでが。。
プログラミングの準備
今回はテキストエディタとしてAtomを使ってプログラミングをしていきます。
テキストエディタやIDEは好きなもを使っていただければ良いですが、同じようにプログラミングをしたい方はAtomを使って下さい。
Atomには、以下のパッケージを導入して使います。
- script(Atom上でRubyプログラムを実行)
- git-plus(Atom上でGitコマンドを実行)
また、rbenvによるRubyのバージョンの設定と、Gitを使いながらプログラミングをしていきます。
rbenvやGitがよくわからない方は、以下の記事を参考にしてみて下さい。
ディレクトリの作成とRubyのバージョン設定
今回は「たこ焼き」を題材に、『たこ焼きクラス』を作成してみます。
まずはディレクトリ「takoyaki-box」を作成し、移動します。
$ mkdir takoyaki-box
$ cd takoyaki-box
次にrbenvを使い、Rubyのバージョンを指定します。
今回Rubyのバージョンは、「2.4.2」を使います。
$ rbenv local 2.4.2
次にGitを初期化し、ファイル「.ruby-version」をコミットします。
$ git init
$ git add .
$ git commit -m “Initial commit”
Atomを使って開発を始める
ここからはAtomを使って開発していきます。
ターミナルからAtomを起動します。
※Atom上でscriptパッケージを使用する場合は、ターミナルからAtomを起動して下さい。ターミナルから起動すると環境変数を引き継げるため、日本語による文字化けを防げます。
$ atom .
Atomが起動したら新規ファイルを追加します。
Projectメニューで右クリックして下さい。
メニューの「新規ファイル」をクリックします。
ファイル名を「takoyaki.rb」と入力し、Enterを押します。
新規ファイルが追加されたらOKです。
では実際にコードを書いていきます。
たこ焼きクラスの作成
まずは、たこ焼きクラスを作成します。
たこ焼きクラスには、トッピングやソースを表すインスタンス変数を持たせます。
# たこ焼きクラスを作成
class Takoyaki
attr_reader :topping, :sauce
# たこ焼きのトッピングとソースをパラメータとして持たせる
def initialize(topping, sauce)
@topping = topping
@sauce = sauce
end
end
クラスが作成できたら、オブジェクトを作って確認してみましょう。
今回は2種類のたこ焼きオブジェクトを作成します。
# たこ焼きクラスを作成
class Takoyaki
attr_reader :topping, :sauce
# たこ焼きのトッピングとソースをパラメータとして持たせる
def initialize(topping, sauce)
@topping = topping
@sauce = sauce
end
end
# 一つ目のたこ焼きを作成
takoyaki_1 = Takoyaki.new('かつお節', 'オリジナルソース')
# putsでトッピングとソースを確認
puts takoyaki_1.topping
puts takoyaki_1.sauce
# 二つ目のたこ焼きを作成
takoyaki_2 = Takoyaki.new('ねぎ', 'ポン酢')
# putsでトッピングとソースを確認
puts takoyaki_2.topping
puts takoyaki_2.sauce
Atomのscriptパッケージでプログラムを実行する場合は、ショートカットキー「command + i」で実行できます。
また、ターミナルからプログラムを実行する場合は以下の通りです。
$ ruby taiyaki.rb
かつお節
オリジナルソース
ねぎ
ポン酢
尚、Atomでプログラムを実行した場合は、上記の通りです。
テストコードを書く
ではここからテストコードを書いて確認していきます。
今回はRuby標準のテスティングフレームワークである「Minitest」を使用します。
プログラムを以下のように修正して下さい。
# minitestを読み込み
require 'minitest/autorun'
# たこ焼きクラスを作成
class Takoyaki
attr_reader :topping, :sauce
# たこ焼きのトッピングとソースをパラメータとして持たせる
def initialize(topping, sauce)
@topping = topping
@sauce = sauce
end
end
# テストコードを書く
class TakoyakiTest < Minitest::Test
def test_taiyaki
# 一つ目のたこ焼きを作成
takoyaki_1 = Takoyaki.new('かつお節', 'オリジナルソース')
# 一つ目のたこ焼きを検証する
assert_equal 'かつお節', takoyaki_1.topping
assert_equal 'オリジナルソース', takoyaki_1.sauce
# 二つ目のたこ焼きを作成
takoyaki_2 = Takoyaki.new('ねぎ', 'ポン酢')
# 二つ目のたこ焼きを検証する
assert_equal 'ねぎ', takoyaki_2.topping
assert_equal 'ポン酢', takoyaki_2.sauce
end
end
テストコードを書くには、「Minitest::Test」を継承したクラスを用意し、test_xxxというメソッドを定義するとそのメソッドがテストの実行対象となります。
また、「assert_equal 期待値, 検証値」で実行結果を検証します。(アサーションと呼びます)
期待値と検証値が一致すればテストがパスし、一致しない場合はテストが失敗します。
test_xxxというメソッドはクラス内に複数書くこともでき、1つのテストメソッド内に「assert_equal」を複数書くのもOKです。
ただし、原則として1テストメソッドに対して1アサーションとするのが望ましいです。
では修正したプログラムを実行してみましょう。
テストは全てパスするので、実行結果は以下のようになります。
Run options: –seed 2411
# Running:
.
Finished in 0.000691s, 1447.1777 runs/s, 5788.7108 assertions/s.
1 runs, 4 assertions, 0 failures, 0 errors, 0 skips
[Finished in 0.232s]
作成したプログラムをGitでコミットする
ここまでで基本的なたこ焼きクラスを作成しました。
この後さらに機能を追加してきますが、その前にGitにコミットしておきましょう。
Atomのgit-plusパッケージでGitを使うには、ショートカットキー「shift + command + a」でAdd、ショートカットキー「shift + command + c」でコミットできます。
または、Atomのメニューにあるパッケージから「Git Plus」を選択し、「Add + Commit」でコミットまで同時に実行することができます。
コミットを実行すると、コミットメッセージの入力が求められるので、今回は「Add takoyaki class」と入力して保存(command + s)します。
また、ターミナルからコミットしたい場合は、以下の通りです。
$ git add .
$ git commit -m “Add takoyaki class”
Gitの履歴を確認したい場合は?
Gitでコミットした履歴を確認することが可能です。
Atomのメニューにあるパッケージから「Git Plus」を選択し、「Log」をクリックします。
コミットした履歴が表示されます。
履歴をクリックすると、プログラムの更新内容の確認が可能です。
たこ焼きクラスに「個数」を持たせる
次はたこ焼きクラスに、たこ焼きの個数を持たせてみます。
まずは以下のようにテストコードを追加し、テストが失敗することを確認します。
今回一つ目と二つ目のたこ焼きの個数は、それぞれ6個、12個とします。
# minitestを読み込み
require 'minitest/autorun'
# たこ焼きクラスを作成
class Takoyaki
attr_reader :topping, :sauce
# たこ焼きのトッピングとソースをパラメータとして持たせる
def initialize(topping, sauce)
@topping = topping
@sauce = sauce
end
end
# テストコードを書く
class TakoyakiTest < Minitest::Test
def test_taiyaki
# 一つ目のたこ焼きを作成
takoyaki_1 = Takoyaki.new('かつお節', 'オリジナルソース', 6)
# 一つ目のたこ焼きを検証する
assert_equal 'かつお節', takoyaki_1.topping
assert_equal 'オリジナルソース', takoyaki_1.sauce
# 一つ目のたこ焼きはの個数は6個であることを検証する
assert_equal 6, takoyaki_1.quantity
# 二つ目のたこ焼きを作成
takoyaki_2 = Takoyaki.new('ねぎ', 'ポン酢', 12)
# 二つ目のたこ焼きを検証する
assert_equal 'ねぎ', takoyaki_2.topping
assert_equal 'ポン酢', takoyaki_2.sauce
# 二つ目のたこ焼きはの個数は12個であることを検証する
assert_equal 12, takoyaki_2.quantity
end
end
実行結果は以下の通りです。
Run options: –seed 38344
# Running:
E
Finished in 0.000822s, 1216.5451 runs/s, 0.0000 assertions/s.
1) Error:
TakoyakiTest#test_taiyaki:
ArgumentError: wrong number of arguments (given 3, expected 2)
/Users/あなたのユーザ名/takoyaki-box/takoyaki.rb:9:in `initialize’
/Users/あなたのユーザ名/takoyaki-box/takoyaki.rb:19:in `new’
/Users/あなたのユーザ名/takoyaki-box/takoyaki.rb:19:in `test_taiyaki’
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
[Finished in 0.187s]
実行結果にある「/Users/あなたのユーザ名/takoyaki-box/takoyaki.rb:9:in `initialize’」のように、エラー個所が表示されるのでテストがパスするように修正していきます。
では、たこ焼きクラスを以下の通り修正して下さい。
# minitestを読み込み
require 'minitest/autorun'
# たこ焼きクラスを作成
class Takoyaki
attr_reader :topping, :sauce, :quantity
# たこ焼きのトッピングとソースをパラメータとして持たせる
def initialize(topping, sauce, quantity)
@topping = topping
@sauce = sauce
@quantity = quantity
end
end
# テストコードを書く
class TakoyakiTest < Minitest::Test
def test_taiyaki
# 一つ目のたこ焼きを作成
takoyaki_1 = Takoyaki.new('かつお節', 'オリジナルソース', 6)
# 一つ目のたこ焼きを検証する
assert_equal 'かつお節', takoyaki_1.topping
assert_equal 'オリジナルソース', takoyaki_1.sauce
# 一つ目のたこ焼きはの個数は6個であることを検証する
assert_equal 6, takoyaki_1.quantity
# 二つ目のたこ焼きを作成
takoyaki_2 = Takoyaki.new('ねぎ', 'ポン酢', 12)
# 二つ目のたこ焼きを検証する
assert_equal 'ねぎ', takoyaki_2.topping
assert_equal 'ポン酢', takoyaki_2.sauce
# 二つ目のたこ焼きはの個数は12個であることを検証する
assert_equal 12, takoyaki_2.quantity
end
end
以下のように、プログラムを修正後にテストがパスすればOKです。
Run options: –seed 10126
# Running:
.
Finished in 0.000693s, 1443.0011 runs/s, 8658.0067 assertions/s.
1 runs, 6 assertions, 0 failures, 0 errors, 0 skips
[Finished in 0.202s]
このように、テストコードを書きながら開発する方法は「テスト駆動開発」とも呼ばれ、以下のようなサイクルで開発していきます。
- 失敗するテストを書く
- できる限り早く、テストに通るような最小限のコードを書く
- コードの重複を除去する(リファクタリング)
自分の状態を文字列として返せるようにする
次はたこ焼きに保持されているトッピングやソース、そして先ほど追加した個数などの状態を、文字列として返せるようにしてみます。
まずは以下のように、putsでオブジェクトを確認します。
# minitestを読み込み
require 'minitest/autorun'
# たこ焼きクラスを作成
class Takoyaki
attr_reader :topping, :sauce, :quantity
# たこ焼きのトッピングとソースをパラメータとして持たせる
def initialize(topping, sauce, quantity)
@topping = topping
@sauce = sauce
@quantity = quantity
end
end
# テストコードを書く
class TakoyakiTest < Minitest::Test
def test_taiyaki
# 一つ目のたこ焼きを作成
takoyaki_1 = Takoyaki.new('かつお節', 'オリジナルソース', 6)
# 一つ目のたこ焼きを検証する
assert_equal 'かつお節', takoyaki_1.topping
assert_equal 'オリジナルソース', takoyaki_1.sauce
# 一つ目のたこ焼きはの個数は6個であることを検証する
assert_equal 6, takoyaki_1.quantity
# putsでオブジェクトを確認
puts takoyaki_1
# 二つ目のたこ焼きを作成
takoyaki_2 = Takoyaki.new('ねぎ', 'ポン酢', 12)
# 二つ目のたこ焼きを検証する
assert_equal 'ねぎ', takoyaki_2.topping
assert_equal 'ポン酢', takoyaki_2.sauce
# 二つ目のたこ焼きはの個数は12個であることを検証する
assert_equal 12, takoyaki_2.quantity
# putsでオブジェクトを確認
puts takoyaki_2
end
end
実行結果は以下の通りです。
Run options: –seed 57463
# Running:
#<Takoyaki:0x00007fb66208e218>
#<Takoyaki:0x00007fb66208dca0>
.
Finished in 0.000674s, 1483.6795 runs/s, 8902.0770 assertions/s.
1 runs, 6 assertions, 0 failures, 0 errors, 0 skips
[Finished in 0.203s]
実行結果の通り、オブジェクトの出力結果が「#<Takoyaki:0x00007fb66208e218>」のように表示されたのは、オブジェクトのto_sメソッドを返しているからです。
よって、to_sメソッドをオーバーライドし、自分の状態を文字列として返せるように修正します。
ではテストコードを以下の通り修正して下さい。
# minitestを読み込み
require 'minitest/autorun'
# たこ焼きクラスを作成
class Takoyaki
attr_reader :topping, :sauce, :quantity
# たこ焼きのトッピングとソースをパラメータとして持たせる
def initialize(topping, sauce, quantity)
@topping = topping
@sauce = sauce
@quantity = quantity
end
end
# テストコードを書く
class TakoyakiTest < Minitest::Test
def test_taiyaki
# 一つ目のたこ焼きを作成
takoyaki_1 = Takoyaki.new('かつお節', 'オリジナルソース', 6)
# 一つ目のたこ焼きを検証する
assert_equal 'かつお節', takoyaki_1.topping
assert_equal 'オリジナルソース', takoyaki_1.sauce
# 一つ目のたこ焼きはの個数は6個であることを検証する
assert_equal 6, takoyaki_1.quantity
# 自分の状態を文字列で確認できることを検証する
assert_equal 'トッピング: かつお節, ソース: オリジナルソース, 個数: 6', takoyaki_1.to_s
# 二つ目のたこ焼きを作成
takoyaki_2 = Takoyaki.new('ねぎ', 'ポン酢', 12)
# 二つ目のたこ焼きを検証する
assert_equal 'ねぎ', takoyaki_2.topping
assert_equal 'ポン酢', takoyaki_2.sauce
# 二つ目のたこ焼きはの個数は12個であることを検証する
assert_equal 12, takoyaki_2.quantity
# 自分の状態を文字列で確認できることを検証する
assert_equal 'トッピング: ねぎ, ソース: ポン酢, 個数: 12', takoyaki_2.to_s
end
end
ではテストが失敗することを確認し、テストがパスするように修正してみて下さい。
テストがパスしたら、コミットメッセージを「Add quantity and to_s method to takoyaki」とし、Gitにコミットして下さい。
解答例
たこ焼きクラスに以下のメソッドを追加すると、テストがパスします。
# 自分の状態を文字列として返す
def to_s
“トッピング: #{topping}, ソース: #{sauce}, 個数: #{quantity}”
end
たこ焼きクラスに値段を持たせる
次はたこ焼きクラスに値段を持たせてみます。
値段の計算には、たこ焼きクラスにpriceメソッドを追加する想定とします。
たこ焼きの値段は以下のルールで計算し、まずはテストコードを追加して下さい。
- たこ焼き1個の値段が70円
- トッピングが「ねぎ」の場合、+20円
- ソースが「オリジナルソース」以外の場合、+10円
テストコードの解答例
# 一つ目のたこ焼きの値段を検証する
assert_equal 420, takoyaki_1.price
# 二つ目のたこ焼きの値段を検証する
assert_equal 870, takoyaki_2.price
値段を計算するpriceメソッドの追加
テストが失敗するのを確認し、たこ焼きクラスに値段を計算するpriceメソッドを追加して下さい。
解答例
たこ焼きクラスに以下のメソッドを追加すると、テストがパスします。
# 値段を計算して返す
def price
# 1個の値段と個数から値段を計算
amount = 70 * quantity
# トッピングがねぎの場合
if topping == ‘ねぎ’
amount += 20
end
# ソースがオリジナルソース以外の場合
if sauce != ‘オリジナルソース’
amount += 10
end
amount
end
自分の状態を返すメソッドを修正
自分の状態を文字列で返すときに、追加した値段も確認できるように修正します。
まずは、自分の状態を文字列で確認するテストコードを以下のように修正して下さい。
# 自分の状態を文字列で確認できることを検証する
assert_equal ‘トッピング: かつお節, ソース: オリジナルソース, 個数: 6, 値段: 420円‘, takoyaki_1.to_s
# 自分の状態を文字列で確認できることを検証する
assert_equal ‘トッピング: ねぎ, ソース: ポン酢, 個数: 12, 値段: 870円‘, takoyaki_2.to_s
テストが失敗することを確認し、テストがパスするようにプログラムを修正して下さい。
テストがパスしたら、コミットメッセージを「Add price method to takoyaki」とし、Gitにコミットして下さい。
最終的なプログラムの解答例
今回作成したプログラムは、最終的に以下のようになりました。
# minitestを読み込み
require 'minitest/autorun'
# たこ焼きクラスを作成
class Takoyaki
attr_reader :topping, :sauce, :quantity
# たこ焼きのトッピングとソースをパラメータとして持たせる
def initialize(topping, sauce, quantity)
@topping = topping
@sauce = sauce
@quantity = quantity
end
# 値段を計算して返す
def price
# 1個の値段と個数から値段を計算
amount = 70 * quantity
# トッピングがねぎの場合
if topping == 'ねぎ'
amount += 20
end
# ソースがオリジナルソース以外の場合
if sauce != 'オリジナルソース'
amount += 10
end
amount
end
# 自分の状態を文字列として返す
def to_s
"トッピング: #{topping}, ソース: #{sauce}, 個数: #{quantity}, 値段: #{price}円"
end
end
# テストコードを書く
class TakoyakiTest < Minitest::Test
def test_taiyaki
# 一つ目のたこ焼きを作成
takoyaki_1 = Takoyaki.new('かつお節', 'オリジナルソース', 6)
# 一つ目のたこ焼きを検証する
assert_equal 'かつお節', takoyaki_1.topping
assert_equal 'オリジナルソース', takoyaki_1.sauce
# 一つ目のたこ焼きはの個数は6個であることを検証する
assert_equal 6, takoyaki_1.quantity
# 自分の状態を文字列で確認できることを検証する
assert_equal 'トッピング: かつお節, ソース: オリジナルソース, 個数: 6, 値段: 420円', takoyaki_1.to_s
# 二つ目のたこ焼きを作成
takoyaki_2 = Takoyaki.new('ねぎ', 'ポン酢', 12)
# 二つ目のたこ焼きを検証する
assert_equal 'ねぎ', takoyaki_2.topping
assert_equal 'ポン酢', takoyaki_2.sauce
# 二つ目のたこ焼きはの個数は12個であることを検証する
assert_equal 12, takoyaki_2.quantity
# 自分の状態を文字列で確認できることを検証する
assert_equal 'トッピング: ねぎ, ソース: ポン酢, 個数: 12, 値段: 870円', takoyaki_2.to_s
end
end
最後に
今回はたこ焼きを題材としてクラスを作成しました。
クラスについては、実際にある物の状態をインスタンス変数で表したり、値段をインスタンスメソッドで計算しましたね。
クラスの定義は一つですが、newする(オブジェクトを作成する)タイミングで、オブジェクトの状態が変わるのが確認でき、クラスとオブジェクトの関係性について理解を深められたかと思います。
このように、実際にある物をプログラミングで表現するのはなんだか楽しいなと私は感じましたが、いかがだったでしょうか?
今回はいくつかの要素しか持たせませんでしたが、例えば製造日や賞味期限など他の要素を加えれば、色々遊べるので試してみて下さい。
また、テストコードやGitを使った開発方法を行ったことで、実際の現場で開発するイメージも少しは付けられたかと思います。
とはいえまだまだ初歩的な部分なので、これからも楽しみながらプログラミング技術の向上を目指しましょう!
- 関連記事
Tomoyuki
最新記事 by Tomoyuki (全て見る)
- Go言語(Golang)専門の技術ブログ「Golang-Tech」を開設!? - 2024年9月26日
- 【2024年】モンハンワイルズの推奨スペックとおすすめのゲーミングPCを紹介! - 2024年9月26日
- 37歳Web系エンジニア3年目。生成AI(ChatGPT・Gemini)現る。 - 2024年7月3日
コメントを残す