DEV Community

loading...
Cover image for Testing CSV and Excel uploads with tempfiles

Testing CSV and Excel uploads with tempfiles

Ana Nunes da Silva
I'm a web developer working mainly on Rails backends. I love programming but also books, films and strolling around in my hometown, Lisbon.
Originally published at ananunesdasilva.com ・3 min read

Lately, I've been working on a couple of features that allow bulk data to be uploaded through .csv or .xlsx files.

When working with files, passing file fixtures to tests is a common practice. File fixtures are sample files that you can store under spec/fixtures/files (or in your own customized path). This way, you can read an actual file in your tests or just pass the file path to other classes that might need to access the file data.

File fixtures are simple to use but I've found them useful only on cases where either the file data is not that relevant or it is not going to vary.

The problem is that in most cases the user can upload files with different structures or data, depending on the use case. This means that, for test purposes, if we want to cover different scenarios, we will eventually end up stuffing our fixtures folder with enough file variations that would cover all the scenarios. It's just not flexible enough and it looks hard to maintain. Plus, it has the downside of hiding data from your tests since you have to open the files to know what's inside.

Ideally, we should be able to create the sample data explicitly in each test scenario, run our expectations, and clean up the data afterward. Sounds like a good case for Tempfiles - a file that will be available to test but that will be deleted after the test runs.

# students_uploader_spec.rb
describe 'Services::StudentsUploader' do 
  it 'creates students' do 
    Tempfile.open(['students.csv', '.csv']) do |temp|
       CSV.open(temp, 'wb', col_sep: ';', headers: true) do |csv|
         csv << ['email', 'name']
         csv << ['student@example.com', 'Student Name']
       end

       result = described_class.call(temp)

       expect(result[:count]).to eq(1)
       expect(Student.count).to eq(1)
     end
  end
end
Enter fullscreen mode Exit fullscreen mode

Looking at the example above I can cleary define three necessary steps:

  • Open a Tempfile and pass it a filename (extension is optional).
  • Inside the Tempfile block, open a CSV, pass it the tempfile instance and build the rows with the data you need
  • Run the expectations inside the tempfile block

A fourth automatic step will delete the tempfile so you don't end up with a bunch of tempfiles lying around in your project.

It's important to reinforce that everything that depends on the tempfile will have to be run inside the tempfile block. That's because the tempfile is deleted immediately after its block is closed. If you try to run your expectation after the tempfile block is closed, you'll get an error similar to this one:

Failures:
  Failure/Error: result = described_class.call(temp)

  NameError:
    undefined local variable or method `temp' for #<RSpec::ExampleGroups::Services::SudentsUploader:0x00007fb74000c0b8>
  # ./spec/services/students_uploader_spec.rb:1:in `block (3 levels) in <top (required)>'
Enter fullscreen mode Exit fullscreen mode

But going back to our happy path, I can also write a similar context for xlsx files (I'm using the axlsx gem to write excel files):

describe 'Services::StudentsUploader' do
  context 'when uploading a xlsx' do  
    it 'creates students' do 
      Tempfile.open(['students.xlsx', '.xlsx']) do |temp|
        Axlsx::Package.new do |p|
          p.workbook.add_worksheet(name: "test") do |sheet|
            sheet.add_row ['name', 'email', 'code']
            sheet.add_row ['Jane Doe', nil, 123]
            sheet.add_row ['John Doe', 'john@test.com', nil]
          end
          p.serialize(temp.path)
        end

        result = described_class.call(temp.path)

        expect(result[:count]).to eq(1)
        expect(Student.count).to eq(1)
      end
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Now I can play around with different file structures, knowing that I will have my data explicitly available in each scenario and that I will be able to tweak it easily without the hassle of having to create and maintain external files.

Happy testing! 🧪

Discussion (0)