Skip to content

randycoulman/DoubleAgents

Repository files navigation

DoubleAgents

Maintenance Status

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.

Introduction

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

Creating a DoubleAgent

There are four kinds of DoubleAgent:

  • A standalone DoubleAgent acts as an instance of its target class. This is the normal usage. A standalone DoubleAgent 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 the DoubleAgent.

  • 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 standalone DoubleAgent 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-side DoubleAgent 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 the DoubleAgent.

  • 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 like Timer class>>after:do: will hang the debugger. An in-place class-side DoubleAgent is created by sending #classSideDouble to the class, or by explicitly creating the DoubleAgent.

  • 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-instance DoubleAgent is created by sending #anyInstanceDouble to the class, or by explicitly creating the DoubleAgent.

  • aClass anyInstanceDouble

  • DoubleAgent forAnyInstanceOf: AClass

Stubbing and Mocking Methods

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.

Return Values

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, then anObject is returned from every call to the method.
  • If the API call ends with do: aBlock, then aBlock 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.

Stubbing a Method

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.

Disallowing a Message

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.

Defining Expectations for a Method

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

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.

Verifying that Expectations Are Met

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 DoubleAgents 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 registered DoubleAgents and then ensures that any cleanup actions they need to perform are done. Only the first BurnNotice will be reported. All cleanup actions will be performed even if a BurnNotice is raised, and even if a cleanup action raises an exception. This method should be called from the tearDown of your test class. If your tearDown method performs other actions that might fail, it is recommended that you use an #ensure: block to guarantee that Agency class>>tearDown is sent in all cases.

  • Agency class>>setUp verifies that the Agency was torn down correctly by the last test that used it. If not, a BurnNotice will be raised. This may not help figure out which test failed to tear down the Agency, but will alert you to a problem and ensure that each test starts out in a clean state. This method should be called from the setUp of your test class.

  • If you use SUnitToo, DoubleAgentTestCase (in DoubleAgents-SUnitToo) implements setUp and tearDown methods that forward to the Agency. You can have your test class inherit from DoubleAgentTestCase to ensure that the Agency is managed properly. Make sure that your setUp and tearDown also send to super in addition to their own actions. If your tearDown method performs other actions that might fail, it is recommended that you use an #ensure: block to guarantee that the superclass tearDown is sent in all cases.

  • Agency class>>verifyAfter: aBlock wraps aBlock with setUp and tearDown calls. This method is handy for a single test that uses DoubleAgents. For multiple tests in a class, though, it is better to use one of the earlier options.

  • aBlock verifyAgents is a handy shortcut for verifyAfter:. You can wrap the body of your test in a block and send verifyAgents to the block.

  • Agency class>>forceReset ensures that the Agency 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.

Flexible vs Strict

By default, standalone DoubleAgents 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 DoubleAgents, 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.

Ordered Sends

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.

Conflicts

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.

Understanding the Code

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

Acknowledgements

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:

Contributing

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.

Contact Information

If you have any questions about DoubleAgents and how to use it, feel free to contact me.

About

Test Double library for Cincom Visualworks Smalltalk.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published