Recently at ProcurementExpress.com, I had to integrate Digital invoice-matching functionality, we decided to use Mindee, 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 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.
# 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.
# 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 to make API calls, and here is the Mindee client wrapper.
# 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.
# 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?
# 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
# 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:
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.
Integrations::Mindee::Client.stubs(:post).returns(
OpenStruct.new(success?: false, body: { response: { api_request: { error: 'something went wrong' } } }.to_json)
)
Return Success response
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.