akishin999の日記

調べた事などを書いて行きます。

FakeFS でファイルアクセスをテストする

ファイルシステムの操作に関連するクラスのモックオブジェクトを提供するライブラリ「FakeFS」を使ってみたので、使い方をメモしておきます。

defunkt/fakefs
https://github.com/defunkt/fakefs

インストール

% gem install fakefs --no-rdoc --no-ri

Rails を使う場合は Gemfile に以下のように追加します。

group :test do
  gem 'fakefs', :require => "fakefs/safe"
end

使ってみる

FakeFS を require すると以下のクラスが FakeFS が提供するクラスに差替えられます。

  • File
  • FileTest
  • FileUtils
  • Dir
  • Pathname

そのため、これらのクラスを使用してファイルシステムにアクセスするコードを、実際のファイルシステムに変更を加える事なくテストする事ができます。

まずは「fakefs」を require して使ってみます。

# -*- coding: utf-8 -*-

require 'fakefs'

TEST_DIR = '/temp/example'

# ディレクトリの作成
# 全てのパスがモック化されているため、中間パスが実在する場合でも「Errno::ENOENT」になる
# そのためフルパスで指定する場合は mkdir_p などの中間パスも作成してくれるメソッドを使う
p FileUtils.mkdir_p TEST_DIR
#=> ["/temp/example"]

# ディレクトリの削除
# この行が無くても実際のパスにディレクトリは作成されない
# また、削除自体もエラーにはならず成功する
p Dir.rmdir TEST_DIR
#=> true

「require 'fakefs'」した場合、FakeFS はその時点でファイルシステム関連のクラスを差替えてしまいます。
使用しているコード内(ライブラリ内も含む)でのあらゆるファイルアクセスをモック化して問題ない場合以外では、この挙動は少々微妙かも知れません。

そういった場合には「fakefs/safe」の方を require します。
「fakefs/safe」を require した場合のサンプルコードは以下のようになります。

# -*- coding: utf-8 -*-

require 'fakefs/safe'

TEST_DIR  = '/temp/'
TEST_FILE = '/temp/example.txt'

# FakeFS 有効
FakeFS.activate!

# ファイルシステムを操作する処理
Dir::mkdir TEST_DIR
File.open(TEST_FILE, 'w') { |f| f.write 'Hello, World.' }

# FakeFS 無効
FakeFS.deactivate!

上記のように、明示的に FakeFS.activate! を呼ばない限り FakeFS が有効になりません。
また、 FakeFS.deactivate! を呼ぶ事で通常のファイルアクセスが可能になるよう、挙動を戻す事も出来ます。

他にも、以下のようにブロックを渡すと、ブロック内でのみ FakeFS が有効になります。

# -*- coding: utf-8 -*-

require 'fakefs/safe'

TEST_DIR  = '/temp/'
TEST_FILE = '/temp/example.txt'

FakeFS do
  Dir::mkdir TEST_DIR
  File.open(TEST_FILE, 'w') { |f| f.write 'Hello, World.' }
end

こんな感じでとても簡単に使用する事ができるので、既存のテストコードに導入するのも容易だと思います。

RSpec で使う

FakeFS では RSpec と一緒に使うための spec_helpers.rb というファイルも提供されています。
簡単に試してみるため、まずは RSpec を使ったサンプルプロジェクトを用意してみました。

ファイル配置は以下のような感じ。

exampleapp
 ├── Rakefile
 ├── example.rb
 └── spec
     ├── example_spec.rb
     └── spec_helper.rb

Rakefile と spec_helper.rb は以下のような内容で用意しました。

# -*- coding: utf-8 -*-
require "rspec/core/rake_task"

desc 'rake spec'
task :default => [:spec]

RSpec::Core::RakeTask.new(:spec) do |spec|
  spec.pattern = 'spec/*_spec.rb'
  spec.rspec_opts = ['-cfd --backtrace']
end
  • spec_helper.rb
# -*- coding: utf-8 -*-
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'rspec'

RSpec.configure do |config|
end

次にテスト対象クラスです。
若干無理矢理感溢れてますが、適当なものを思いつかなかった為、テスト対象とする Example クラスは以下のような内容とします。

  • example.rb
# -*- coding: utf-8 -*-

class Example
  LOG_FILE = 'example.log'

  def self.create_dir(dir)
    FileUtils::mkdir_p dir
  end

  def self.log(message)
    File.open(LOG_FILE, 'w') { |f| f.write message }
  end
end

spec はこんな感じで適当に。

  • example_spec.rb
# -*- coding: utf-8 -*-
require_relative 'spec_helper'
require_relative '../example'

describe Example do
  describe ".create_dir" do
    subject { Example }
    before  { 
      subject.create_dir("testdir")
    }

    it "ディレクトリが作成されること" do
      expect(File.exists? "testdir").to be_true
    end
  end

  describe ".log" do
    subject { Example }
    before  { 
      subject.log("Hello, World.")
    }

    it "ログファイルが作成されること" do
      expect(File.exists? Example::LOG_FILE).to be_true
    end
  end
end

ここで試しに rake spec してみると、カレントディレクトリ内に「testdir」ディレクトリと「example.log」というファイルが作成されてしまいます。
テストの度にこういったファイルやディレクトリが作成されてしまうのは煩わしいですよね。

という訳で、毎回後始末をするのも面倒なので FakeFS の出番になります。
example_spec.rb に以下のように二行追加します。

  • example_spec.rb
# -*- coding: utf-8 -*-
require_relative 'spec_helper'
require 'fakefs/spec_helpers'    # <= 追加
require_relative '../example'

describe Example do
  include FakeFS::SpecHelpers    # <= 追加
  describe ".create_dir" do
    subject { Example }
    before  { 
      subject.create_dir("testdir")
    }

    it "ディレクトリが作成されること" do
      expect(File.exists? "testdir").to be_true
    end
  end

  describe ".log" do
    subject { Example }
    before  { 
      subject.log("Hello, World.")
    }

    it "ログファイルが作成されること" do
      expect(File.exists? Example::LOG_FILE).to be_true
    end
  end
end

これでこの spec 内で FakeFS が有効になります。
今度はテストを実行してもディレクトリやファイルが作成されません。

ただ、もし複数の spec ファイルでファイルシステムを扱っているような場合、全部のファイルで上の二行を追加するのは面倒です。
そういった場合は、以下のように spec_helper.rb に記述する事で、全ての spec ファイルで FakeFS を有効にすることができます。

  • spec_helper.rb
# -*- coding: utf-8 -*-
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'rspec'
require 'fakefs/spec_helpers'           # <= 追加

RSpec.configure do |config|
  config.include FakeFS::SpecHelpers    # <= 追加
end

ファイルシステムを操作するようなクラスのテストは結構面倒だと思っていたのですが、FakeFS を使うと余り色々な事を気にせずにテストが書けるのでいい感じですね。

参考

Ruby - FakefsでTest-Drivenな運用スクリプト - Qiita [キータ]
http://qiita.com/sawanoboly/items/de2b14779796179ef0d1