NAME Test::Mock::Object - Dead-simple mocking VERSION version 0.1 SYNOPSIS use Test::Mock::Object qw(create_mock read_only); my $r = create_mock( package => 'Apache2::RequestRec', methods => { uri => read_only('/foo/bar'), status => undef, # read/write method headers_in => {}, param => sub { my ( $self, $param ) = @_; my %value_for = ( skip_this => 1, thing_id => 1001, ); return $value_for{$param}; }, }, method_chains => [ # arrayref [ qw/foo bar baz/ => $final_value ], # of arrayrefs ], ); DESCRIPTION Mock objects can be a controversial topic, but sometimes they're very useful. However, mock objects in Perl often come in two flavors: * Incomplete mocks of existing modules * Generic mocks with clumsy interfaces I can never remember This module is my attempt to make things dead-easy. Here's a simple mock object: my $mock = create_mock( package => 'Toy::Soldier', methods => { name => 'Ovid', rank => 'Private', serial => '123-456-789', } ); You can figure out what that does and it's easy. However, we have a lot more. Note, that while "$mocked->isa($package)" will return true for the name of the package you're mocking, but the package will be blessed into a namespace similar to "MockMeAmadeus::$compact_package", where $compact_package is the name of the blessed package, but with "::" replaced with underscores, along with a prepended "_V$mock_number". Thus, mocking something into the "Foo::Bar" package would cause "ref" to return something like "MockMeAmadeus::Foo_Bar_b1". If you need something more interesting for "isa", pass in your own: my $mock = create_mock( package => 'Toy::Soldier', methods => { name => 'Ovid', rank => 'Private', serial => '123-456-789', isa => sub { my ( $self, $class ) = @_; return $class eq 'Toy::Soldier' || $class eq 'Toy'; }, } ); FUNCTIONS These functions are exportable individually or with "::all": use Test::Mock::Object qw( add_method create_mock read_only reset_mocked_calls ); # same as use Test::Mock::Object ':all'; "create_mock( package => $package, methods => \%methods )" use Test::Mock::Object qw(create_mock read_only); my $r = create_mock( package => 'Apache2::RequestRec', methods => { uri => read_only('/foo/bar'), status => undef, # read/write method headers_in => {}, param => sub { my ( $self, $param ) = @_; my %value_for = ( skip_this => 1, thing_id => 1001, ); return $value_for{$param}; }, } method_chains => [ # arrayref [ qw/foo bar baz/ => $final_value ], # of arrayrefs ], ); say $r->uri; # /foo/bar say $r->param('thing_id'); # 1001 say $r->status; # undef $r->status(404); say $r->status; # 404 $r->uri($new_value); # fatal error say $r->foo->bar->baz; # $final_value (from method_chains) We simply declare the package and the methods we need. If the package has not yet been loaded, we alter %INC to ensure the package cannot be loaded after this. This is a convenience if we have a module that's very hard to load. As for the methods, if we point to a coderef, that's the method. If we point to *anything else*, the method will return that value and you can set it to a new value. Arguments to "create_mock()" are: * "package" The name of the package we will mock. Required. * "methods" Key/Value pairs of methods. Keys are method names of the objects and values what those methods return, with one important exception. If the value is a subroutine reference, that reference *becomes* the method for the key. If you want a method to *return* a subroutine reference, you need to wrap that in another subroutine reference. method => sub { sub { ... } } Optional. * "method_chains" (Still experimental) An array reference of array references. Optional. my $mock = create_mock( package => 'Some::Package', method_chains => [ [ qw/ name to_string Ovid / ], [ qw/ name reversed divO / ], [ qw/ foo bar baz 42 / ], ] ); say $mock->name->to_string; # Ovid say $mock->name->reversed; # divO say $mock->foo->bar->baz; # 42 We have incomplete support for chains that might start with the same method. "read_only($value)" When used with a method value, will throw a fatal error if you try to set that value: uri => read_only('/foo/bar') "add_method($mock_object, $method_name, $value)" Just like the key/value pairs to "create_mock()", this adds a getter/setter for $value. If $value is a code reference, it will be added directly as the method. You can make the value read-only, if needed: add_method($mock_object, 'created', read_only(DateTime->now)); "reset_mocked_calls($mock_object)" reset_mocked_calls($mock_object); This reset the "times called" internals to 0 for every method. See "Inside the object". Mocked Methods "isa" if ( $r->isa('Apache2::RequestRec') ) { ... } Returns true if the classname passed in matches the name passed to "create_mock" Inside the object The object returned encapsulates all data thoroughly. However, it's a blessed hashref whos keys are the names of the methods, each pointing to hashref with information about how they were called in the code. So our example above would have this: bless( { 'foo' => { 'times_called' => 1, 'times_with_args' => 0, 'times_without_args' => 1 }, 'headers_in' => { 'times_called' => 0, 'times_with_args' => 0, 'times_without_args' => 0 }, 'isa' => { 'times_called' => 0, 'times_with_args' => 0, 'times_without_args' => 0 }, 'param' => { 'times_called' => 0, 'times_with_args' => 0, 'times_without_args' => 0 }, 'some_object' => { 'times_called' => 0, 'times_with_args' => 0, 'times_without_args' => 0 }, 'status' => { 'times_called' => 0, 'times_with_args' => 0, 'times_without_args' => 0 }, 'uri' => { 'times_called' => 0, 'times_with_args' => 0, 'times_without_args' => 0 } }, 'MockMeAmadeus::Apache2_RequestRec_V1' ) "times_called" is the number of times that method was called in the code you're using it in. "times_with_args" is the number of times that method was called with arguments. "times_without_args" is the number of times that method was called without arguments. Unknown methods The methods you request are the methods you will receive. Any attempt to call unknown methods will be a fatal error. BEST PRACTICES Don't Use Mock Objects See "Interface Changes". However, if you're relying on sometthing you don't control, such as an object that requires a database connection or an internet connection, a mock might be acceptable. Only Mock the Methods You Use You might be tempted to mock every single method in an interface. Don't do that. Only mock the methods that you actually use. That way, if the code is updated to call a method you didn't mock, your test with fail with a "Method not found" error. LIMITATIONS Be aware that while mock objects can be useful, there are several limitations to be aware of. Interface Changes In theory, objects should be open for extension, closed for modification. In practice, we have deadlines, we make mistakes, needs evolve, whatever. If your mock object mocks an instance of "Foo::Bar" and you install a new version of "Foo::Bar" with a different interface, your mock may very well hide the fact that your code is broken. Encapsulation Violations Constantly you see developers do things like this: # don't reach inside! my $name = $object->{name}; And: # this should be an ->isa check if ( ref $object eq 'Toy::Soldier' ) { ... } Both of those will fail with "Test::Mock::Object". This is by design to avoid the temptation to ignore these issues. This might mean that "Test::Mock::Object" is not suitable for your needs. We Changes Instances, Not Classes Thus, if you mock an instance of a base class, subclasses won't see that (and other instances won't see that either). Instead, you might find Mock::Quick useful. Test::MockModule might also help, or if you just need to replace one or two methods in a lexical scope, see Sub::Override. NOTES Memory Leak Protection If you install Test::LeakTrace, a test in our test suite will verify that we do not have memory leaks. I've only tested this on a couple of versions of Perl. It's possible that some versions will leak. Please let me know if this happens. Chained Methods Method chains are often a code smell. You can read about The Law of Demeter for more information. However, breaking a chain sometimes means creating a series of mocks for each method in the chain. So we support method chains. This *is* a code smell. Method chains are fragile. Instead of this: my $office = $customer->region->sales_rep->office; Consider this: # in the Customer class sub regional_office ($self) { return $self->region->sales_rep->office; } And then you can just call: my $office = $customer->regional_office; If the office is then moved directly to the region instead of the salesperson, you can change that method to: sub regional_office ($self) { return $self->region->office; } And your code doesn't break instead of hunting down all of the offending method chains. (Of course, you would do this in all the places where you need to break those chains). That being said, it's often more work than you have time for, so this module provides method chains. Sadly, it's again the difference between theory and practice. SEE ALSO * Test::MockObject I used this years ago when chromatic first wrote it for the company we worked at. I've used it off and on over the years and I *never* remember its interace. * Mock::Quick This one is actually pretty good, but still does a bit more than I want, and doesn't support method chains. * Test2::Tools::Mock This is the successor to Mock::Quick and is included with Test2. If you have Test2 installed, you don't need to install another dependency. * Test::MockModule Another useful module whose interface I find cumbersome, but it uses a completely different approach from this module. * Test::Mock::Apache2 This was missing some methods I needed and is what finally led me to write this module. AUTHOR Curtis "Ovid" Poe COPYRIGHT AND LICENSE This software is Copyright (c) 2021 by Curtis "Ovid" Poe. This is free software, licensed under: The Artistic License 2.0 (GPL Compatible)