A sort of thought about interaction (and perhaps state-based) tests

This here post is about making tests terse by specifying what has happened instead of (as in interaction tests) who did it or (as in state-based test) the different kinds of things-it-has-happened-to.

I have a test that says that the Availability object should use the TupleCache object to get particular values for: all animals, animals that are still working, and animals that have been removed from service. If one wants to show animals that can be removed from service, it’s this:

all animals - animals still working - animals already removed from service

Here’s a mock-style test that describes how the Availability uses the TupleCache:

 should use tuple cache to produce a list of animals do
      @availability.override(mocks(:tuple_cache))
      during {
        @availability.animals_that_can_be_removed_from_service
      }.behold! {
        @tuple_cache.should_receive(:all_animals).once.
                     and_return([{:animal_name => out-of-service jake‘},
                                 {:animal_name => working betsy‘},
                                 {:animal_name => some…‘},
                                 {:animal_name => …other…‘},
                                 {:animal_name => …animals‘}])
        @tuple_cache.should_receive(:animals_still_working_hard_on).once.
                     with(@timeslice.first_date).
                     and_return([{:animal_name => working betsy‘}])
        @tuple_cache.should_receive(:animals_out_of_service).once.
                     and_return([{:animal_name => out-of-service jake‘}])
       }
      assert_equal([”…animals“, …other…“, some…“], @result)
    end

I’m not wild about the amount of detail in the test, but let’s leave that to the side. Notice that the results of the test imply that the Availability is turning the tuples (think of them as hashes or dictionaries) into a simple list of strings. Notice also that the list of strings is sorted. Noticing that brings a couple of questions to mind:

  • That sorting - does it use ASCII sorting, which sorts all uppercase characters in front of lowercase? or is it the kind of sorting the users expect (where case is irrelevant)?

  • Are duplicates stripped out of the result?

As it happens, I want the responsibility of converting tuples into lists to belong to another object. I’d prefer Availability to have only the responsibility of asking the right questions of the persistent data, not also of massaging the results. I’d like to put that responsibility into a Reshaper object. Here’s an expanded test that does that:

    should use tuple cache to produce a list of animals do
      @availability.override(mocks(:tuple_cache, :reshaper))
      during {
        @availability.animals_that_can_be_removed_from_service
      }.behold! {
        @tuple_cache.should_receive(:all_animals).once.
                     and_return([”…tuples-all…“])
        @tuple_cache.should_receive(:animals_still_working_hard_on).once.
                     with(@timeslice.first_date).
                     and_return([”…tuples-work…“])
        @tuple_cache.should_receive(:animals_out_of_service).once.
                     and_return([”…tuples-os…“])
        # New lines
        @reshaper.should_receive(:extract_to_values).once.
                  with(:animal_name, [’…tuples-work…‘], [”…tuples-os…“], [”…tuples-all…“]).
                  and_return([[”working betsy“], [’out-of-service jake‘],
                              [’working betsy‘, out-of-service jake‘,
                              some…‘, …other…‘, …animals‘]])
        @reshaper.should_receive(:alphasort).once.
                  with([’some…‘, …other…‘, …animals‘]).
                  and_return([”…animals“, …other…“, some…“])
      }
      assert_equal([”…animals“, …other…“, some…“], @result)
    end

It shows that the Availability method calls Reshaper methods which we could see (if we looked) guarantee the properties that we want. But I don’t like this test. The relationship between Availability and Reshaper doesn’t seem to me nearly as fundamental as that between Availability and TupleCache. And I hate the notion that the general notion of “convert a pile of tuples into a sensible list” is made so specific: it will make maintenance harder. And I’m not thrilled (throughout this test) of the way that the human reader must infer claims about the code from the examples.

So how about this?:

   should use tuple cache to produce a list of animals do
      @availability.override(mocks(:tuple_cache))
      during {
        @availability.animals_that_can_be_removed_from_service
      }.behold! {
        @tuple_cache.should_receive(:all_animals).once.
                     and_return([{:animal_name => out-of-service jake‘},
                                 {:animal_name => working betsy‘},
                                 {:animal_name => some…‘},
                                 {:animal_name => …other…‘},
                                 {:animal_name => …animals‘}])
        @tuple_cache.should_receive(:animals_still_working_hard_on).once.
                     with(@timeslice.first_date).
                     and_return([{:animal_name => working betsy‘}])
        @tuple_cache.should_receive(:animals_out_of_service).once.
                     and_return([{:animal_name => out-of-service jake‘}])
      }
      assert_equal([”…animals“, …other…“, some…“], @result)
      assert { @result.history.alphasorted }

The last line of the test claims that—at some point in the past—the result list has been “alphasorted”. A list that’s been alphasorted has the properties we want, which we can check by looking at the tests for the Reshaper#alphasort method.

In essence, we check whether at some point in the past the object we’re looking at has been “stamped” with an appropriate description of its properties. Therefore, we don’t have to construct test input that checks the various ways that description can become true - we simply trust earlier tests of what the stamp means.

Here’s code that adds the stamp:

    def result.history()
      @history = OpenStruct.new unless @history
      @history
    end
    result.history.alphasorted = true
    result.freeze

(Notice that I “freeze” the object. In Ruby, that makes the object immutable. That’s in keeping with my growing conviction that maybe programs should consist of functional code sandwiched between carefully-delimited bits of state-setting code.)

Having said all that, I suspect that the original awkwardness in the tests is a sign that I need a different factoring of responsibilities, rather than making up this elaborate solution. But I haven’t figured out what that factoring should be, so I offer the alternative for consideration.

Leave a Reply

You must be logged in to post a comment.