# Using Mocha to stub external APIs with minitest.

Recently at ProcurementExpress.com, I had to integrate Digital invoice-matching functionality, we decided to use [Mindee](https://mindee.com), Which has an accurate and lightning-fast document parsing API.

Here is the implementation we have done with Ruby on Rails. One small note, they already have [https://github.com/mindee/mindee-api-ruby](https://github.com/mindee/mindee-api-ruby) SDK for ruby on rails, but because our ruby version was not supported by SDK, I had to write a wrapper for it.

Here is a Ruby class I created for Mindee Document-based prediction API.

InvoiceFields is a Concern that extracts different required fields from Mindee's response.

```ruby
# app/services/integrations/mindee/invoice_fields.rb

module Integrations
  module Mindee
    module InvoiceFields
      extend ActiveSupport::Concern

      def date
        value_of(:date)
      end

      def due_date
        value_of(:due_date)
      end

      def invoice_number
        value_of(:invoice_number)
      end

      def supplier_name
        value_of(:supplier_name)
      end
    end
  end
end
```

ConfidenceScore is another concern that returns the Average confidence score from line items.

```ruby
# app/services/integrations/mindee/confidence_score.rb

module Integrations
  module Mindee
    module ConfidenceScore
      extend ActiveSupport::Concern

      # Confidence score of the average of the confidence scores of the line items
      def confidence_score
        return 0 if line_items.count.zero?

        sum_confidence = line_items.map { |item| item[:confidence] }.sum
        sum_confidence / line_items.length
      rescue StandardError => e
        puts "Error calculating confidence score: #{e.message}"
        0
      end
    end
  end
end
```

We are using [HTTParty](https://github.com/jnunemaker/httparty) to make API calls, and here is the Mindee client wrapper.

```ruby
# app/services/integrations/mindee/client.rb

module Integrations
  module Mindee
    class Client
      include HTTParty
      base_uri 'https://api.mindee.net/v1'
    end
  end
end
```

And finally, here is the `predict_document.rb` service class that makes an actual call to the API service.

```ruby
# app/services/integrations/mindee/predict_document.rb

module Integrations
  module Mindee
    class PredictDocument
      # concerns
      include Integrations::Mindee::InvoiceFields
      include Integrations::Mindee::ConfidenceScore

      # attributes
      attr_reader :file, :error, :success, :response

      def initialize(file:)
        @file = file
        @error = nil
        @success = nil
        @token = ENV.fetch('MINDEE_API_KEY')
      end

      # Extract the invoice data from the file using the Mindee API
      def call
        return unless file.present?

        response = client.post(
          '/products/mindee/financial_document/v1/predict',
          body: { document: file },
          headers: {
            Authorization: "Token #{@token}"
          }
        )
        body = JSON.parse(response.body).with_indifferent_access

        @error =  !response.success?
        @success = response.success?
        @response = body
      end

      def line_items
        prediction[:line_items] || []
      end

      private

      def client
        @client ||= Integrations::Mindee::Client
      end

      def prediction
        @response.dig(:document, :inference, :prediction) || {}
      end

      def value_of(key)
        pred = prediction[key]

        if pred&.is_a?(Array)
          pred.first[:value]
        elsif pred&.is_a?(Hash)
          pred[:value]
        end
      end
    end
  end
end
```

# How to Test the given service class using Mocha?

```ruby

# Gemfile

group :test do
  gem 'mocha'
end
```

Generally, I like to make one valid API call to the service and put the actual response in the JSON file inside the test fixture, so that we can re-use that JSON in different test cases.

> NOTE: Below JSON file does not contains valid JSON response

```ruby
# test/fixtures/mindee_response.json

{
  "api_request": {
    ... 
  },
  "document": {
    "inference": {
      "extras": {},
      "finished_at": "2023-07-04T09:56:03.893829",
      "is_rotation_applied": true,
      "pages": [
        {
         ...   
        "line_items": [
          {
            "confidence": 0.99,
            "description": "test",
            "page_id": 0,
            "polygon": [],
            "product_code": "1",
            "quantity": 1.0,
            "tax_amount": null,
            "tax_rate": 0.0,
            "total_amount": 12.0,
            "unit_price": 12.0
          }
        ],
        "locale": {
          "confidence": 0.71,
          "currency": "EUR",
          "language": "en"
        },
        "reference_numbers": [
          {
            "confidence": 1.0,
            "page_id": 0,
            "polygon": [],
            "value": "#2534212-2023-07-03"
          }
        ],
        ...
       "started_at": "2023-07-04T09:56:02.755287"
    },
    "n_pages": 1,
    "name": "test.pdf"
  }
}
```

And finally, here is the mini test example:

```ruby
require 'test_helper'

module Integrations
  module Mindee
    class PredictDocumentTest < ActiveSupport::TestCase
      setup do
        @response = File.read('test/files/mindee_response.json')
        @subject = Integrations::Mindee::PredictDocument
      end

      test 'should return error if response is not success' do
        Integrations::Mindee::Client.stubs(:post).returns(
          OpenStruct.new(success?: false, body: { response: { api_request: { error: 'something went wrong' } } }.to_json)
        )
        service = @subject.new(file: 'file')
        service.call
        assert service.error
      end

      test 'should return success if response is success' do
        assert stub_and_call_service.success
      end

      test 'should return the confidence score' do
        assert_equal 0.99, stub_and_call_service.confidence_score
      end

      test 'should return the invoice date' do
        assert_equal '2023-07-03', stub_and_call_service.date
      end

      test 'should return invoice items' do
        items = stub_and_call_service.line_items
        assert_equal 1, items.size
        assert_equal 'test', items.first[:description]
        assert_equal 1, items.first[:quantity]
        assert_equal 0, items.first[:tax_rate]
        assert_equal 12, items.first[:unit_price]
        assert_equal 12, items.first[:total_amount]
      end

      ...
    end
  end
end

def stub_and_call_service
  Integrations::Mindee::Client
    .stubs(:post)
    .returns(OpenStruct.new(success?: true, body: @response))
  service = @subject.new(file: 'file')
  service.call
  service
end
```

We can use `stubs` method to stub the `post` action from HTTParty and return our custom success or failed response. Also note that, we are directly stubbing `Integrations::Mindee::Client` service class and not the `Integrations::Mindee::PredictDocument`. That way when we call `.call` method it will stub the response and still allow us to test rest of the logic from the `.call` method, and that means, we can test both `success` and `failure` response logic.

**Return Error response.**

```ruby
Integrations::Mindee::Client.stubs(:post).returns(
  OpenStruct.new(success?: false, body: { response: { api_request: { error: 'something went wrong' } } }.to_json)
)
```

**Return Success response**

```ruby
Integrations::Mindee::Client.stubs(:post).returns(OpenStruct.new(success?: true, body: @response))
service = @subject.new(file: 'file')
```

Through this blog post, we have explored the key features of Mocha and demonstrated how it can be integrated with minitest to create robust test suites. We discussed the process of mocking and stubbing external APIs, enabling developers to isolate their code and test it in isolation from external factors. This approach not only enhances the reliability of tests but also accelerates development cycles by reducing the reliance on external services during the testing phase.
