Skip to content

Instantly share code, notes, and snippets.

@abMatGit
Last active June 29, 2016 22:50
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save abMatGit/803c2a71095723f5787f1e389c573725 to your computer and use it in GitHub Desktop.
Save abMatGit/803c2a71095723f5787f1e389c573725 to your computer and use it in GitHub Desktop.

Fairly recently we investigated a runtime error while running cron jobs against our subscription purchases. The error stemmed from a class mismatch when a method method_foo was returning a SubscriptionFoo object when we were expecting a SubscriptionBar object. The solution was fairly straight forward, however writing a well-scoped test for method_foo and its return value was not. Why? Because it is often overlooked to write well-scoped tests for relatively large applications in favor of writing narrow-scoped tests that strictly test what you want. Tests should be just as scalable, flexible and maintainable as our main code.

The initial attempt was to test method_foo's class return types: SubscriptionFoo or SubscriptionBar. The code was relatively simple:

describe FooBar do
  describe '#method_foo' do
    context 'when it should return a SubscriptionTypeFoo' do
      ...
      expect { FooBar.method_foo }.to be_an_instance_of(SubscriptionFoo)
    end
    
    context 'when it should return a SubscriptionTypeBar' do
      ...
      expect { FooBar.method_foo }.to be_an_instance_of(SubscriptionBar)
    end
  end
end

However there are a few issues with this:

  • It seems wrong to test static return types within a dynamic language, especially when it comes to class names.
  • What would happen if we change the class name from SubscriptionFoo to SubscriptionFooBar? Should that fundamentally fail the test?

Class names do not strike the core of the issue, nor do class names describe what we are really testing. So WHAT if method_foo returns SubscriptionFoo? What does that mean? The issue here is that class names do not REPRESENT anything more meaningful than the name itself. The lack of abstraction ends up directly relating to the lack of descriptiveness in our test. And we can properly abstract SubscriptionFoo and SubscriptionBar into what we call interfaces or behaviours.

So instead of testing class names, we want to test BEHAVIOUR. Abstract the idea of testing a class to testing something that adheres to an interface instead. In other words, instead of testing SubscriptionFoo or SubscriptionBar, test that whatever method_foo returns, adheres to an interface and behaves consistently.

The code will instead look like:

shared_context 'a foo subscription' do
  it { is_expected.to respond_to 'subscription_method_1' }
  it { is_expected.to respond_to 'subscription_method_2' }
  it { is_expected.to respond_to 'subscription_method_foo' }
end

shared_context 'a bar subscription' do
  it { is_expected.to respond_to 'subscription_method_1' }
  it { is_expected.to respond_to 'subscription_method_2' }
  it { is_expected.to respond_to 'subscription_method_bar' }
end

describe FooBar do
  describe '#method_foo' do
    context 'for a Foo-like subscription' do
      it_behaves_like 'a foo subscription'
    end
    
    context 'for a Bar-like subscription' do
      it_behaves_like 'a bar subscription'
    end
  end
end

This type of testing is very much in line with the concept of duck typing. By testing if the returned value walks like a duck and quacks like a duck, we have much more assurance that it IS a duck than just testing the class name Duck. In particular, we are testing the interfaces of SubscriptionFoo and SubscriptionBar instead of just the static class names. This gives a much deeper scope and integrity to what we are testing, in addition to giving the developer documentation of how we expect our return types to behave.

Overall this is a beautiful way of compromising with the dynamic nature of the ruby language. Instead of testing for hardcoded values and testing if your return values are of a specific type, test their behaviour. Test the interfaces they adhere to and you have added more integrity and depth to the test, while having no constraints on hardcoded values.

@alan-andrade
Copy link

alan-andrade commented Jun 29, 2016

I feel like the goal of this blog post is to explain duck typing and how you came to understand it. If the blog post is seen as a pyramid, I feel this is upside down. The base at the top and the pointy at the bottom. The pointy thingy you want to be at the top since is the most engaging part of the blog post.

In order to do that, we can think of the main idea and write that as the first paragraph and develop the story as support to the idea. Keep the reader engaged. It feels like this is an educational blog post, so write more about teaching, and less about the sequence of events.

Maybe something like this ?

Duck typing is a concept that I really understood when I was writing a test for the payments system. You might know the lyrics of this song already but do you really know what's up ?
If you have found yourself writing a test like this:

    context 'when it should return a SubscriptionTypeFoo' do
      ...
      expect { FooBar.method_foo }.to be_an_instance_of(SubscriptionFoo)
    end

you might want to keep reading since you'll save many keystrokes and learn more about the value of dynamic languages and test driven development.

.... when I was a kid.... my momma told me.....
... kaboom... you are a better developer now. Enjoy !

@abMatGit
Copy link
Author

abMatGit commented Jun 29, 2016

👌 💡 I totally get what you're saying and I agree. Reading this again seems like just a recount of history events that lead up to a conclusion: "1 happened, and then I did 2, then 3 which lead me to 4". Its dry, mechanical and not engaging. Flip it around and make the conclusion what engages the reader from the beginning and focus on that as the journey.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment