行動すれば次の現実

テック中心の個人ブログ

FTPに配置されたファイルを取り込む処理をRSpecでテストする方法

外部サービスとデータ連携する際に、FTPに配置されたファイルをローカルにダウンロードして、データを取り込むような処理を実装するケースは少なくないと思います。

当該処理のユニットテストを実装する場合、毎回リアルなFTPサーバーを使ってテストするのは現実的ではありません。 本記事では、FTP通信の実装例とモックを使用したRSpecのテストコードを紹介したいと思います。

net/ftpをラップしたFtpClientクラスを用意

net/ftpはRuby標準のFTPライブラリです。そのまま使うには自由度が高すぎるので、専用のラッパークラスを介してnet/ftpを扱うようにします。

下記クラスはあくまでもサンプルなので作りが甘い部分があります。実際にはエラーハンドリングなどを考慮する必要があります。

lib/utils/ftp_client.rb

require 'net/ftp'

class FtpClient

  def initialize(host, user, password)
    @host = host
    @user = user
    @password = password
  end

  def connect
    @ftp = Net::FTP.new
    @ftp.connect(@host)
    @ftp.login(@user, @password)
  end

  def close
    @ftp.close
  end

  def copy_to_local(remote_filepath, local_filepath)
    @ftp.get(remote_filepath, local_filepath)
  end
end

FtpClientの使用例

FtpClientの使用例は以下の通りです。 ファイル取込用のサービスクラス(ImportDataSerice)で、FtpClientを使用する例を記します。

app/services/import_data_service.rb

class ImportDataSerice

  def call
    # ログイン
    # 実際にはログイン情報は環境変数などで渡したほうが良いです
    ftp_client = FtpClient.new('111.111.111.111', 'ftp-user', 'password')
    # 接続開始
    ftp_client.connect
    # ファイルダウンロード
    ftp_client.copy_to_local('ftp_dir/data.csv', 'tmp/data.csv')
    # 接続終了
    ftp_client.close

    # 以下に取込処理を実装する
  end
end

RSpecの実装例

ImportDataSericeをRSpecでテストしていきます。

FtpClientを実際に接続することを避けるためにFtpClientの各メソッドをallow_any_instance_ofでモック化します。 *1

connectとcloseメソッドは単純にtrueを返す実装に変更させます。

copy_to_localメソッドは、spec/data/services/import_data_service/data.csvにあるテストデータをtmp/data.csvにコピーするという処理に置き換えます。

こうすることで、ImportDataSericeの取込処理では、tmp/data.csvに配置された状態で動作することになります。

テストデータはFTPにアップロードすることなく、ローカルのspec/data/services/import_data_service/data.csvを直接編集すれば良いので楽にテストすることが出来ます。

require 'rails_helper'

RSpec.describe ImportDataService, type: :service do
  describe 'call' do
    example 'ファイルが取り込まれること' do
        allow_any_instance_of(FtpClient).to receive(:connect) do
          true
        end
        allow_any_instance_of(FtpClient).to receive(:close) do
          true
        end
        allow_any_instance_of(FtpClient).to receive(:copy_to_local) do
          FileUtils.cp('spec/data/services/import_data_service/data.csv', 'tmp/data.csv')
        end

        ImportDataService.new.call
        # 以下で取込結果を検証していく
    end
  end
end

*1:※ allow_any_instance_ofを使用していますが、別のメソッドでもモック化出来るかもしれません