Search code examples
ruby-on-railsrubyunit-testingconstantsminitest

How would I test Ruby or Rails constants using minitest?


I believe all end-developer code (including constants) should be tested (as mentioned here). I'm not looking for a debate on that.

Assume I have these Ruby and/or Rails constants that I'd like to test using minitest (not RSpec). How would I go about that?

#####################################################################
#
# Constants
#
#####################################################################
THE_ANSWER_TO_EVERYTHING = 42
PI_WITH_PRECISION_FIVE   = 3.14159
FIRST_TEN_PRIME_NUMBERS  = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29].freeze
FAMOUS_CUTE_CATS         = ['Grumpy Cat', 'Hello Kitty', 'Jinx the Cat'].freeze
HEXADECIMAL_COLOR_REGEX  = /\A#\h{6}\z/.freeze

You can assume the above are constants on a model called CuteCat, but the solution should work for any model, controller, PORO, etc.


Solution

  • I like to test Ruby constants in three ways:

    1. There is the main, obvious way which checks that the value of the constant in question is exactly equal to the expected value.
    2. I then also check to make sure the constant is frozen (although some might prefer their linter do this job). In the case of arrays, I only check the array itself is frozen, not the values inside (which could be done recursively if desired for completeness).
    3. And then finally I like a canary test that goes off (fails) when someone has added a new constant but forgotten to add a test for it. From my experience, this is extremely helpful as projects progress.

    Here is what that all looks like in minitest:

    test/models/cute_cat_test.rb

    # frozen_string_literal: true
    
    require 'test_helper'
    
    class CuteCatTest < ActiveSupport::TestCase
    
      ###################################################################
      #
      # Constants
      #
      ###################################################################
      test 'have a specific number of local, frozen constants' do
        assert_constant_count(CuteCat, 5)
        assert_constants_frozen(CuteCat)
      end
    
      ###############################################
      test 'describe the valid answer to everything' do
        expected = 42
        assert_equal(expected, CuteCat::THE_ANSWER_TO_EVERYTHING)
      end
    
      ###############################################
      test 'describe pi with a precision of five' do
        expected = 3.14159
        assert_equal(expected, CuteCat::PI_WITH_PRECISION_FIVE)
      end
    
      ###############################################
      test 'describe the first ten prime numbers' do
        expected = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29].freeze
        assert_equal(expected, CuteCat::FIRST_TEN_PRIME_NUMBERS)
      end
    
      ###############################################
      test 'describe the famous cute cats we care about' do
        expected = ['Grumpy Cat', 'Hello Kitty', 'Jinx the Cat'].freeze
        assert_equal(expected, CuteCat::FAMOUS_CUTE_CATS)
      end
    
      ###############################################
      test 'describe the hexadecimal color regular expression' do
        expected = /\A#\h{6}\z/.freeze
        assert_equal(expected, CuteCat::HEXADECIMAL_COLOR_REGEX)
      end
    end
    

    You'll notice the canary test at the beginning is making use of two, custom methods. Here are the definitions for those methods:

    test/test_helper.rb

    #####################################################################
    #
    # Finders
    #
    #####################################################################
    
    # Return constant names for the given class or module. It attempts to find names that are defined directly on the
    # given class, are uppercase, and that do not refer to inner classes. This assumes that the given class follows the
    # uppercase naming convention for its constants, while attempting to exclude names that refer to inner classes whose
    # names are improperly uppercased, e.g., Facebook::API::URL.
    def find_constants(klass)
      inherited     = false
      constant_list = klass.constants(inherited)
    
      constant_list.select { |c| c == c.upcase &&
                                 klass.const_get(c).class != Class }
    end
    
    
    
    #####################################################################
    #
    # Assertions
    #
    #####################################################################
    def assert_constant_count(klass, expected_count)
      constant_list = find_constants(klass)
      message       = "Expected number of constants for #{klass.name} does not match actual count"
      assert_equal(expected_count, constant_list.count, message)
    end
    
    def assert_constants_frozen(klass)
      find_constants(klass).each do |constant|
        message = "Expected constant #{klass.name}::#{constant} to be frozen"
        assert(klass.const_get(constant).frozen?, message)
      end
    end