The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/bin/env perl
#
# mocks.t
#
# Object mocking and stubs.
#
########################################################################
#

package Testcase::Spec::Mocks;
use Test::Spec;
use base qw(Test::Spec);

use List::Util ();

# Just a dummy class hierarchy for our testing
{
  package TestOO;
  sub new {
    bless {}, shift;
  }
  sub desc {
    my $self = shift;
    "bottom";
  }
}
{
  package TestORM;
  our @ISA = qw(TestOO);
  sub create { 'ORIGINAL' }
  sub retrieve { 'ORIGINAL' }
  sub desc {
    shift->SUPER::desc . " middle";
  }
}
{
  package TestProduct;
  our @ISA = qw(TestORM);
  use overload eq => sub { 1 }; # stub for with() test
  sub prices { 'ORIGINAL' }
  sub desc {
    # normally "bottom middle top"
    shift->SUPER::desc . " top";
  }
}

sub contains_ok {
  my ($array,$matcher) = @_;
  local $Test::Builder::Level = $Test::Builder::Level + 1;
  my $ok;
  if (ref $matcher eq 'Regexp') {
    ok( $ok = List::Util::first { $_ =~ $matcher } @$array );
  }
  else {
    ok( $ok = List::Util::first { $_ eq $matcher } );
  }
  if (not $ok) {
    # ganked from Test::Builder::_regex_ok
    my $candidates = join("\n" . (" " x 18), map { "'$_'" } @$array);
    my $match = "don't match";
    Test::More->builder->diag(sprintf <<'DIAGNOSTIC', $candidates, "don't match", $matcher);
                  %s
    %13s '%s'
DIAGNOSTIC
  }
}

describe 'Test::Mocks' => sub {
  # <enter context "The mocking system">

  describe "->stubs()" => sub {

    # replace TestProduct->create for this scope (which is actually TestORM->create)
    my $class;
    my $test_product = TestProduct->new;
    TestProduct->stubs('create' => sub {
      $class = shift;
      return $test_product;
    });

#    after each => sub {
#      # <enter context anonymous>
#      # TODO: WHAT SHOULD HAPPEN HERE?
#      # <leave context>
#    };
#
#    after all => sub {
#      # <enter context anonymous>
#      # TODO: WHAT SHOULD HAPPEN HERE?
#      # <leave context>
#    };

    it 'stubs a class method' => sub {
      my $product = TestProduct->create(price => 1000);
      is($product, $test_product);
    };

    it 'calls the stubbed method with the correct class invocant' => sub {
      TestProduct->create(price => 1000);
      # stub should have set $class
      is($class, 'TestProduct');
    };

    describe "with a before:all block" => sub {
      my $i = 0;
      before all => sub {
        $i++;
        TestProduct->stubs('retrieve' => sub { $i });
      };
      it 'stubs methods in before:all blocks' => sub {
        is(TestProduct->retrieve, 1);
      };
      it 'stubs only once' => sub {
        is(TestProduct->retrieve, 1);
      };
    };
    
    describe "outside and after the before:all block" => sub {
      it "restored the original method" => sub {
        is(TestProduct->retrieve, 'ORIGINAL');
      };
    };

    describe "with a before:each block" => sub {
      my $i = 0;
      my $tests_run;  # in case only specific tests are run
      before each => sub {
        $i++;
        TestProduct->stubs('retrieve' => sub { $i });
      };

      it "stubs once per test" => sub {
        is(TestProduct->retrieve, ++$tests_run);
      };

      it "continues to stub once per test" => sub {
        is(TestProduct->retrieve, ++$tests_run);
      };

    };
    
    describe "outside and after the before:each block" => sub {
      it "restored the original method" => sub {
        is(TestProduct->retrieve, 'ORIGINAL');
      };
    };

    it 'stubs an instance method on all instances of a class' => sub {
      TestProduct->stubs('name')->returns('stubbed_name');
      my $product = TestProduct->new;
      is($product->name, 'stubbed_name');
      # TestProduct->name is un-stubbed automatically
    };

    it 'calls stubbed instance methods with the correct instance invocant' => sub {
      my $invocant;
      TestProduct->stubs(name => sub { $invocant = shift });
      my $product = TestProduct->new;
      $product->name;
      is($invocant, $product);
    };

    it 'stubs instance methods' => sub {
      my @prices = (1000, 2000);
      my $product = TestProduct->new;
      $product->stubs('prices')->returns(\@prices);
      is_deeply( $product->prices, \@prices );
    };

    my $shared_product;
    it 'stubs only the instances requested' => sub {
      my $before_unstubbed = TestProduct->new;

      my @prices = (1000, 2000);
      $shared_product = TestProduct->new;
      $shared_product->stubs('prices')->returns(\@prices);

      my $after_unstubbed = TestProduct->new;
      is_deeply( [$before_unstubbed->prices, $after_unstubbed->prices],
                ['ORIGINAL','ORIGINAL'] );
    };

    it 'restores stubbed instance methods' => sub {
      is_deeply($shared_product->prices, 'ORIGINAL');
    };

    # "necessarily," because you have to specify the package in your code
    it 'does not necessarily break SUPER::' => sub {
      TestORM->stubs('desc' => sub {
        package TestORM;
        shift->SUPER::desc . " STUBBED";
      });
      is(TestProduct->new->desc, 'bottom STUBBED top');
    };

    it 'does not break inheritance chains after restoring a method' => sub {
      # usefulness depends on previous test having been run first
      is(TestProduct->new->desc, 'bottom middle top');
    };
  };

  describe "::stub()" => sub {

    it 'creates anonymous stubs' => sub {
      my $stub = stub(stubbed_method => 'result');
      is( $stub->stubbed_method, 'result' );
    };

  };

  describe "->expects()" => sub {
    it 'mocks a class method' => sub {
      TestProduct->expects('retrieve')->returns(42);
      is(TestProduct->retrieve(1), 42);
    };

    it 'mocks an instance method' => sub {
      my $product = TestProduct->new;
      $product->expects('save')->returns(42);
      is($product->save, 42);
    };

    it 'expects exactly one call by default' => sub {
      # looking for something like "expected retrieve to be called
      # exactly once, but it was called 0 times"
      my $expectation = TestProduct->expects('retrieve')->returns(42);
      $expectation->cancel;
      contains_ok([$expectation->problems],
                  qr/expected.*exactly once.*0 times/);
    };

    it 'dies if there are any problems' => sub {
      my $expectation = TestProduct->expects('retrieve')->returns(42);
      $expectation->cancel;
      eval { $expectation->verify };
      like($@, qr/expected.*exactly once.*0 times/);
    };

    it 'runs verify after a test block' => sub {
      my $verified = 0;
      my $block_ended = 0;
      Test::Spec::Mocks::Expectation->stubs(verify => sub {
        # ensure it actually happens 
        die "verify called before block ended" unless $block_ended;
        $verified++;
      });
      # yuck, private method. maybe change later.
      Test::Spec->current_context->_in_anonymous_context(sub {
        TestProduct->expects('retrieve')->returns(42);
        $block_ended++;
      });
      ok($verified);
    };


    describe "raising exceptions" => sub {
      it "raises the exception" => sub {
        my $stub = stub();
        my $expectation = $stub->expects('run');
        $expectation->cancel; # don't verify
        $expectation->raises("Foo\n");
        eval {
          $stub->run;
        };
        if ($@ eq "Foo\n") {
          pass("As expected");
        }
        else {
          fail("Told the mock to raise an exception, but it didn't happen");
        }
      };
    };

    describe "argument matching" => sub {
      my ($stub, $expectation);
      my ($with_method, $num_args_mismatch_err, $args_mismatch_err);

      before each => sub {
        $stub = stub();
        $expectation = $stub->expects('run');
        $expectation->cancel; # don't verify
      };

      shared_examples_for "number of arguments" => sub {
        it "passes when expecting no arguments" => sub {
          $expectation->$with_method();
          $stub->run();
          is(scalar($expectation->problems), 0);
        };

        it "passes when expecting no arguments and never called" => sub {
          $expectation->any_number->$with_method();
          # $stub->run();  # nope!
          is(scalar($expectation->problems), 0);
        };

        it "passes when expecting one argument and never called" => sub {
          $expectation->any_number->$with_method("Foo");
          # $stub->run();  # nope!
          is(scalar($expectation->problems), 0);
        };

        it "fails when expecting no arguments and one argument given" => sub {
          $expectation->$with_method();
          $stub->run(1);
          contains_ok([$expectation->problems], $num_args_mismatch_err);
        };

        it "fails when expecting one argument but given none" => sub {
          $expectation->$with_method("Foo");
          $stub->run();
          contains_ok([$expectation->problems], $num_args_mismatch_err);
        };

        it "fails when expecting one argument but given two" => sub {
          $expectation->$with_method("Foo");
          $stub->run("Foo", "Bar");
          contains_ok([$expectation->problems], $num_args_mismatch_err);
        };

      };

      shared_examples_for "shallow string comparisons" => sub {
        it "passes when expecting one String('Foo') argument" => sub {
          $expectation->$with_method("Foo");
          $stub->run("Foo");
          is(scalar($expectation->problems), 0);
        };

        it "fails when expecting one String('Foo') argument but given a different String" => sub {
          $expectation->$with_method("Foo");
          $stub->run("Bar");
          contains_ok([$expectation->problems], $args_mismatch_err);
        };

        it "fails when expecting many string arguments but given different arguments" => sub {
          $expectation->$with_method('Foo', 'Bar', 'Baz');
          $stub->run('Foo', 'Bar', 'Bat');
          contains_ok([$expectation->problems], $args_mismatch_err);
        };
      };

      describe "with eq" => sub {
        before all => sub {
          $with_method = 'with';
          $num_args_mismatch_err = qr/^Number of arguments don't match expectation$/;
          $args_mismatch_err = qr/^Expected argument in position/;
        };

        it_should_behave_like "number of arguments";
        it_should_behave_like "shallow string comparisons";

        it "passes when expecting an object argument that was given" => sub {
          my $obj = TestOO->new;
          $expectation->with($obj);
          $stub->run($obj);
          is(scalar($expectation->problems), 0);
        };

        it "fails when expecting an object argument but given a different one" => sub {
          $expectation->with(TestOO->new);
          $stub->run(TestOO->new);
          contains_ok([$expectation->problems], qr/^Expected argument in position 0 to be 'TestOO=HASH.+ but it was 'TestOO=HASH/);
        };

        it "passes when expecting an object argument and given a different one that compares with eq operator" => sub {
          $expectation->with(TestProduct->new);
          $stub->run(TestProduct->new);
          is(scalar($expectation->problems), 0);
        };
      };

      describe "with Test::Deep" => sub {
        before all => sub {
          $with_method = 'with_deep';
          $num_args_mismatch_err = qr/^Compared array length/;
          $args_mismatch_err = qr/^Compared .*(?!length)/;
        };

        it_should_behave_like "number of arguments";
        it_should_behave_like "shallow string comparisons";

        it "passes when expecting an object argument that was given" => sub {
          my $obj = TestOO->new;
          $expectation->with_deep($obj);
          $stub->run($obj);
          is(scalar($expectation->problems), 0);
        };

        it "passes when expecting an empty hash and given a different one" => sub {
          $expectation->with_deep({});
          $stub->run({});
          is(scalar($expectation->problems), 0);
        };

        it "passes when given a copy of the data structure it is expecting" => sub {
          $expectation->with_deep({ key => 'value' });
          $stub->run({ key => 'value' });
          is(scalar($expectation->problems), 0);
        };

        it "passes when expecting an object and given a clone" => sub {
          $expectation->with_deep(TestOO->new);
          $stub->run(TestOO->new);
          is(scalar($expectation->problems), 0);
        };

        it "does a deep comparison of nested structures" => sub {
          $expectation->with_deep({ product => TestProduct->new });
          $stub->run({ product => TestProduct->new });
          is(scalar($expectation->problems), 0);
        };
      };
    };

    describe "call count expectation" => sub {

      my $stub = stub();
      my $expectation;
      before each => sub {
        $expectation = $stub->expects('run')->returns(42);
        $expectation->cancel; # don't verify
      };

      describe "'exactly'" => sub {
        before sub { $expectation->exactly(42) };
        it "passes when called exactly N times" => sub {
          for (1..42) { $stub->run }
          is(scalar($expectation->problems), 0);
        };
        it "fails when called less than N times" => sub {
          $stub->run;
          contains_ok([$expectation->problems], qr/expected.*42.*1 time/);
        };
        it "fails when called more than N times" => sub {
          for (1..43) { $stub->run }
          contains_ok([$expectation->problems], qr/expected.*42.*43 times/);
        };
      };

      describe "'never'" => sub {
        before sub { $expectation->never };
        it "passes when called never" => sub {
          is(scalar($expectation->problems), 0);
        };
        it "fails when called" => sub {
          $stub->run;
          ok(scalar($expectation->problems) > 0);
        };
      };

      describe "'once'" => sub {
        before sub { $expectation->once };
        it "passes when called once" => sub {
          $stub->run;
          is(scalar($expectation->problems), 0);
        };
        it "fails when not called" => sub {
          contains_ok([$expectation->problems],
                      qr/expected.*exactly once.*0 times/);
        };
        it "fails when called more than once" => sub {
          $stub->run;
          $stub->run;
          contains_ok([$expectation->problems],
                      qr/expected.*exactly once.*2 times/);
        };
      };

      describe "'at_least'" => sub {
        before sub { $expectation->at_least(3) };
        it "fails when called fewer than N times" => sub {
          $stub->run;
          contains_ok([$expectation->problems], qr/expected.*\bat least 3\b.*\b1 time/);
        };
        it "passes when called N times" => sub {
          for (1..3) { $stub->run }
          is(scalar($expectation->problems), 0);
        };
        it "passes when called more than N times" => sub {
          for (1..4) { $stub->run }
          is(scalar($expectation->problems), 0);
        };
      };

      describe "'at_least_once'" => sub {
        before sub { $expectation->at_least_once };
        it "fails when not called at least once" => sub {
          contains_ok([$expectation->problems],
                      qr/expected.*\bat least 1\b.*\b0 times/);
        };
        it "passes when called once" => sub {
          $stub->run;
          is(scalar($expectation->problems), 0);
        };
        it "passes when called more than once" => sub {
          for (1..3) { $stub->run }
          is(scalar($expectation->problems), 0);
        };
      };

      describe "'at_most'" => sub {
        before sub { $expectation->at_most(2) };
        it "passes when never called" => sub {
          # test specifically for zero, since it's an edge case
          is(scalar($expectation->problems), 0);
        };
        it "passes when called fewer than N times" => sub {
          $stub->run;
          is(scalar($expectation->problems), 0);
        };
        it "passes when called at most N times" => sub {
          for (1..2) { $stub->run }
          is(scalar($expectation->problems), 0);
        };
        it "fails when not called at most N times" => sub {
          for (1..3) { $stub->run }
          contains_ok([$expectation->problems], qr/expected.*\bat most 2\b.*\b3 times/);
        };
      };

      describe "'at_most_once'" => sub {
        before sub { $expectation->at_most_once };
        it "passes when never called" => sub {
          # test specifically for zero, since it's an edge case
          is(scalar($expectation->problems), 0);
        };
        it "passes when called exactly once" => sub {
          $stub->run;
          is(scalar($expectation->problems), 0);
        };
        it "fails when called more than once" => sub {
          for (1..2) { $stub->run }
          contains_ok([$expectation->problems], qr/expected.*\bat most 1\b.*\b2 times/);
        };
      };

      describe "'maybe'" => sub {
        # TODO: add ability to share tests between contexts. these are the
        # same tests for at_most_once, since 'maybe' is an alias for that
        before sub { $expectation->maybe };
        it "passes when never called" => sub {
          # test specifically for zero, since it's an edge case
          is(scalar($expectation->problems), 0);
        };
        it "passes when called exactly once" => sub {
          $stub->run;
          is(scalar($expectation->problems), 0);
        };
        it "fails when called more than once" => sub {
          for (1..2) { $stub->run }
          contains_ok([$expectation->problems], qr/expected.*\bat most 1\b.*\b2 times/);
        };
      };

      describe "'any_number'" => sub {
        before sub { $expectation->any_number };
        it "passes when not called" => sub {
          is(scalar($expectation->problems), 0);
        };
        it "passes when called once" => sub {
          $stub->run;
          is(scalar($expectation->problems), 0);
        };
        it "passes when called more than once" => sub {
          for (1..2) { $stub->run }
          is(scalar($expectation->problems), 0);
        };
      };

    };

  };

  describe "::mock()" => sub {
    it 'allows anonymous mocking' => sub {
      my $mock = mock();
      $mock->expects('expected_method')->returns("result");
      #->with("p1","p2")->returns("result");
      is($mock->expected_method, "result");
    };
  };

  # <leave context "The mocking system">
};

runtests unless caller;