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 testing 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 testing to generate and then test PDF, XLS, or any other binary files from a Rails application. Read on to learn more about test driven development in ruby.
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:
- Write test helper
- Implement requested functionality
- Generate a fresh file with #update content
- 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:
- Payments collector can be encapsulated inside the query object.
- Payments builder can extract payment details and create an array with plain old Ruby objects (PORO).
- .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. Give us a call today if you have any questions about running a binary test using RSpec. Our on-demand binary testing team is here to help!