---
title: 'Meet Duck Typer: your new duck typing friend'
teaser: Keep the beauty of duck typing with no annotations while still catching missing
  methods, mismatched signatures, and accidental drift across classes.
tags: rspec,minitest,testing,ruby,oop,development
author: Thiago Araújo Silva
published_on: 2026-03-23
---

The Ruby language leans on duck typing rather than formal interfaces.
"If it walks like a duck and quacks like a duck, it's a duck."

```rb
bodies = [poem, essay, case_study, user_manual].map(&:body)
```

These objects come from different classes but all respond to `body`,
so you can treat them uniformly. That's informal _polymorphism_: no
type annotations, no ceremony.

Duck typing is great, but what I miss is _enforcement_. If one class
implements a method and another forgets to, I want something to
complain.

## The interface is a living document

In Ruby, interfaces are implicit. They live in the public methods of
your classes. Private methods are implementation details and not part
of the interface.

```rb
class MessagePresenter
  def title; end
  def url(format:); end
end

class CommentPresenter
  def title; end
  def url(format:); end
end
```

Just by looking at these two classes, we can see an implicit
interface: `title` and `url(format:)`.

If `CommentPresenter` also had a `body` method, `title` and
`url(format:)` would still form a partial interface, one we might
call `Linkable`.

Now the question is: how do we enforce that these classes adhere
to this implicit interface?

## Enforcing an interface

How do we enforce that future changes don't break this contract?
Tests can help:

```rb
test "presenter interface" do
  message = Message.new(title: "Dummy", path: "/dummy")
  comment = Comment.new(title: "Dummy", path: "/dummy")

  [message, comment].each do |object|
    assert_respond_to object, :title
    assert_respond_to object, :url
  end
end
```

It works, but it feels weird to instantiate objects just to ask what
methods they have. We can skip instances and check the class directly:

```rb
def assert_instance_responds_to(klass, method, arity: 0)
  # asserts method is public and arity matches
end

test "presenter interface" do
  [MessagePresenter, CommentPresenter].each do |klass|
    assert_instance_responds_to klass, :title, arity: 0
    assert_instance_responds_to klass, :url, arity: 1
  end
end
```

Nicer, and arity gives us more confidence than just checking the
method name. Still, arity alone misses keyword names, optional vs.
required params, and won't catch added or removed methods
automatically.

## The duck_typer gem

To solve this optimally (keeping the spirit of duck typing while
still enforcing interfaces), I wrote a tiny gem called
[`duck_typer`](https://github.com/thoughtbot/duck_typer). It's a
minimal set of helpers extracted from patterns I've used to test
duck-typed interfaces across several projects. You don't need to
explicitly define an interface. What matters is comparing the
objects' public interfaces. If they match, the interface is
implemented correctly; if there are differences, the tooling surfaces
them.

After installing, you can assert that a list of classes share
compatible interfaces:

```rb
test "presenter interface" do
  assert_interfaces_match [MessagePresenter, CommentPresenter]
end
```

You can add as many classes as you want. The failure messages are very
explicit. If you add a `body` method to only one presenter:

```
Expected MessagePresenter and CommentPresenter to implement compatible
interfaces, but the following method signatures differ:

MessagePresenter: body()
CommentPresenter: body not defined
```

It also catches signature drift. Remove a keyword argument in one
class:

```
MessagePresenter: url()
CommentPresenter: url(format:)
```

Add a positional argument:

```
MessagePresenter: title(truncate)
CommentPresenter: title()
```

Yep, everything you'd expect to break does break.

To check a partial interface, pass `methods:`. You can also name it,
which shows up in the failure message:

```rb
assert_interfaces_match [MessagePresenter, CommentPresenter],
  name: "Linkable",
  methods: %i[title url]
```

```
Expected MessagePresenter and CommentPresenter to implement compatible
"Linkable" interfaces, but the following method signatures differ:
```

And if you want to check class methods instead of instance methods,
pass `type: :class_methods`:

```rb
assert_interfaces_match [MessagePresenter, CommentPresenter],
  type: :class_methods
```

If you're on RSpec, the same options are available via
`have_matching_interfaces` or `it_behaves_like "an interface", [...]`.

The [README](https://github.com/thoughtbot/duck_typer) has full setup
instructions and all available options.

## Bonus feature

Suppose you create a new class that implements the interface. You
still need to _remember_ to add it to the list of classes under test.

If your classes share a namespace, you can pass it directly and
`duck_typer` will find them for you:

```rb
assert_interfaces_match namespace: Presenters::Linkable
```

If your codebase already gathers related classes in one place, you
can pass that directly instead:

```rb
assert_interfaces_match Presenter::LINKABLE_CLASSES, name: "Linkable"
```

Either way, if someone adds a `PostPresenter` that forgets to
implement `url(format:)`, you'll get a failure without needing to
update the test list.

The same options work in RSpec via `have_matching_interfaces` or
`it_behaves_like "an interface"`.

## Limitations

There's one fundamental limitation: you can't check types of method
arguments. This is Ruby; that's the tradeoff. And honestly, types
aren't really part of a Ruby interface anyway.

## Conclusion

By treating public methods as an implicit interface and enforcing that
interface through tests, we get the best of both worlds: the freedom
of Ruby with the safety net of interface checks. That's what
[`duck_typer`](https://github.com/thoughtbot/duck_typer) aims to
provide: a tiny tool that keeps your classes honest without
annotations or explicit interface definitions.

If you've ever caught yourself writing ad-hoc tests to enforce
interfaces, as was my case,
[`duck_typer`](https://github.com/thoughtbot/duck_typer) might be
for you.
