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
toSubscriptionFooBar
? 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.
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:
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 !