外部サービスとデータ連携する際に、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を使用していますが、別のメソッドでもモック化出来るかもしれません