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
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