惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Stack Overflow Blog
Stack Overflow Blog
量子位
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
美团技术团队
小众软件
小众软件
aimingoo的专栏
aimingoo的专栏
Recent Announcements
Recent Announcements
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Microsoft Security Blog
Microsoft Security Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
酷 壳 – CoolShell
酷 壳 – CoolShell
J
Java Code Geeks
V
V2EX
大猫的无限游戏
大猫的无限游戏
D
DataBreaches.Net
博客园 - Franky
爱范儿
爱范儿
T
Tailwind CSS Blog
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
博客园_首页
B
Blog RSS Feed
博客园 - 司徒正美
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Giant Robots Smashing Into Other Giant Robots

Client success starts before kickoff 5 easy, actionable tips for software development in healthcare Announcing importmap-update: automated dependency updates for importmap-rails When to vibe code an app and when to hire someone Why leaders have to know about PMS 🩸 Is AI ruining my brain? Tech Leaders Meetup is coming to Edinburgh Designers already think in React Tech Leaders Meetups are back in London this autumn GPT and Claude go to heraldry school Inserting State Transitions in Postgres Don’t hire thoughtbot to write code AI makes creating software faster, but in regulated industries, judgment matters more Tech Leaders meetup in Amsterdam PMs Don't Need to Code, but They Do Need to Understand How healthcare tech teams innovate while balancing speed and security Can’t touch the DOM? Reach for :has() to style any element Buying Time, Choosing Words: Consulting Through Diplomatic Communication thoughtbot around the world, meet us at upcoming events Modeling State Transitions in Postgres Humid 1.0: React server-side rendering in Rails can be easy! A prototype is not a product. It's a conversation. New: The State of Software Delivery in Healthcare Sign in with Google for React Native What founders told us about working with AI tools for startups Join us: Building Secure Healthcare Systems Upcase has retired, but the learning continues The Bike Shed Ep 506: The Muppet Software Team Migrating to native stack navigation, with a surprise from iOS 26 Past and present thoughtbotters at LRUG this Monday
Why Duck Typer?
Thiago Araújo Silva · 2026-05-26 · via Giant Robots Smashing Into Other Giant Robots

Duck Typer is a Ruby gem that validates interface compatibility across polymorphic classes sharing the same role, so they can be used interchangeably. It detects and clearly reports interface drift directly in your test suite.

Since Duck Typer launched, there’s been some discussion about the validity of interface testing. In this post, I want to make the case for it.

“Interface tests are fragile, so you shouldn’t write them”

That’s not true without context. How is your test suite structured? What do you test? Obviously, if you write only interface tests like this:

def test_interfaces_match
  assert_interfaces_match [StripeProcessor, PaypalProcessor]
end

With no behavior tests to accompany it, that quote will be true. Why? Because you’re not testing actual code behavior. Alone, Duck Typer tests are fragile. So why should you still write them?

“But I already have behavior tests that catch mismatches”

You do, and they will catch mismatches eventually, assuming you have good test coverage. The problem is how they catch them. A behavior test will blow up with a NoMethodError or an ArgumentError, but nothing about that tells you it’s an interface problem across a group of classes. You have to figure that out yourself, then work backwards to find which class drifted and what changed.

Duck Typer short-circuits that investigation. It tells you what drifted and where, in a single message, before you ever hit a behavioral failure:

Expected StripeProcessor and PaypalProcessor to implement compatible
interfaces, but the following method signatures differ:

StripeProcessor: refund(transaction_id)
PaypalProcessor: refund not defined

There’s also a sharper version of this objection: “You can remove the implementation and the test still passes, so it’s not a good test.” That’s true, and it’s by design. Duck Typer checks shape, not behavior. It explicitly marks that a set of classes is expected to evolve together, and when one changes, the failure makes it clear. That’s a different job than verifying correctness, and both are worth doing.

It’s about quality of life

At thoughtbot, we always valued testing UX and clear error reporting. We care about the details. For example, this is a style of test generally not encouraged here:

expect(objects).to eq([post_1, post_2, post_3])

Assume that the post objects are complex Active Record instances. Can you imagine what the error message will look like if one object has differences? It will dump a huge blob of text that incurs overhead to parse. What are we really testing there? That we’re getting the right objects! Instead, we can use named identifiers to make error reporting more actionable and crystal clear:

expect(objects.map(&:title)).to eq(["Post 1", "Post 2", "Post 3"])

Duck Typer applies the same principle to interface errors. Without it, you only get generic Ruby errors that say nothing about interface drift across classes. With Duck Typer, you also get a clear, targeted failure:

Expected StripeProcessor and BraintreeProcessor to implement compatible
interfaces, but the following method signatures differ:

StripeProcessor: charge(amount, currency:)
BraintreeProcessor: charge(amount, currency:, description:)

StripeProcessor: refund(transaction_id)
BraintreeProcessor: refund(transaction_id, amount)

It communicates design intent as actionable errors

I wish Ruby had interfaces. As I said in the introductory post, I want to be alerted of interface drift because it’s a great developer experience feature.

It’s not always obvious when classes are supposed to be used interchangeably. A clear error message communicates which classes share a role and what shape their interfaces should have.

What if you join a legacy project where the original developers left a long time ago? Duck Typer would be super helpful there too.

A concrete example: Null Objects. You add a deactivate method to User, and your behavior tests for User pass. But NullUser, which is supposed to be interchangeable with User, silently drifts because nobody remembered to update it. Behavior tests on User won’t catch that. Duck Typer will, immediately, because it treats those classes as a group that must stay in sync. It also reminds you to write the actual behavior test for NullUser#deactivate.

As a developer who loves targeted feedback, that is right up my alley.

It helps you think about design

Let’s say that introducing a do_stuff public method in StripeProcessor is the easiest way to accomplish a goal. You add it, but get a test failure like the following:

Expected StripeProcessor and PaypalProcessor to implement compatible
interfaces, but the following method signatures differ:

StripeProcessor: do_stuff(data)
PaypalProcessor: do_stuff not defined

That message doesn’t just report interface drift. It actually asks:

Why are you doing that? A public method in StripeProcessor should also exist in the other processors.

Most likely, your do_stuff method is not in the right place. Maybe it belongs in a collaborator object, or maybe it should be a private method that isn’t part of the public interface at all.

The same applies to differing method parameters; if you introduce a parameter in one class but it is not needed in another class from the same interface, you are probably doing something wrong.

“But that’s just like shoulda-matchers”

Not quite. Shoulda Matchers are great for shortening TDD feedback loops when working with Rails conventions. They verify a single object’s declarations: does this model have_many :posts? Does it validate_presence_of :email? That’s inward-facing: one object, one declaration.

In fact, once the code has enough behavior coverage, you could delete the shoulda-matchers tests entirely. They’ve done their job.

Duck Typer is cross-cutting. It checks whether a group of objects agrees on a shared interface. The question isn’t “does StripeProcessor have a charge method?” but “do StripeProcessor, PaypalProcessor, and BraintreeProcessor all define charge with the same signature?” That’s a fundamentally different concern, and one that single-object matchers can’t express.

“But it doesn’t catch errors in production”

Some prefer an approach where the interface is validated at class load time: declare the contract, and if a class doesn’t conform, raise a RuntimeError immediately. That way, mismatches surface as errors in production rather than only in tests.

That’s a valid approach, although not exactly great. In a typed language, an interface mismatch would never be deployed because the code wouldn’t compile. Ruby doesn’t have a compiler, but it has its own equivalent: the test suite. And guess what inhibits bad deployments in Ruby projects? In all my years working with Ruby, I’ve never seen a project without a CI pipeline. If tests fail, your code doesn’t get deployed. In practice, the safety net is the same.

On top of that, runtime checks add metaprogramming to your production code, and you’d still need tests to verify the setup is correct.

That’s why Duck Typer deliberately stays in the test suite: it’s Ruby’s natural place to enforce constraints like this, and your implementation stays clean, without workarounds that try to mimic static typing at runtime.

If you want compile-time or runtime guarantees, tools like Sorbet or RBS take a fundamentally different approach to the same problem and you wouldn’t need Duck Typer. That said, Duck Typer gives you some of those benefits with a fraction of the effort, at least when it comes to interfaces.

Wrapping up

Duck Typer won’t replace your behavior tests, and it was never meant to. It’s a small, focused tool that gives you targeted feedback when interfaces drift. It’s usually a one-liner to add, has no runtime dependencies, and lives only in your test environment. If you value clear error messages and care about keeping polymorphic classes in sync, give it a try.