Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mock atLeastOnce with concrete value, the rest not important

The question is in PHP, but applies to any language using the xUnit framework.

I want a mock, that expects 140 calls to method jump.
I need to verify, that at least once there is a call with 500 as parameter.
I don't care if all the calls are 500, but I need at least one that is called with 500.

$mock = $this->getMock('Trampoline', ['jump']);

$mock->expects($this->atLeastOnce())
     ->method('jump')
     ->with($this->equalTo(500))
     ->will($this->returnValue(true));

$sportsman->setTramploine($mock);
$sportsman->jumpToRandomHeights($times = 140); // this calls Trampoline->jump
// I need to verify the sportsman had jumped 
// to the height of 500 at least once out of the 140 jumps he is performing

In the current code, the test fails after the first invocation of jump because the first invocation had a value different of 500, meaning the atLestOnce here only indicates that the method should be called, but not that it should be called with specific value among other calls.


Solution

The missing piece of information was using callbacks inside the with. Thanks to edorian's answer below this is what worked out:

$testPassed = false;

$checkMinHeight = function ($arg) use(&$testPassed)
{
    if($arg === 500)
        $testPassed = true;

    // return true for the mock object to consider the input valid
    return true;
}


$mock = $this->getMock('Trampoline', ['jump'])
    ->expects($this->atLeastOnce())
    ->method('jump')
    ->with($checkMinHeight)
    ->will($this->returnValue(true));

$sportsman->setTramploine($mock);
$sportsman->jumpToRandomHeights($times = 1000); // this calls Trampoline->jump
// I need to verify the sportsman had jumped 
// to the height of 500 at least once out of the 1000 jumps he is performing


$this->assertTrue($testPassed, "Sportsman was expected to 
    jump 500m at least once");
like image 356
Alex Avatar asked Oct 06 '22 21:10

Alex


1 Answers

You can but the best implementation withing PHPUnits mocking API, that I could come up with, still looks quite creepy.

Another way to solve this is a little more readable way would be to create your own subclass of Trampoline and implement it there.

But for the challenge:

Assuming this class:

<?php
class FancyMocking {
    function doThing($value) { }
}

and that we have $x calls and one of those has to have a $value > 200:


<?php

class FancyMockingTest extends PHPUnit_Framework_TestCase {

    public function testAtLeastOfMy200CallsShouldHaveAValueGreaterThan500() {
      $maxInvocations = 200;

      $mock = $this->getMock('FancyMocking');
      $mock->expects($this->exactly($maxInvocations))
        ->method('doThing')
        ->with($this->callback(function ($value) use ($maxInvocations) { 
            static $invocationCount = 0;
            static $maxValue = 0;

            $maxValue = max($value, $maxValue);
            /* The assertion function will be called twice by PHPUnit due to implementation details, so the *2 is a hack for now */
            if (++$invocationCount == $maxInvocations * 2) { 
                $this->assertGreaterThan(200, $maxValue, 'in 500 tries the max value didn\'t to over 200');
            } 
            return true;
        }))
        ->will($this->returnCallback(function ($value) { 
            return $value >= 200;
        }));
     for($i = $maxInvocations - 2; $i; --$i) { 
          $mock->doThing(50);
     } 
     var_dump($mock->doThing(250));
     var_dump($mock->doThing(50));
    }


}

This will produce:

PHPUnit 3.7.9 by Sebastian Bergmann.

.bool(true)
bool(false)


Time: 0 seconds, Memory: 2.75Mb

OK (1 test, 2 assertions)

Meaning the call with 250 returns true an the whole test case works.

If it fails:

To make it fail we change var_dump($mock->doThing(250)); to var_dump($mock->doThing(70)); and run it again:

PHPUnit 3.7.9 by Sebastian Bergmann.

Fbool(false)


Time: 0 seconds, Memory: 2.75Mb

There was 1 failure:

1) FancyMockingTest::testAtLeastOfMy200CallsShouldHaveAValueGreaterThan500
Expectation failed for method name is equal to <string:doThing> when invoked 200 time(s)
in 500 tries the max value didn't to over 200
Failed asserting that 70 is greater than 200.

.../FancyMockingTest.php:29

FAILURES!
Tests: 1, Assertions: 1, Failures: 1.

The solution that doesn't use PHPUnits mocking API would be something like

class FancyMockingFakeImplementation extends FancyMocking, using that instead of the mock and writing the custom logic there.

like image 59
edorian Avatar answered Oct 11 '22 01:10

edorian