Consider this Ruby code which generates a report for a subset of companies:

class CustomReport
  def initialize(output:)
    @output = output
  end

  def companies
    DB.companies.query(vertical: 'airline').all
  end

  def generate
    report = # do something with companies

    File.open(@output, 'w') { |f| f.write(report) }
  end
end
class CustomReportTest < Minitest::Test
  def setup
    @airline = DB.companies.create(vertical: 'airline')
    @gallery = DB.companies.create(vertical: 'fine_art')
  end

  def test_generate_report_includes_only_airlines
    CustomReport.new(output: '/tmp/custom_report.txt').generate
    report = File.read('/tmp/custom_report.txt')

    report_ids = extract_ids(report)
    expected_ids = Set.new([@airline.id])

    assert_equal expected_ids, report_ids
  end

  def extract_ids(report)
    # return a Set of IDs found in the report
  end
end

Our task is to generate the same report for another vertical. The simplest solution would be adding a second DB query with the new vertical hardcoded. Instead, let’s make the algorithm generic to make supporting future verticals easier.

In our scenario, adding new verticals happens rarely and with ample lead time, so deploying code is a feasible workflow for adding more. A list of the verticals can be stored in a constant on CustomReport and iterated over. Adding a new report simply means adding a new element to the list:

class CustomReport
  VERTICALS = ['airline', 'performance']

  def initialize(output:)
    @output_dirname, @output_filename = parse_dir_and_filename(output)
  end

  def companies_by_vertical
    VERTICALS.each_with_object({}) do |v, h|
      h[v] = DB.companies.query(vertical: v).all
    end
  end

  def generate
    companies_by_vertical.each do |vertical, companies|
      report = # do something with companies

      filename = "#{@output_dirname}/#{vertical}_#{@output_filename}"
      File.open(filename, 'w') { |f| f.write(report) }
    end
  end

  def parse_dir_and_filename(path)
    # return dirname, filename
  end
end

This approach introduces some tradeoffs in testing. The test could check the same verticals defined in CustomReport::VERTICALS, but that means changing the test each time a new vertical is added.

The code and its tests are tightly coupled.

Decoupling would require modifying the value of CustomReport::VERTICALS in the test, which can be done but requires some push-ups. It’s an indicator that there’s probably a better way.

Let’s use a class method instead, which is easier to stub in test:

class CustomReport
  def self.verticals
    ['airline', 'performance']
  end

  def initialize(output:)
    @output_dirname, @output_filename = parse_dir_and_filename(output)
  end

  def companies_by_vertical
    self.class.verticals.each_with_object({}) do |v, h|
      h[v] = DB.companies.query(vertical: v).all
    end
  end

  def generate
    companies_by_vertical.each do |vertical, companies|
      report = # do something with companies

      filename = "#{@output_dirname}/#{vertical}_#{@output_filename}"
      File.open(filename, 'w') { |f| f.write(report) }
    end
  end

  def parse_dir_and_filename(path)
    # return dirname, filename
  end
end

The test can now stub CustomReport.verticals which achieves the decoupling we’re after. However, that self.class.verticals.each_with_object looks onerous. And it does rely on stubbing, which incurs some risk. I don’t quite like it.

The key here is that the verticals are configuration for CustomReport and should live outside the class. This configuration is a dependency for CustomReport. Let’s inject this dependency:

class CustomReport
  def initialize(output:, verticals:)
    @output_dirname, @output_filename = parse_dir_and_filename(output)
    @verticals = verticals
  end

  def companies
    @verticals.each_with_object({}) do |v, h|
      h[v] = DB.companies.query(vertical: v).all
    end
  end

  def generate
    companies_by_vertical.each do |vertical, companies|
      report = # do something with companies

      filename = "#{@output_dirname}/#{vertical}_#{@output_filename}"
      File.open(filename, 'w') { |f| f.write(report) }
    end
  end

  def parse_dir_and_filename(path)
    # return dirname, filename
  end
end

The test now looks like this:

class CustomReportTest < Minitest::Test
  def setup
    @verticals = ['airline', 'performance']
    @airline = DB.companies.create(vertical: 'airline')
    @gallery = DB.companies.create(vertical: 'fine_art')
    @theater = DB.companies.create(vertical: 'performance')
  end

  def test_generate_airline_report_includes_only_airlines
    CustomReport.new(
      output: '/tmp/custom_report.txt',
      verticals: @verticals
    ).generate
    report = File.read('/tmp/airline_custom_report.txt')

    report_ids = extract_ids(report)
    expected_ids = Set.new([@airline.id])

    assert_equal expected_ids, report_ids
  end

  def test_generate_theater_report_includes_only_theaters
    CustomReport.new(
      output: '/tmp/custom_report.txt',
      verticals: @verticals
    ).generate
    report = File.read('/tmp/performance_custom_report.txt')

    report_ids = extract_ids(report)
    expected_ids = Set.new([@theater.id])

    assert_equal expected_ids, report_ids
  end

  def extract_ids(report)
    # return a Set of IDs found in the report
  end
end

The test generates reports for airlines and theaters, but the production code can generate reports for food trucks and toy stores. The code and its tests are completely decoupled, and adding more verticals is as straightforward as the initial solution. The CustomReport class can be used elsewhere in the code for different verticals if required.

Pay attention to those code smells. If something looks off or is difficult to test, it’s probably a nudge to a better design.