概要
主にテスト時に当てることを想定しています
基本はオープンクラスを使う感じにはなると思います
prepend や delegate を使ってもできるかもしれませんが今回はオープンクラスを使った方法を考えます
環境
- macOS 10.15.5
- Ruby 2.7.1p83
サンプルコード
メインのコードです
外部にアクセスすることを想定して作っています
理由は net/http
自体にモンキーパッチを当てるケースがあることを想定しているためです
vim lib/user.rb
require 'net/https'
class User
def initialize(name, age)
@name = name
@age = age
end
def hello
"#{@name},#{@age}"
end
def access
uri = URI.parse 'https://kaka-request-dumper.herokuapp.com/'
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
req = Net::HTTP::Get.new uri.request_uri
res = http.request req
res.body
end
end
テストコード
テストコードを複数のファイルに分けて書く場合を想定しています
メインのテストファイルを書いてあとは test ディレクトリ配下に test_hogehoge.rb
のようなファイルを作成していきます
vim test/run_test.rb
base_dir = File.expand_path(File.join(File.dirname(__FILE__), ".."))
lib_dir = File.join(base_dir, "lib")
test_dir = File.join(base_dir, "test")
$LOAD_PATH.unshift(lib_dir)
require 'test/unit'
exit Test::Unit::AutoRunner.run(true, test_dir)
vim test/test_user.rb
require 'user'
require 'json'
class TestUser < Test::Unit::TestCase
def test_hello
u = User.new("hawksnowlog", 10)
assert_equal("hawksnowlog,5", u.hello)
end
def test_acccess
u = User.new("hawksnowlog", 10)
assert_equal(true, JSON.parse(u.access).has_key?("key"))
# assert_nothing_thrown {
# JSON.parse(u.access)
# }
end
end
実行
すべて失敗することを確認しましょう
ここからモンキーパッチを当ててテストを成功するようにしてみます
ruby test/run_test.rb
パッチを当ててみる (単純にクラスをオープンするだけ)
テストコード内でオープンすればアプリ側に影響はないのでこれが一番単純です
ruby のオープンクラスはグローバルなのですべてのテストに影響する点に注意しましょう
vim test/test_user.rb
require 'user'
require 'json'
class User
def hello
"hawksnowlog,5"
end
def access
'{"key":"value"}'
end
end
class TestUser < Test::Unit::TestCase
def test_hello
u = User.new("hawksnowlog", 10)
assert_equal("hawksnowlog,5", u.hello)
end
def test_acccess
u = User.new("hawksnowlog", 10)
assert_equal(true, JSON.parse(u.access).has_key?("key"))
end
end
net/http にパッチを当てるには
既存のクラスにパッチを当ててみます
愚直にやるのであれば先ほどと同じように既存のクラスを開いてメソッドを再定義すれば OK です
vim test/test_user.rb
require 'user'
require 'json'
class User
def hello
"hawksnowlog,5"
end
end
module Net
class HTTP
def request(req, data = nil)
HTTPResponse.new(nil, nil, nil)
end
end
class HTTPResponse
def body
'{"key":"value"}'
end
end
end
class TestUser < Test::Unit::TestCase
def test_hello
u = User.new("hawksnowlog", 10)
assert_equal("hawksnowlog,5", u.hello)
end
def test_acccess
u = User.new("hawksnowlog", 10)
assert_equal(true, JSON.parse(u.access).has_key?("key"))
end
end
そもそもパッチは別ディレクトリで管理したほうがいい
パッチをテストと同じファイルに書いていましたが管理しやすいように別ディレクトリにしたほうが良いでしょう
また後で紹介しますが alias でパッチを当てる前の古いメソッドを保存しておきます
vim monkeypatch/patch.rb
module Net
class HTTP
alias old_request request
def request(req, data = nil)
HTTPResponse.new(nil, nil, nil)
end
end
class HTTPResponse
alias old_body body
def body
'{"key":"value"}'
end
end
end
そしてこの monkeypatch/
ディレクトリを run_test.rb
でロードしていつでもパッチを当てられるようにしておきます
vim test/run_test.rb
base_dir = File.expand_path(File.join(File.dirname(__FILE__), ".."))
lib_dir = File.join(base_dir, "lib")
patch_dir = File.join(base_dir, "monkeypatch")
test_dir = File.join(base_dir, "test")
$LOAD_PATH.unshift(patch_dir)
$LOAD_PATH.unshift(lib_dir)
require 'test/unit'
exit Test::Unit::AutoRunner.run(true, test_dir)
こうすることで以下のような感じでテスト時にパッチを require することができます
ただ前述の通りオープンクラスはグローバルに影響するためどこで require しても影響はグローバルに及ぶことを認識しておきましょう
vim test/test_user.rb
require 'user'
require 'json'
class TestUser < Test::Unit::TestCase
def test_hello
u = User.new("hawksnowlog", 10)
assert_equal("hawksnowlog,10", u.hello)
end
def test_acccess
# これ以降のテストもパッチは当たった状態になる
require 'patch'
u = User.new("hawksnowlog", 10)
assert_equal(true, JSON.parse(u.access).has_key?("key"))
end
end
パッチを外すには
ではグローバルに当たってしまったパッチを外すにはどうするか考えます
一番簡単そうに実現できそうなのは alias
を使う方法かなと思います
解除する場合を考慮してパッチを当てる前のメソッドを残しておきそれを後からアンパッチします
alias は先程 patch.rb で定義したのでアンパッチ用のファイルを作成します
vim monkeypatch/unpatch.rb
module Net
class HTTP
alias request old_request
end
class HTTPResponse
alias body old_body
end
end
あとはパッチを外したいテストのときに require すれば OK です
vim test/test_user.rb
require 'user'
require 'json'
class TestUser < Test::Unit::TestCase
def test_hello
u = User.new("hawksnowlog", 10)
assert_equal("hawksnowlog,10", u.hello)
end
def test_acccess
require 'patch'
u = User.new("hawksnowlog", 10)
assert_equal(true, JSON.parse(u.access).has_key?("key"))
end
def test_acccess2
require 'unpatch'
u = User.new("hawksnowlog", 10)
assert_equal(true, JSON.parse(u.access).has_key?("body"))
end
end
最後に
Ruby でオープンクラスを使ったモンキーパッチの当て方を考えてみました
基本的にはモックと同じようにテストや開発時のデバッグに使うことが多いかなと思います
オープンクラスを使った場合のポイントはスコープがグローバルになるということをしっかりと理解して使いましょう
またプロダクションのコードで同じようにモンキーパッチを当てるようなことをする場合はアプリ全体に影響を及ぼす可能性があるので注意して使いましょう
実は過去に rspec-mock という rspec 用のモックライブラリの使い方を紹介しています
すでに rspec を使っているのであればこっちを使ったほうがテスト専用なので安心して使えるかなと思います
0 件のコメント:
コメントを投稿