Create and use "test doubles" in unit tests.
DoubleAgents is licensed under the MIT license. See the copyright tab in the RB, the 'notice' property of this package, or the License.txt file on GitHub.
DoubleAgents' primary home is the Cincom Public Store Repository. Check there for the latest version. It is also on GitHub.
DoubleAgents was developed in VW 7.10.1, but is compatible with VW 7.7 and later. If you find any incompatibilities with VW 7.7 or later, let me know (see below for contact information) or file an issue on GitHub.
DoubleAgents is a library for creating and working with "test doubles" (think "stunt doubles" in acting) when writing unit tests. It is optimized for keeping test code as readable as possible.
DoubleAgents supports two kinds of test doubles. See Mocks Aren't Stubs for more information.
-
Stubs are simple objects that don't have any real behavior. Each stubbed method does no work and returns a given value.
-
Mocks are objects that are pre-programmed with a set of "expectations". After running the code under test, the mock is checked to see if all of the expectations have been met.
In this library, all test doubles are instances of DoubleAgent
and
can contain a mixture of stubbed methods and expectations.
DoubleAgent
s can be created directly and used in place of instances
of other classes. In addition, it is possible to stub or mock methods
on existing instances (including class-side methods of existing
classes). It is also possible to stub or mock methods on all
instances of a class. In these latter cases, all non-doubled
methods will continue to function like normal.
DoubleAgents was designed to support unit testing in the style recommended by Sandi Metz in her excellent Practical Object-Oriented Design in Ruby: An Agile Primer. See her slides from a talk at Ancient City Ruby for a good summary of her advice.
There are four kinds of DoubleAgent
:
-
A standalone
DoubleAgent
acts as an instance of its target class. This is the normal usage. A standaloneDoubleAgent
is created by sending one of the#expect:*
or#stub:*
API methods directly to a class, by sending#doubleAgent
to the class, or by explicitly creating theDoubleAgent
. -
AClass expect: #aMessage [...]
-
AClass stub: #aMessage [...]
-
AClass doubleAgent
-
DoubleAgent of: AClass
-
An in-place instance-side
DoubleAgent
is used for objects that are part of a more complex object structure where it is difficult to inject a standaloneDoubleAgent
as a dependency. It allows selective mocking and stubbing, where only a few methods are doubled and all other methods have their normal behavior. An in-place instance-sideDoubleAgent
is created by sending one of the#expect:*
or#stub:*
API methods directly to the instance, by sending#doubleAgent
to the instance, or by explicitly creating theDoubleAgent
. -
anInstance expect: #aMessage [...]
-
anInstance stub: #aMessage [..]
-
anInstance doubleAgent
-
DoubleAgent around: anInstance
-
An in-place class-side
DoubleAgent
is similar to an in-place instance-side double, but is used for mocking or stubbing class-side methods of a class. Note that doubled methods on classes are global throughout the system, so use this facility with care. For example, attempting to stub something likeTimer class>>after:do:
will hang the debugger. An in-place class-sideDoubleAgent
is created by sending#classSideDouble
to the class, or by explicitly creating theDoubleAgent
. -
aClass classSideDouble
-
DoubleAgent around: AClass
-
An any-instance
DoubleAgent
is used when it is necessary to mock or stub methods on instances of a class that are not directly visible to the test. Any-instance doubles should be used very rarely, as they generally suggest that there is a more fundamental problem with the design of the code under test. However, there are times when they are needed. An any-instanceDoubleAgent
is created by sending#anyInstanceDouble
to the class, or by explicitly creating theDoubleAgent
. -
aClass anyInstanceDouble
-
DoubleAgent forAnyInstanceOf: AClass
There is a family of methods for creating method stubs and defining
method expectations. These methods can be sent to a DoubleAgent
, to
a class (which automatically creates and returns a DoubleAgent
), or
to an object (which stubs or mocks the method "in place").
A method may only be mocked or stubbed if it is understood by the class or instance being doubled. This is to provide extra assurance that the test double uses the same API as the real class.
All stubbed and mocked methods return a value when they are called.
- If the API call doesn't specify a return value, then
self
itself is returned. - If the API call ends with
return: anObject
, thenanObject
is returned from every call to the method. - If the API call ends with
do: aBlock
, thenaBlock
is evaluated with the arguments passed to the method, and the result is returned. Note that the arguments are "culled" to the block, so the block doesn't need to specify the arguments if it doesn't need them.
To stub a method, use one of the following:
stub: aMessage
stub: aMessage return: anObject
stub: aMessage do: aBlock
These methods do nothing but return a value as outlined above.
If the arguments are important, use one of the following:
stub: aMessage with: anObject
stub: aMessage with: anObject with: anotherObject
stub: aMessage with: anObject with: anotherObject with: aThirdObject
stub: aMessage withArguments: aCollection
stub: aMessage with: anObject return: anObject
stub: aMessage with: anObject with: anotherObject return: anObject
stub: aMessage with: anObject with: anotherObject with: aThirdObject return: anObject
stub: aMessage withArguments: aCollection return: anObject
stub: aMessage with: anObject do: aBlock
stub: aMessage with: anObject with: anotherObject do: aBlock
stub: aMessage with: anObject with: anotherObject with: aThirdObject do: aBlock
stub: aMessage withArguments: aCollection do: aBlock
The general forms are the #stub:withArguments:*
methods; the others
are provided as convenient shortcuts. These stubs are used when
aMessage
is sent with arguments that are "congruent" (see below) to
those specified. They do nothing else except return a value as outlined above.
For even more flexible argument checking, use one of the following:
stub: aMessage where: aBlock
stub: aMessage where: aBlock return: anObject
stub: aMessage where: aBlock do: returnBlock
These stubs are used when aMessage
is sent with arguments that
satisfy aBlock
. aBlock
must take some or all of the arguments and
return a Boolean indicating whether the arguments are satisfactory.
They do nothing else except return a value as outlined above.
The argument-matching forms of the #stub:*
methods are only
useful when you need to respond differently to a stubbed message
depending on one or more of the arguments. This is needed
occasionally, but only after careful consideration of other options.
In general, you should prefer non-argument-matching forms above.
Sometimes, it is desirable to explicitly state that a particular message will not be sent to an object. To disallow a message send, use the following:
disallow: aMessage
If a disallowed message is sent, a BurnNotice
is raised.
In order to verify that a message is sent to an object, use one of the following:
expect: aMessage
expect: aMessage return: anObject
expect: aMessage do: aBlock
These methods check that aMessage
was sent, but do not perform any
checks on the arguments. They do nothing else except return a value
as outlined above.
If the arguments are important, use one of the following:
expect: aMessage with: anObject
expect: aMessage with: anObject with: anotherObject
expect: aMessage with: anObject with: anotherObject with: aThirdObject
expect: aMessage withArguments: aCollection
expect: aMessage with: anObject return: anObject
expect: aMessage with: anObject with: anotherObject return: anObject
expect: aMessage with: anObject with: anotherObject with: aThirdObject return: anObject
expect: aMessage withArguments: aCollection return: anObject
expect: aMessage with: anObject do: aBlock
expect: aMessage with: anObject with: anotherObject do: aBlock
expect: aMessage with: anObject with: anotherObject with: aThirdObject do: aBlock
expect: aMessage withArguments: aCollection do: aBlock
The general forms are the #expect:withArguments:*
methods; the
others are provided as convenient shortcuts. These methods check that
aMessage
was sent with arguments that are "congruent" (see below) to
those specified. They do nothing else except return a value as
outlined above.
Argument congruency is implemented using the #===
method provided by
the Threequals package.
See that package for more details, but as a summary:
- Objects that are
#=
are also#===
. - A class is
#===
to an object that#isKindOf:
the class. - A block is
#===
to an object if it evaluates to true when passed the object. - An interval is
#===
to a number that is between the endpoints of the interval, including the endpoints. - If Threequals-Regex is loaded, a regular expression is
#===
to a string that matches it.
Using #===
allows for expectations like the following:
myDouble expect: #with:and:do:
with: (40 to: 42)
with: [:x | x even]
with: BlockClosure
This expectation will be satisfied if the arguments are some number between 40 and 42 inclusive, an even number, and a block.
For even more flexible argument checking, use one of the following:
expect: aMessage where: aBlock
expect: aMessage where: aBlock return: anObject
expect: aMessage where: aBlock do: returnBlock
These methods check that aMessage
was sent with arguments that
satisfy aBlock
. aBlock
must take some or all of the arguments and
return a Boolean indicating whether the arguments are satisfactory.
They do nothing else except return a value as outlined above.
If an unexpected message is sent to a DoubleAgent
, it will
immediately raise a BurnNotice
exception. It is necessary to check
that all expectations are met at the end of a test. All DoubleAgents
register with a singleton instance of Agency
. Agency
is
responsible for verifying all of the DoubleAgent
s and for ensuring
that they clean up after themselves. If an expectation is not met, a
BurnNotice
exception will be raised.
Agency
provides several options for verification and cleanup.
-
Agency class>>tearDown
verifies all registeredDoubleAgent
s and then ensures that any cleanup actions they need to perform are done. Only the firstBurnNotice
will be reported. All cleanup actions will be performed even if aBurnNotice
is raised, and even if a cleanup action raises an exception. This method should be called from thetearDown
of your test class. If yourtearDown
method performs other actions that might fail, it is recommended that you use an#ensure:
block to guarantee thatAgency class>>tearDown
is sent in all cases. -
Agency class>>setUp
verifies that theAgency
was torn down correctly by the last test that used it. If not, aBurnNotice
will be raised. This may not help figure out which test failed to tear down theAgency
, but will alert you to a problem and ensure that each test starts out in a clean state. This method should be called from thesetUp
of your test class. -
If you use SUnitToo,
DoubleAgentTestCase
(in DoubleAgents-SUnitToo) implementssetUp
andtearDown
methods that forward to theAgency
. You can have your test class inherit fromDoubleAgentTestCase
to ensure that theAgency
is managed properly. Make sure that yoursetUp
andtearDown
also send tosuper
in addition to their own actions. If yourtearDown
method performs other actions that might fail, it is recommended that you use an#ensure:
block to guarantee that the superclasstearDown
is sent in all cases. -
Agency class>>verifyAfter: aBlock
wrapsaBlock
withsetUp
andtearDown
calls. This method is handy for a single test that usesDoubleAgent
s. For multiple tests in a class, though, it is better to use one of the earlier options. -
aBlock verifyAgents
is a handy shortcut forverifyAfter:
. You can wrap the body of your test in a block and sendverifyAgents
to the block. -
Agency class>>forceReset
ensures that theAgency
is cleaned up correctly, but does not perform any verification. This method should not be used in normal circumstances, but can be used in a pinch if your image gets left in a bad state somehow.
By default, standalone DoubleAgent
s are "strict". That is, they
only allow messages to be sent that have been stubbed or mocked. All
other message sends raise a BurnNotice
. For in-place
DoubleAgent
s, messages that have not been stubbed or mocked
implement their normal behavior. A "flexible" DoubleAgent
will
allow other messages to be sent; they will simply return self
.
DoubleAgent
implements #flexible
and #strict
to convert between
the two.
By default, mock expectations are "unordered". That is, the messages
can be sent to the DoubleAgent
in any order. An "ordered"
DoubleAgent
requires the messages to be sent in the specified order.
It will raise a BurnNotice
if any messages are sent out of order.
DoubleAgent
implements #ordered
and #unordered
to convert
between the two.
The same method can be mocked or stubbed repeatedly. The rule is "last one wins". That is, if you first stub a method, and then later set a mock expectation on it, then the method will be a mock that is verified. Similarly, if you set a mock expectation on a method and then later stub the same method, then it will be a stub. Setting a mock expectation on a method that is already a mock simply adds the new expectation to the existing expectations; it means that the method must be sent more than once.
A common pattern is to stub a method in a test's setUp
, and then in
one or more tests, set a mock expectation to verify that the message
is sent to the object.
DoubleAgent
is the central class in this library. It has subclasses
that implement the four main types of agent: StandaloneDouble
,
InPlaceInstanceDouble
, InPlaceClassDouble
, and
AnyInstanceDouble
.
All DoubleAgent
s register with the singleton Agency
, which is
responsible for verifying all mock expectations and cleaning up.
When verifying expectations, method arguments are verified by an
ArgumentPolicy
such as IgnoreArguments
, ArgumentsEqual
, or
ArgumentsMatch
.
Doubled methods are represented by a MethodDouble
, either
MockMethod
or a StubMethod
.
I stood on the shoulders of several giants when implementing this library.
As already mentioned, I was inspired to write this library by trying to follow Sandi Metz's testing advice in Practical Object-Oriented Design in Ruby: An Agile Primer.
I looked at several other test double libraries for API and implementation ideas, including:
The in-place double implementations use some clever tricks that were inspired by the MethodWrappers project and a couple of blog posts by Travis Griggs:
I'm happy to receive bug fixes and improvements to this package. If you'd like to contribute, please publish your changes as a "branch" (non-integer) version in the Public Store Repository and contact me as outlined below to let me know. I will merge your changes back into the "trunk" as soon as I can review them.
If you have any questions about DoubleAgents and how to use it, feel free to contact me.
- Web site: http://randycoulman.com
- Blog: Courageous Software (http://randycoulman.com/blog)
- E-mail: randy at randycoulman dot com
- Twitter: @randycoulman
- GitHub: randycoulman