When is a Mock not a Mock?
As I mentioned in an earlier article, whilst we may call Amoss a "Mock Object Framework", it's more accurately called a "Test Double Library". Why might I say that?
When people usually talk about Mock objects, they usually mean one of a number of different things - and one thing any software developer should abhor is ambiguity in terms!
The Amoss documentation takes its lead from Gerard Meszaros's excellent book "xUnit Test Patterns: Refactoring Test Code" when it comes to terminology.
In the book, Gerard lays out quite a number of patterns for Unit Testing, it's a highly recommended read for anyone serious about Unit Testing.
Amongst the many definitions are a series of patterns on when and where to use different types of "Test Doubles" and he defines clear terms for each of the scenarios. They're extremely useful when talking, and thinking, about how to approach a particular test.
Amoss focusses on 3 such types (and I'll describe 4, for reasons that should become immediately clear).
Test Double
A Test Double isn't really a type in its own right. Instead, it's a super-set, and synonymous with what people often mean when they say "Mock Object".
It is any object that is used in a test as a replacement for a real system object. This is true regardless of the reason for its use.
Everything that Amoss does is to present "Configurable Test Doubles" for use in tests, hence why I say it is more accurately called a "Test Double Library".
Test Stub
A Test Stub is the simplest use case of a Test Double.
It defined as a Test Double that is used to replace a real system object primarily in order to direct a test down a particular path. That is, it is used to define the "Indirect Inputs" into the "system under test" (SUT).
By "Indirect Inputs" we mean the inputs into the SUT that are not directly provided via such as the values of parameters / member variables of the SUT which are directly settable in the test.
The point being that it is easy for us to build a stub and direct it to behave in a particular way, when it may be difficult to do that with the real system object.
For example, you may use a "Stub" version of an method to force an Exception to be thrown when that method is called, or to ensure that 'false' is always returned when that method is called.
In Amoss, this is represented by the definition of method behaviour using "when" and "allows". Often this is done without specifying the parameters that are required for the Stub to exhibit the behaviour.
Test Spy
A Spy takes the concept a little further.
Whilst a Test Spy may be used, like a Stub, to direct execution down a particular path, it is also used in order to test the behaviour the "Indirect Outputs" of the method under test.
By "Indirect Outputs" we mean the outputs from the method that are not directly provided (I.E. by returning values or mutating parameters). Instead, these are behaviours that are applied to other objects.
That is, we may choose a "Test Spy" when we want to test that the SUT calls certain methods against other objects, passing particular values.
Generally, this is done by keeping track of the methods and parameters that are passed, and then once the SUT has been acted upon, asking the Stub objects what they were passed.
In Amoss, this is represented by defining a Test Stub using "when" and "allows", and once the test's 'Act' is complete, asking the Stub for a record of the parameters that were passed to particular methods.
Mock Object
A Mock Object fulfils the same fundamental purpose as a Test Spy, but does so in a slightly different way.
With a "Test Spy" it is the responsibility of the Unit Test code to verify that the expected methods were called, and with the expected parameters passed.
A Mock Object has the capability of "asserting" itself, checking:
- The expected methods are called, and are passed the expected parameters.
- The methods are called in the order they are expected.
- No other methods are called, and no methods are called with unexpected parameters.
In Amoss, this is performed by defining method behaviours using "expects", and then before performing any assertions calling 'verify' to ensure that all the expected methods were called.
This is valuable for checking the absolute precise order of execution of a SUT, and results in a high degree of validation for a small amount of test code.
Combining the concepts in a single object
Since Amoss does not distinguish between these types in any way other than how the method behaviours are defined, it is possible to create a Test Double that combines the capabilities of a Test Stub and Mock Object (for example).
We may decide, for a given test, that we do not care what parameters are passed into one method, whilst wanting to ensure that another method is called exactly once, and with certain parameters.
With Amoss, we can do that, defining some behaviours as 'when', and others as 'expects'.
This gives you great flexibility when defining how your Test Doubles will behave.
Conclusion
Amoss and other Mock Object Frameworks provide a lot of options that allow you to define the behaviours of your "Test Doubles" in different ways. Understanding what those different behaviours are, and assigning clear labels to them allows us to talk clearly and concisely about them and ultimately helps us choose which are most appropriate at any given time.
One of the strengths of Amoss is that it can provide all these capabilities within a single definition - a Mock Object can be defined with some Stub capabilities and vice versa. Fundamentally they are the same thing - it's just about how you use them.
For more information, and to download and use Amoss - take a look at the full documentation and codebase here: https://github.com/bobalicious/amoss
Full disclosure - Amoss is an Open Source project to which I am currently the sole contributor.