Ruby Inline Tests

Did you know you can inline tests with your ruby code in the same file?

Posted by Anton Katunin on 31 May 2016
Tags: code

After reading the title Embedding a Test Suite in a Single-file Ruby App (Part 1) I thought: aha! finally somebody has done it and used ruby data block to store tests. However I was disappointed as there was another approach, which I think is less elegant.

Below I show you a proof of concept of embedding tests in ruby code.

Why?

Because tests and code are in the same file:

  • easier to refactor
  • easier to navigate
  • documentation from tests near the code
  • does not affect code performance
  • reminds you to write tests
  • How

    Ruby has special feature which I call data blocks. In short you can embed any data after your ruby code in the same file and when you run that file that data will be available as DATA constant. All you need is to separate it with __END__.

    For example

    $ cat t.rb
    puts DATA.gets
    __END__
    hello world!
    
    $ ruby t.rb
    hello world!
    

    Putting all together

    Below is my proof of concept to run rspec tests from the same file.

    # drink.rb
    
    class Drink
      attr_reader :type
    
      def initialize
        @type = "water"
      end
    end
    
    __END__
    require 'spec_helper'
    
    RSpec.describe Drink do
       it 'is has default type water' do
        expect(Drink.new.type).to eq('water')
      end
    end
    

    In order for that to work we need to put small patch to rspec so it reads the file correctly.

    # spec_helper.rb
    class DataBlockLoader
    
      module RspecConfig
        def load_spec_files
          super
          files_to_run.uniq.each do |f|
            file = File.expand_path(f)
    
            DataBlockLoader.load file
          end
        end
      end
    
      END_TOKEN = /^__END__\r?\n/
    
      def self.load file_path
        io = File.open(file_path, 'r' )
    
        loop do
          line = io.gets
    
          break if line.nil?
          break if line.match(END_TOKEN)
        end
    
        line_no = io.lineno
    
        eval io.read, nil, file_path, line_no + 1
      end
    end
    
    RSpec::Core::Configuration.prepend DataBlockLoader::RspecConfig
    

    Finally

    Now we can just run that file with rspec:

    $ rspec drink.rb
    
     1/1 |========================= 100 ==========================>| Time: 00:00:01
    
    Finished in 0.86 seconds (files took 0.70232 seconds to load)
    1 example, 0 failures
    

    This is just a small proof of concept which requires more work on adopting rspec internals to work with data blocks. Let's me know if you'll take a challenge on that.


    Read next:

    Volunteering at RailsGirls Next

    at Melbourne