Testing Generated Binary Files w/RSpec

19 Feb 2019

 

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 in this article 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.

Write test helper

To begin, 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” because your tests can 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")
```

Apply changes requested by client

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.

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.

Build XLS 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 would create several payments with specific dates. Then you pass these payments to the service and check the content of the file produced.

Summary and Recommendations

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. Test against generated file.

Following the steps outlined in this article can solve a significant problem: You can control your generated files. This method will also allow 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. 

For more information about Sphere’s QA and testing services, get in touch.