Teaching Your Tests To Report Unused Parameters

Note: a Pivotal Labs version of this article is available here.

Recently I was about to check in some changes and did a last minute click through of the application. All of a sudden I’m staring at a stack trace. My tests were green and I had functional tests for the failing

Tests are like pants — they cover your backside while you focus on other things like adding features to your application. Suddenly I felt a breeze on my cheeks. Something was amiss.

I soon discovered the action and its associated tests had diverged over time. Some of the parameters were renamed in the action but not in the functional test. Since some of the work of the action was
conditional on the presence of certain parameters, that work was no longer being tested.

This exposed weaknesses in the tests and code, such as expected side effects in the tests that are never checked. If they had been checked the tests would have failed and the parameter name mismatch
would have been discovered.

Most functional tests provide specific parameters that should at least be examined during the processing of the action. Reporting unread parameters would strengthen those tests. It was conceivable to me that
some of the other functional tests had similar unused parameters. I wanted all of my functional tests to report all unused parameters.

The first step was to instrument the params hash. I wanted to track access to the params hash and report parameters that were not read during the processing of the action. I don’t know what all is done to params during the life cycle of a test. I’m only interested in access
from the time the action starts till it returns so I need to be able
to turn the tracking on and off at specific times.

It turns out that Rails uses a subclass of Hash called HashWithIndifferentAccess. I added my changes to HashWithIndifferentAccess in test/test_helper.rb:

    class HashWithIndifferentAccess
      def [](key)
        @accessed_keys ||= {}
        @accessed_keys[key] = true
      def start_logging
        @accessed_keys = {}
      def end_logging
        @accessed_keys['action'] = true
        @accessed_keys['controller'] = true
        never_accessed = []
        self.each_key do |key|
          never_accessed << key unless @accessed_keys.include?(key)
        raise "Some keys never accessed: #{never_accessed.join(', ')}" unless never_accessed.empty?

With these changes an exception will be raised if any first level keys are not read between start_logging and end_logging.

In each of my functional tests I added code similar to this (from account_controller_test.rb):

    class AccountController
      around_filter :check_params
      def check_params

The around filter starts and ends the logging in the context of the action.

With these changes in place my tests no longer passed and my backside was warm and protected again.