There is one programming pattern that I sometimes use which I wanted to tell you about today. I called this technique yield default object. I am pretty sure it was already presented somewhere, by someone, but I could not find any good reference.

Imagine code like this:

def start_till_cash_register_session
  cmd = Till::StartNewSession.new(
    uuid: SecureRandom.uuid,
    terminal_name: "2nd floor, nr 2103",
    employee_name: "Jurgen Klinsman",
    organizer_id: user_id,
    currency: currency,
    starting_cash_balance: "0.00",
  )
  command_bus.call(cmd)
end

It doesn’t matter where it is located (TestActor btw), what it does, or what’s the full context (selling in a boutique/box office/museum). We will be looking at it from a mechanical perspective.

So the code is all good, and provides some nice defaults that we can use in our tests. However, sometimes we would like to override those defaults. What options do we have?

default named arguments

We could use default named arguments:

def start_till_cash_register_session(
    uuid: SecureRandom.uuid,
    terminal_name: "2nd floor, nr 2103",
    employee_name: "Jurgen Klinsman",
    starting_cash_balance: "0.00",
  )

  cmd = Till::StartNewSession.new(
    uuid: uuid,
    terminal_name: terminal_name,
    employee_name: employee_name,
    organizer_id: user_id,
    currency: currency,
    starting_cash_balance: starting_cash_balance,
  )
  command_bus.call(cmd)
end
start_till_cash_register_session(employee_name: "Batman")

I don’t know about you, but that feels a little verbose to me. On the other hand the code is very grep-able and if you later want to rename something it should be pretty straight-forward to rename any argument and find where they are being used.

After years of using Ruby and Rails, that’s something that I value highly.

It would also work very nice with code-editors that will be capable of showing nicely all arguments that you can pass to the method.

merge / reverse_merge

We can use the double splat operator and handle any named arguments.

def start_till_cash_register_session(**attributes)
  defaults = {
    uuid: SecureRandom.uuid,
    terminal_name: "2nd floor, nr 2103",
    employee_name: "Jurgen Klinsman",
    organizer_id: user_id,
    currency: currency,
    starting_cash_balance: "0.00",
  }
  cmd = Till::StartNewSession.new(defaults.merge(attributes))
  command_bus.call(cmd)
end
start_till_cash_register_session(employee_name: "Batman")

It’s definitely less verbose, but you might not get any errors in case of typos (depends on how the StartNewSession constructor is implemented, whether it will silently ignore additional attributes or not). And you won’t get autocomplete.

yield default object

Here is another approach. One that I wanted to show you.

def start_till_cash_register_session
  cmd = Till::StartNewSession.new(
    uuid: SecureRandom.uuid,
    terminal_name: "2nd floor, nr 2103",
    employee_name: "Jurgen Klinsman",
    organizer_id: user_id,
    currency: currency,
    starting_cash_balance: "0.00",
  )
  yield cmd if block_given?
  command_bus.call(cmd)
end

Instead of passing the attributes around we are passing the whole command object built with defaults by yield-ing it to the caller.

start_till_cash_register_session{|cmd| cmd.employee_name = "Batman" }

For me there is certain appeal to this solution.

And if you use the fluent interface as well you could have:

start_till_cash_register_session do |cmd| 
  cmd.employee_name("Batman").starting_cash_balance("200.00")
end

Of course the number of solutions to this problem is infinite and you could mix and match those approaches (for example by yielding attributes instead of the object).

Which approach do you use in your test?

P.S. Yes, I know about factory_girl, you don’t need to mention it.

Want to know more?

Check out our video course Hands-on Ruby, TDD, DDD - a simulation of a real project which contains 51 short videos, each one discussing a small refactoring or technique.

Use discount code YIELD_DEFAULT to purchase with 50% discount. The offer expires on Fed 10, 2017.