はじめに
hkob の雑記録の第165回目は、multipart のテストをスキップする stub の作成について解説します。
WebMock における multipart/form-data の制約
NotionRubyMapping では API アクセスをすることなくテストするために WebMock を利用しています。これまでの JSON のやり取りをするテストは問題なくテストできているのですが、今回 multipart/form-data のテストはうまく動きませんでした。WebMock リポジトリでも以下のように issue にもなっています。
これには二つの問題があります。 一つは multipart の boundary にランダムな文字列が入ってしまう問題です。もう一つは JSON が UTF-8 なのに対して、アップロードしたファイルが ASCII-8BIT であるため、テスト比較の際にエンコードエラーになる点です。前者は SecureRandom.hex の stub 化で対応可能でしたが、後者が非常に面倒で対応するのはかなり困難でした。
前回、仮実装で動作することは確認できたので、API のアクセスの部分はメソッドの stub 化でテストを回避することにしました。
method stub の作成
現在の multipart_request の実装部分はこのようになっています。この multipart_client に対する send メソッドが Notion API の呼び出しになります。従来はこの部分を WebMock で対応しようとしていましたが、このメソッド呼び出しを stub 化します。
# @param [String] path # @param [Hash] json # @param [String] fname # @return [Hash] response hash def multipart_request(path, json, fname) raise "Please call `NotionRubyMapping.configure' before using other methods" unless @notion_token content_type = MIME::Types.type_for(fname).first.to_s sleep @wait json_part = Faraday::Multipart::ParamPart.new(json.to_json, "application/json") file_part = Faraday::Multipart::FilePart.new(fname, content_type, File.basename(fname)) response = multipart_client.send(:post) do |request| request.headers["Authorization"] = "Bearer #{@notion_token}" request.headers["content-Type"] = "multipart/form-data" request.path = path request.body = {json: json_part, file: file_part} end p response.body if @debug response.body end
これを受けて、FileUploadObject の初期化部のテストを以下のように記述しました。Singleton である TestConnection のインスタンスが NotionCache を nc として保持しています。そこで、この nc が持つ multipart_client に send が送られた時に、Faraday::Response の mock が返るようにしています。この mock に対して body メソッドを送った時に、curl で取得した JSON が返るようにしています。
# frozen_string_literal: true module NotionRubyMapping RSpec.describe FileUploadObject do tc = TestConnection.instance describe "initialize" do context "with a small file" do let(:fname) { "spec/fixtures/ErSxuLeq.png-medium.png" } let(:id) { TestConnection::FILE_UPLOAD_IMAGE_ID } let(:response) { tc.read_json "upload_file_image" } before do allow(tc.nc.multipart_client).to receive(:send) .and_return(instance_double(Faraday::Response, {body: response})) end subject { described_class.new(fname: fname) } it { expect(subject.id).to eq id } end end end end
これでテストが通過するかと思ったのですが、エラーが起こってしまいました。テスト内で記述しているFILE_UPLOAD_IMAGE_ID は「20cd8e4e98ab81aa973b00b23083c115」のように「-」なしの ID になっているのに対し、JSON に記載されているものは「20cd8e4e-98ab-81aa-973b-00b23083c115」のようになっているためでした。これまで、NotionRubyMapping ではこれらを unique にするために内部では全て「-」なしの ID に変更していましたが、そのことを忘れていたようです。そこで、FileUploadOject の create と single_file_upload のメソッドを以下のように修正しました。NotionCache の Singleton インスタンスに hex_id というメソッドを用意していたので、それを使って id を正規化するようにしています。
# @return [FileUploadObject] def create(payload) nc = NotionRubyMapping::NotionCache.instance response = nc.create_file_upload_request(payload) @id = nc.hex_id response[:id] end def single_file_upload nc = NotionRubyMapping::NotionCache.instance response = nc.file_upload_request @fname, @id return if nc.hex_id(response[:id]) == @id && response[:status] == "uploaded" raise StandardError, "File upload failed: #{response}" end
これでテストをすると以下のように成功しました。
FileUploadObject initialize with a small file is expected to eq "20cd8e4e98ab81aa973b00b23083c115" Finished in 0.01382 seconds (files took 0.49544 seconds to load) 1 example, 0 failures
おわりに
今回は multipart/form-data の WebMock の代わりに、メソッドを stub 化することでテストが通るようにしてみました。明日はこの FileObject を icon に設定する部分をテストおよび実装していきます。