Testing Generated Binary Files with RSpec

Home » Services » Consulting & Advisory » Testing Generated Binary Files with RSpec

 

RSpec has become the standard framework used to test code written in Ruby. Its extensive library allows developers to isolate, extract, and test specific files within an application in order to decrease bugs and improve overall quality and stability. Although RSpec provides the tools necessary to test virtually every aspect of an application, some developers may not be familiar with certain time-saving techniques that can be applied during the testing process. This article focuses on using RSpec to generate and then test PDF, XLS, or any other binary files from a Rails application.

Testing content extracted from such files is extremely difficult and, in the case of images (i.e., JPG files), essentially impossible. For every single file format (PDF, XLS, etc.), a separate library is needed. Writing tests this way can be very complicated and time-consuming. These files are located beyond code boundaries and are untestable without a library to load the content back into the application. Developers might choose to write a new library for their specific needs, but this task is also difficult. The methods I describe below allow developers to test any binary file without using additional libraries, including JPG images. All that is needed is an “approved” binary file and a unit test that will compare generated content with approved content.

As detailed in this article, testing generated binary files consists of four basic steps:

    1. Write test helper
    2. Implement requested functionality
    3. Generate a fresh file with #update content
    4. Build and test XLS file against generated file

1. Write Test Helper

Here is the code for Rspec helper + test:

Utility Class:

ruby
module BinaryHelper
 def check_content(generated_content, file_path)
approved_contents  = File.open(file_path, 'rb').read
   expect(file_contents).to eq(approved_content)
 end
 def update_content(content, file_path)
   File.open(file_path, 'wb') { |f| f << content }
raise “Do not Commit tests with this statement.”
 end
end

# check_content to write tests
# update_content to generate file

Be very careful not to remove the exception: “Do not commit tests with this statement.” Your tests can still deliver false positives, even though errors in the code may exist.  

Here is a simple test fragment you can apply my technique to:

ruby
  xls_content = XlsxBuilder.new(payments).build
  check_content(xls_content, "spec/fixtures/reports/acme_report.xlsx")

2. Implement Requested Functionality

To illustrate this step, imagine your client is an online bookstore that accepts payments directly from individual customers. You have a RubyOnRails application that can accept user payments, query 3rd party APIs, and collect payment API responses. Several Rails models related to these functions are available on your server:

app/models/publisher.rb
app/models/payment.rb
app/models/user.rb

Now let’s say the bookstore/client requests additional functionality. They want an XLS file that includes all payments received, and they want this file to be generated every day after midnight. Your task is to produce this file and send it to the client. How do you create a testable design? By building your functionality from top to bottom. Before you begin, I recommend decoupling the file generation from your application to simplify testing.

First, we need a public interface to generate the XLS file:

ruby
class XlsGeneratorService
 def self.call(publisher, date)
    return ‘/tmp/dummy_report.xls’
 end
end

This service can be added to the cron scheduler. We can then submit the file with fake data just to pretest the submission.

3. Generate a Fresh File with #update Content

In order to generate the file with real data, we need to add some classes with the requested functionality. These classes should be tested separately to avoid future problems in the end file:

    1. Payments collector can be encapsulated inside the query object.
    2. Payments builder can extract payment details and create an array with plain old Ruby objects (PORO).
    3. .XlsContent builder can implement an XLS file content builder capable of consuming this payment array with PORO.

We could collect payments and build XLS content directly from the payment model, but doing so would make our tests unnecessarily complex. The following is more testable:

ruby
 class PaymentsQuery
   def initialize(client, date)
      @client = client
      @date = date
   end
   def call
      @client.payments.successful
   end
 end

In the above class, we can collect payments and the test call method. Several unit tests would then be sufficient.

4. Build and Test XLS File Against Generated File

Our next step is to transform the array of payments into an array of PORO:

ruby
class ReportedPayment
 attr_accessor :amount, item
end
class ReportedPaymentsBuilder
  def initialize(payments)
     @payments = payment
  End
  def build
     @payments.map do |payment|
        ReportedPayment.new.tap do |poro|
          poro.amount = format(payment.amount)
          Poro.item = payment.product.shord_description
        end
     end
  End
  private def format(amount)
     # formatting logic
  end
end

The above builder is also easy to test. You would pass payments inside Ruby and then examine #build output. You can also easily write unit tests for this array. With PORO, you can then decouple the file generator from the application and test the generated file with fake data:

class XlsContentBuilder
 def initialize(reported_payments, report_date)
    @reported_payments = reported_payments
    @report_data = report_date
 end
 def build
   # rubyXL gem can produce object which can be transformed into xls file
   workbook = RubyXL::Workbook.new
   worksheet = workbook[0]
   Woksheet.add_cell(0,0, report_date)
   # ….. Add payment
   workbook
  end
end

You can easily test the above builder as well. You would create the array with fake data, generate content, and store it to file. Then you compare the stored content with the generated content:

ruby
class XlsGeneratorService
 def self.call(publisher, date)
    payments = PaymentsQuery.new(client, date).call
    reported_payments = ReportedPaymentsBuilder.new(payments).build
    xls_content = XlsContentBuilder.new(reported_payments, date).build
    xls_content.write(“/tmp/report.xls”)
 end
end

The service itself can be tested as a blackbox. To execute a high-level integration test, you create several payments with specific dates. Then you pass these payments to the service and check the content of the file produced.

Following the steps outlined in this article can solve a significant problem: You can control your generated files. This method also allows you to create as many “approved” files as you need to cover your solution and deliver an overall high quality, stable solution. However, this standard testing method does not simplify the process of debugging files whose content can’t be viewed (PDF, XLS). It does not eliminate the need to pay close attention to detail. You can still create files with no content which will result in your subsequent tests delivering false positives. You need to manually check each file before testing to avoid these problems.

Previous

Next