More RDingus

1 view
Skip to first unread message

Jonathan Penn

unread,
Dec 16, 2008, 11:30:10 PM12/16/08
to CleRB
Well, I kept running with this and decided to promote what I've done
from a gist to a full github project. I found it easier to break out
the classes and specs into the appropriate directories. It also made
running autospec easier. :-)

http://github.com/jonathanpenn/rdingus/

I wrote "spec/rdingus_exaples_spec.rb" as a collection of high level
examples of usage. I also used that file as a guide for what I wanted
the syntax to ultimately look like in my version.

I'm not married to this implementation, so feel free to fork and hack
away at it if you see a better set of solutions.

When Gary was talking at CleRB, he mentioned that he wished he had
some way to separate the setup phase from the usage phase (if I
remember correctly). I decided to shoot for that kind of separation
and came up with these three phases below.

1. The optional setup phase.

# Define the method :subscription to return whatever we want
d = RDingus.new
d._define.subscription { @a_subscription }


2. The usage phase

# Somewhere in the system, someone does something like this...
puts d.subscription


3. The checking phase

# In test/unit
assert d._calls[:subscription]

# In Rspec
d._calls.should include(:subscription)
# or
d._calls[:subscription].should_not be_nil


The two main methods to use are "_define" and "_calls". I really
didn't want to use an underscore in the name, but I couldn't think of
a better way that wouldn't clash with what we might want to record
with the dingus.

Again, check "spec/rdingus_examples_spec.rb" for the different ways to
interact with it.

And I'm open for suggestions.

Gary Bernhardt

unread,
Dec 17, 2008, 1:03:18 PM12/17/08
to cl...@googlegroups.com
On Tue, Dec 16, 2008 at 11:30 PM, Jonathan Penn <jonath...@gmail.com> wrote:
>
> I wrote "spec/rdingus_exaples_spec.rb" as a collection of high level
> examples of usage. I also used that file as a guide for what I wanted
> the syntax to ultimately look like in my version.
>
> I'm not married to this implementation, so feel free to fork and hack
> away at it if you see a better set of solutions.
>
> When Gary was talking at CleRB, he mentioned that he wished he had
> some way to separate the setup phase from the usage phase (if I
> remember correctly). I decided to shoot for that kind of separation
> and came up with these three phases below.
>
(phase details snipped)

I think my ideas were a little more grand than I expressed. You've broken it
up syntactically on the dingus itself, which is great, but my idea was to
separate the test phases within the testing framework itself.

The way that we write tests now is incomplete. There's no way to define an
explicit "exercise the system under test" phase. What I'd like to see is an
explicit recognition of all four phases. That would allow dinguses to only
record the exact interaction that's under test. Example:

def setup():
# Set up any necessary dinguses and dingus customizations. No dingus
# recording is done here, so we can set the dinguses up by accessing them
# directly:
some_dingus.foo().returns(5)
# and we don't have to worry about the calls being recorded. This is more
# natural than having a special _define attribute on the dingus. (Dingus
# setup is rare, but when you need it you *really* need it.)

def exercise():
# Perform the action we're testing (instantiate the object, call some
# method on it, or whatever). This step should be *exactly* one line of
# code. Dingus recording is on for this phase, and *only* for this phase.

def make_assertions():
# What it sounds like. Recording is off, so once again we can use direct
# calls to get at objects and make assertions about them. We don't have to
# worry about them polluting the recordings from the exercise phase:
assert some_dingus.foo().called
# or maybe just:
assert some_dingus.foo()
# (I haven't thought about it much yet.) This phase gets combined with the
# "exercise" phase in current testing systems, which is the problem.

def teardown():
# HURRRRR

tl;dr: Splitting the "test" step into separate "exercise" and "assert" steps
obviates the need for special dingus methods for setting up and querying
calls.

This change is especially good when you want to get at a return value.
Sometimes I'll have to do something like
some_return = some_dingus.foo.return_value.bar.return_value
but with the changes above I could do
some_return = some_dingus.foo().bar()

(Pulling return values out of nested objects is something that happens a lot
when you're writing fully isolated unit tests. A lot of code you'll interact
with will force those kinds of nasty interactions on you, so your dingus has
to be able to cope.)

> The two main methods to use are "_define" and "_calls". I really
> didn't want to use an underscore in the name, but I couldn't think of
> a better way that wouldn't clash with what we might want to record
> with the dingus.

That is a problem, and it's exactly why so many mocking libraries have some
kind of mock manager that you create first, and then use to create the mocks
themselves. That's total weaksauce, though, and I won't have it. I like the
way you've handled it; I think it's a good compromise for existing test
runners. But I'm more excited about how a full, four-step test runner could do
it. (There's a reason I've been working on my own spec runner lately. ;)

--
Gary
http://blog.extracheese.org

Jonathan Penn

unread,
Dec 17, 2008, 2:20:53 PM12/17/08
to CleRB
> I think my ideas were a little more grand than I expressed. You've broken it
> up syntactically on the dingus itself, which is great, but my idea was to
> separate the test phases within the testing framework itself.
>
> The way that we write tests now is incomplete.  There's no way to define an
> explicit "exercise the system under test" phase.  What I'd like to see is an
> explicit recognition of all four phases. That would allow dinguses to only
> record the exact interaction that's under test. Example:
> --examples deleted---

Great thoughts Gary. I like the idea of having a framework that knows
how to tell whether to setup the dingus or use it. I've never messed
with extending a test framework, so I don't know how easy that would
be to return a different kind of object in test's setup() or rspec's
before(). But that idea sounds intriguing.

> (Pulling return values out of nested objects is something that happens a lot
> when you're writing fully isolated unit tests. A lot of code you'll interact
> with will force those kinds of nasty interactions on you, so your dingus has
> to be able to cope.)

Yeah, that can be tough. I don't know how deep you had a chance to
look at what I came up with, but I tried to make declaring a nested
object's return value dead simple. To wit:

dingus._define.method1.method2 { "return value" }
puts dingus.method1.method2("some argument") # -> "return value"
dingus._calls[:method1][:method2].args.first == "some argument"

I don't like how ugly it looks, still. But it's a start.

> > The two main methods to use are "_define" and "_calls".  I really
> > didn't want to use an underscore in the name, but I couldn't think of
> > a better way that wouldn't clash with what we might want to record
> > with the dingus.
>
> That is a problem, and it's exactly why so many mocking libraries have some
> kind of mock manager that you create first, and then use to create the mocks
> themselves. That's total weaksauce, though, and I won't have it. I like the
> way you've handled it; I think it's a good compromise for existing test
> runners. But I'm more excited about how a full, four-step test runner could do
> it. (There's a reason I've been working on my own spec runner lately. ;)

I'd be really interested in seeing how your spec runner develops. And
having dingus be the built in mock-like way of interacting with
systems would be slick.

As far as what I built, I was focusing most on integrating with
current testing environments. This is why I had to use "_define" and
"_calls" on the dingus itself to setup and check. But, you could
easily write a wrapper factory that knows how to setup the dingus for
you (doing the "_define" automatically). And then you can have a
second wrapper object that knows how to query a dingus (doing the
"_call" automatically). And then the spec framework could hide all
that behind a "make_me_a_dingus()" call that knows what do do when.

Thanks for the feedback. Great stuff.

PS. I saw your tweet. I must respectfully disagree. I love the
optional method call parens in ruby. :-P

Joe Fiorini

unread,
Dec 17, 2008, 2:26:04 PM12/17/08
to cl...@googlegroups.com
I'm curious about the _define. Wasn't the whole point of this that we could use method_missing to not have to initially define something before using it?

Great work so far, can't wait to go over this at the next meeting!

- Joe
--
joe fiorini
http://www.faithfulgeek.org
// freelancing & knowledge sharing

Gary Bernhardt

unread,
Dec 17, 2008, 2:30:14 PM12/17/08
to cl...@googlegroups.com
On Wed, Dec 17, 2008 at 2:26 PM, Joe Fiorini <j...@faithfulgeek.org> wrote:
> I'm curious about the _define. Wasn't the whole point of this that we could
> use method_missing to not have to initially define something before using
> it?

Sometimes you need to explicitly define something (e.g., the SUT says
"x < 5", and relies on the result, but x happens to be dingused). By
default, a dingus will just birth more dinguses.

> Great work so far, can't wait to go over this at the next meeting!

Agreed.

--
Gary
http://blog.extracheese.org

Jonathan Penn

unread,
Dec 17, 2008, 2:31:41 PM12/17/08
to cl...@googlegroups.com
It's all about setting up return values for the system under test, and it's only if you need them.

I originally tried it this way:

dingus.foo.returns { "some value" }

But the problem with that is that you have to use "returns" or some other method on the dingus.  It was an implementation mess if you wanted to quickly define nested object return values like so:

dingus.foo.bar.returns { "some value" }

And based on Gary's original comment about wishing for a "setup" phase and a "usage" phase, I thought it'd be best to have this _define method that handles that.  It just returns a DefinitionProxy that also is a blank slate that uses method missing to set up the return values on the parent dingus.

Let me know if this makes sense, Joe, or if I missed something that you see.
Reply all
Reply to author
Forward
0 new messages