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

推荐订阅源

The Last Watchdog
The Last Watchdog
C
Cyber Attacks, Cyber Crime and Cyber Security
L
LINUX DO - 热门话题
G
GRAHAM CLULEY
S
Schneier on Security
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
IT之家
IT之家
阮一峰的网络日志
阮一峰的网络日志
Recorded Future
Recorded Future
I
Intezer
云风的 BLOG
云风的 BLOG
博客园 - Franky
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
T
Tenable Blog
The Hacker News
The Hacker News
T
The Blog of Author Tim Ferriss
Attack and Defense Labs
Attack and Defense Labs
D
DataBreaches.Net
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
News and Events Feed by Topic
有赞技术团队
有赞技术团队
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
N
News and Events Feed by Topic
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
S
Secure Thoughts
The Register - Security
The Register - Security
B
Blog
Security Archives - TechRepublic
Security Archives - TechRepublic
The Cloudflare Blog
Webroot Blog
Webroot Blog
W
WeLiveSecurity
H
Heimdal Security Blog
博客园 - 三生石上(FineUI控件)
V
Vulnerabilities – Threatpost
G
Google Developers Blog
O
OpenAI News
V
V2EX
罗磊的独立博客
博客园_首页
N
News | PayPal Newsroom
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
TaoSecurity Blog
TaoSecurity Blog
Cloudbric
Cloudbric
H
Hacker News: Front Page
博客园 - 叶小钗
T
Tor Project blog
AI
AI

Giant Robots Smashing Into Other Giant Robots

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 The Bike Shed Ep 505: What is a “principal” or “staff” engineer? Your vibe coded website is going to get you fined Roux’s New Component Library Why we're choosing stewardship over an exit The Bike Shed Ep 503: Seeing the Graph for the Trees Announcing Shoulda Matchers 8.0: validate multiple attributes in one line AI's "overnight" solution for our flaky tests took two weeks to adopt The Playwright debugging tool Rails devs aren't using Meet thoughtbot at Brighton Ruby 2026 614: AI Code Audits The mistake I didn't realise I was making when designing workshops AI crawlers are inflating your view counts 502: Apps That Make Our Work Go Toast: the 2-minute test that reveals how you think about building products How I Built a Chrome Extension Wrapper (and Everything That Tried to Stop Me) Enforcing Your Ruby Style Guide on AI-Generated Code Copy as Markdown: AI-friendly blog posts 501: What makes for good technical writing? The Four Signals of AI Observability Can you really launch a tech business with a no-code app builder? 612: Do fish drink? This week in #dev (May 15, 2026) Lost, forgotten, and unfamiliar HTML 500: Celebrating with past hosts Biometrics authentication for your mobile app
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.