NotionRubyMapping のアップデート(10) : hkob の雑記録 (165)

はじめに

hkob の雑記録の第165回目は、multipart のテストをスキップする stub の作成について解説します。

WebMock における multipart/form-data の制約

NotionRubyMapping では API アクセスをすることなくテストするために WebMock を利用しています。これまでの JSON のやり取りをするテストは問題なくテストできているのですが、今回 multipart/form-data のテストはうまく動きませんでした。WebMock リポジトリでも以下のように issue にもなっています。

github.com

これには二つの問題があります。 一つは 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 に設定する部分をテストおよび実装していきます。

hkob.notion.site