Giter VIP home page Giter VIP logo

mockify's People

Contributors

asandau avatar dbucky avatar manwar avatar

Watchers

 avatar  avatar

mockify's Issues

Original functionality should be restored when Mockify goes out of scope

Currently this is not the case, but it is very important.

This proposed test will fail because $Mockify is not allowing itself to be cleaned up when it goes out of scope (its DESTROY method is never called).

sub test_mockRevertsWhenInjectorGoesOutOfScope {
    my $self = shift;
    my $SubTestName = (caller(0))[3];

    my $originalValue = FakeModuleForMockifyTest::DummyMethodForTestOverriding();
    my $mockValue = 'A mock dummy method';

    {
        my $Mockify = Test::Mockify->new('FakeModuleForMockifyTest');
        $Mockify->mockStatic('FakeModuleForMockifyTest::DummyMethodForTestOverriding')->when()->thenReturn($mockValue);

        is(FakeModuleForMockifyTest::DummyMethodForTestOverriding(), $mockValue, "$SubTestName - prove mock is injection");
    }

    is(FakeModuleForMockifyTest::DummyMethodForTestOverriding(), $originalValue, "$SubTestName - prove mock is reverted");
}

This is important to fix because it could cause memory leaks and tests being affected by prior ones in the same run.

When mocking the SUT instead of a dependency, the signature matcher receives an extra argument and fails

I created test_mockSUT to illustrate the problem:

sub test_mockSUT {
    my $self = shift;
    my $SubTestName = (caller(0))[3];

    my $injector = Test::Mockify::Injector->new('FakeModuleStaticInjection');
    $injector->spy('dependency')->when();

    warn FakeModuleStaticInjection->client();

    ok(WasCalled($injector->getVerifier(), 'dependency'), "$SubTestName - prove SUT can be mocked directly")
}

package FakeModuleStaticInjection;
sub dependency {
    my ($self, $arg) = @_;
    return "$arg dependency";
}
sub client {
    my $self = shift;
    return $self->dependency('client');
}

The test fails when FakeModuleStaticInjection->client() is invoked, with this message:

No matching found for stringstring -> $VAR1 = [
          'FakeModuleStaticInjection',
          'client'
        ];

This is not an issue when a dependent module is mocked, as the first argument is shifted off by the SUT before the dependent function is called with the remaining arguments. It may be argued that you shouldn't do this sort of thing, but it seems reasonable that you may want to test one function in a module that relies on another function in the same module. This is the scenario I am covering with the above test.

Injection functionality should be implemented by a separate module from Test::Mockify

Ideally, the injection capabilities of Mockify should not pollute the original implementation, but rather augment it. I like how Mockify was originally constructed to not interfere at all with the modules being mocked, and I think that should remain a core aspect of it.

However, I also am aware that it is not always possible to get the mock object into the system under test without modifying it for that express purpose. That is why the injection functionality was added. The purpose of this issue is to bring to light the inherent differences between the two approaches.

Let's say you have a Dog class with a breed function, which simply returns the breed specified when the Dog instance was created.

package Dog;
use strict;
use warnings;

sub new {
    my ($class, $breed) = @_;
    my $self = bless { breed => $breed }, $class;
    return $self;
}

sub breed {
    my $self = shift;
    return $self->{'breed'};
}

1;

Now let's mock this Dog using both approaches. The original usage of Mockify gives us something like the following. It creates an instance of the Dog class and then overrides the breed method on that instance only! This approach is useful when you have the ability to pass in an object instance to the system under test. In this case, the test object needs to be created ahead of time, and so the fact that a call to Test::Mockify->new creates the object instance on your behalf and passes along the necessary parameters to the constructor makes complete sense.

sub original_dog {
    my $dog = Dog->new('Dalmatian');
    is($dog->breed(), 'Dalmatian', "Original is a Dalmatian");

    my $mockify = Test::Mockify->new('Dog', ['Dalmatian']);
    $mockify->mock('breed')->whenAny()->thenReturn('Great Dane');
    my $mockObject = $mockify->getMockObject();

    is($mockObject->breed(), 'Great Dane', 'Mock is a Great Dane');
    is($dog->breed(), 'Dalmatian', "Original is a Dalmatian");

    ok(WasCalled($mockObject, 'breed'), 'Mocked breed method was called');
}

The static approach is somewhat simplified by comparison, since the mocked functionality is replacing the original implementation at the class level, and therefore applies to all object instances or static functions. The approach is useful for a system under test that creates a new instance of a class or calls a static function that you want to mock. In this case, the system under test handles setting up the instance, so Mockify should not be creating its own. The mocked class can be called directly in the test and provide the mocked functionality. The injector instance could provide the verification capabilities, as there is no mocked object.

sub static_dog {
    my $dog = Dog->new('Dalmatian');
    is($dog->breed(), 'Dalmatian', "Original is a Dalmatian");

    my $injector = Test::Mockify->new('Dog'); # should not call Dog->new() or need constructor parameters
    $injector->mockStatic('breed')->whenAny()->thenReturn('Great Dane');

    is(Dog->breed(), 'Great Dane', 'Mock is a Great Dane');
    is($dog->breed(), 'Great Dane', "Original is now a Great Dane");

    ok(WasCalled($injector, 'breed'), 'Mocked breed method was called');
}

To summarize:

  • The injector should not create a mock object of the target.
  • The injector's new method does not need parameters besides the target class.
  • The injector should never call new on the target. It is replacing class functionality that applies to all object instances and static calls.
  • The verification object will be the injector itself.

Because of these differences between the two approaches, having the injector be a separate module that inherits from Test::Mockify would allow it to diverge enough to achieve these goals.

The static tests should not use the mock object

This is the first static test:

sub test_mockStatic {
    my $self = shift;
    my $SubTestName = (caller(0))[3];
    my $SUT;
    {
        my $Mockify = Test::Mockify->new('FakeModulStaticInjection',[]);
        $Mockify->mockStatic('FakeStaticTools::ReturnHelloWorld')->when(String('German'))->thenReturn('Hallo Welt');
        $Mockify->mockStatic('FakeStaticTools::ReturnHelloWorld')->when(String('Spanish'))->thenReturn('Hola Mundo');
        $Mockify->mockStatic('FakeStaticTools::ReturnHelloWorld')->when(String('Esperanto'))->thenReturn('Saluton mondon');
        $SUT = $Mockify->getMockObject();

        is($SUT->useStaticFunction('German'), 'German: Hallo Welt',"$SubTestName - prove full path call- german");
        is($SUT->useImportedStaticFunction('German'), 'German: Hallo Welt',"$SubTestName - prove that the same mock can handle also the imported call - german");
        is($SUT->useStaticFunction('Spanish'), 'Spanish: Hola Mundo',"$SubTestName - prove full path call - spanish");
        is($SUT->useImportedStaticFunction('Spanish'), 'Spanish: Hola Mundo',"$SubTestName - prove imported call - spanish");
    }
    # With this approach it also is not binded anymore to the scope. The Override stays with the mocked object
    is($SUT->useStaticFunction('Esperanto'), 'Esperanto: Saluton mondon',"$SubTestName - prove full path call, scope independent- german");
    is($SUT->useImportedStaticFunction('Esperanto'), 'Esperanto: Saluton mondon',"$SubTestName - prove that the same mock can handle also the imported call, scope independent - german");

}

The system under test is not being called directly. It is being instantiated and retrieved through Mockify, which erodes confidence in the integrity of the test.

It should look something like this instead:

sub test_mockStatic {
    my $self = shift;
    my $SubTestName = (caller(0))[3];
    {
        my $Mockify = Test::Mockify->new('FakeStaticTools',[]);
        $Mockify->mockStatic('ReturnHelloWorld')->when(String('German'))->thenReturn('Hallo Welt');
        $Mockify->mockStatic('ReturnHelloWorld')->when(String('Spanish'))->thenReturn('Hola Mundo');
        $Mockify->mockStatic('ReturnHelloWorld')->when(String('Esperanto'))->thenReturn('Saluton mondon');

        is(FakeModulStaticInjection->useStaticFunction('German'), 'German: Hallo Welt',"$SubTestName - prove full path call- german");
        is(FakeModulStaticInjection->useImportedStaticFunction('German'), 'German: Hallo Welt',"$SubTestName - prove that the same mock can handle also the imported call - german");
        is(FakeModulStaticInjection->useStaticFunction('Spanish'), 'Spanish: Hola Mundo',"$SubTestName - prove full path call - spanish");
        is(FakeModulStaticInjection->useImportedStaticFunction('Spanish'), 'Spanish: Hola Mundo',"$SubTestName - prove imported call - spanish");
    }
    # With this approach it also is not binded anymore to the scope. The Override stays with the mocked object
    is(FakeModulStaticInjection->useStaticFunction('Esperanto'), 'Esperanto: Saluton mondon',"$SubTestName - prove full path call, scope independent- german");
    is(FakeModulStaticInjection->useImportedStaticFunction('Esperanto'), 'Esperanto: Saluton mondon',"$SubTestName - prove that the same mock can handle also the imported call, scope independent - german");

}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.