skip to content

In Ruby, what does def gateway.charge(amount) do inside a test, and why do other Gateway instances keep the original charge method?

level: juniorimportance: must knowfreq 55%

answer

  1. one object, one hidden class
  2. consulted before Gateway
  3. gateway.class is unchanged
  4. singleton_methods shows it
  5. other instances untouched

basics

~10 s

def gateway.charge defines a singleton method: it is stored in gateway's own singleton class, which Ruby checks before Gateway, so only that object uses it. Every other Gateway instance still finds Gateway#charge.

solid answer

~40 s

`def gateway.charge(amount)` defines a **singleton method**, a method that belongs to one object. Ruby stores it in that object's **singleton class**, a hidden class created for the object on demand and placed in front of `Gateway` when looking up methods on `gateway` only. A call to `gateway.charge` finds the new definition first; `other.charge` never sees it, because `other` has no such singleton class entry. `gateway.class` still returns `Gateway`, and `is_a?` answers as before; `gateway.singleton_methods` returns `[:charge]` and reveals the change. Inside the new body, `super` reaches `Gateway#charge`. That is why the trick works as a quick, per-object stand-in in a test without touching the class.

code

ruby · 14 lines
ruby
class Gateway
  def charge(amount) = raise("network call")
end

gateway = Gateway.new
other   = Gateway.new

def gateway.charge(amount) = :approved

gateway.charge(10)           # => :approved
gateway.class                # => Gateway
gateway.singleton_methods    # => [:charge]
other.singleton_methods      # => []
other.charge(10)             # RuntimeError: network call

go deeper

for a junior

Recall that def obj.name defines a method on that one object, and that other instances and the object's class are unaffected.

for a middle

Explain the singleton class placed in front of the class in lookup, why super reaches the class's method, and how singleton_methods reveals the change.

for a senior

Use per-object methods only on objects the test owns, prefer a block-based definition when the stub needs test values, and recognise the leak risk on shared objects.

for a principal

Set expectations on when hand-written singleton stubs are acceptable versus a test-double library that restores state automatically.

## What the syntax does `def` normally defines a method in a class or module. Written with an explicit receiver — `def gateway.charge(amount)` — it defines the method on **that one object**. Such a method is a **singleton method**, and it is ordinary Ruby, not a testing feature. ```ruby gateway = Gateway.new def gateway.charge(amount) = :approved gateway.charge(10) # => :approved Gateway.new.charge(10) # => whatever Gateway#charge returns ``` ## Where the method lives Every object can have a **singleton class**: a class that has exactly one instance, the object itself. Ruby creates it the first time something needs it, such as a singleton method definition. Its role in method lookup is simple: 1. When you call `gateway.charge`, Ruby first looks in `gateway`'s singleton class. 2. If the method is not there, it continues to `Gateway`, then to `Gateway`'s ancestors. Other instances have no entry for `charge` in a singleton class of their own, so their lookup goes straight to `Gateway`. That is the whole reason the change stays local. ## What stays the same - **`gateway.class`** still returns `Gateway`. The singleton class is hidden from `class`; you reach it with `gateway.singleton_class`. - **Type checks** such as `gateway.is_a?(Gateway)` are unchanged. - **Instance variables** and the rest of the object are untouched; only its method lookup gained a layer. ## What changes, and how to see it | Question | Before the `def` | After it | |---|---|---| | `gateway.singleton_methods` | `[]` | `[:charge]` | | `gateway.charge(10)` | runs `Gateway#charge` | runs the singleton method | | `other.charge(10)` | runs `Gateway#charge` | runs `Gateway#charge` | | `gateway.class` | `Gateway` | `Gateway` | Inside the singleton method, `super` calls the next definition in lookup, which is `Gateway#charge`, so a wrapper that logs and delegates is possible: ```ruby def gateway.charge(amount) puts "charging #{amount}" super end ``` ## Using it in a test Giving one object a custom method is a lightweight way to replace a collaborator's behaviour for a single test: - It needs no library and reads as plain Ruby. - It affects only the object you built for the test. - It cannot be undone easily, so it suits **objects created inside the test** and discarded afterwards, not shared ones. A method body written with `def` cannot see the test's local variables, because `def` starts a new scope. When the stub needs to return a value computed in the test, `define_singleton_method(:charge) { |amount| result }` takes a block that closes over `result`. ## def obj.x among the alternatives | Technique | Scope | Sees test locals? | |---|---|---| | `def gateway.charge` | this object only | no, `def` opens a new scope | | `gateway.define_singleton_method(:charge) { ... }` | this object only | yes, the block is a closure | | mixing a named module into this one object | this object only | no, the module is defined elsewhere | | a small subclass or stand-in class | every instance you build from it | no | The first two write into the same singleton class and differ only in how the body is written. A named module is reusable across tests and easier to recognise in a backtrace. A stand-in class avoids touching the real object at all, which is often the cleanest option when the collaborator is passed in. ## Mistakes to avoid - Believing `def gateway.charge` changes `Gateway` for every instance. - Expecting `gateway.class` to report a new class. - Using a local variable of the test inside a `def` body. - Applying the trick to a long-lived shared object, where the method stays for the rest of the process. ## The model to keep Each object can carry a private, hidden class holding methods that belong to it alone. `def obj.name` writes into that class; every call on `obj` checks it first. Class methods, `class << self` and `singleton_methods` are all views of this one mechanism.

  • Inside def gateway.charge, what does a bare super call?
    The next `charge` in `gateway`'s method lookup, which is `Gateway#charge` (or whatever its ancestors provide). The singleton class sits in front of the class, so a singleton method can wrap the class's version: do something extra, then call `super`.
  • Why does def gateway.charge = result fail to see a local result defined in the test?
    `def` opens a new scope that sees no outer locals. To return a value from the test, use `gateway.define_singleton_method(:charge) { |_amount| result }`; the block is a closure, so `result` stays visible.

A singleton method is like a sticky note on one printed copy of a manual: whoever reads that copy sees the note before the printed page, while every other copy stays as printed.

saying these in an interview costs you the question

  • def gateway.charge redefines charge for every Gateway instance
  • After def gateway.charge, gateway.class returns a new anonymous class
  • A singleton method cannot call the class's version with super
  • A def body inside a test can read the test's local variables
  • Singleton methods are a testing-library feature, not core Ruby