1
votes

I am trying to mock a mysqli object for my unit testing and therefore I have to either mock the property mysqli_result::__get() or mock the property mysqli_result::num_rows directly. I already looked for a solution but the answers I found were just not working.

My Code looks like this:

use PHPUnit\Framework\TestCase;

class Mocks extends TestCase{
    public function makeMysqliMock(array $queries): mysqli{
        // build mocked mysqli-object
        #$link = $this
        #   ->getMockBuilder("mysqli")
        #   // set methods
        #   ->setMethods(['__get', "query", "real_escape_string"])
        #   ->getMock();
        $link = $this->createMock("mysqli");
        // set method 'real_escape_string'
        $link->expects($this->any())
            ->method("real_escape_string")
            ->will($this->returnCallback(function($str){
                return addslashes($str); 
            }));
        // set method 'query'
        $link->expects($this->any())
            ->method("query")
            ->will($this->returnCallback(function(string $query) use ($queries){
                // pick the query result for the current test
                $data = isset($queries[$query]) ? $queries[$query] : null;
                // build mocked 'mysqli_result'
                if (is_array($data)){
                    $result = $this
                        ->getMockBuilder("mysqli_result")
                        ->setMethods(["fetch_assoc", "__get"])
                        ->disableOriginalConstructor()
                        ->getMock();
                    // build simulated fetch method
                    $result->expects($this->any())
                        ->method("fetch_assoc")
                        ->withAnyParameters()
                        ->will($this->returnCallback(function() use ($data){
                            // set internal result pointer
                            static $index = 0;
                            // return current result - increment pointer
                            return isset($data[$index]) ? $data[$index++] : false;
                        }));
                    $result->expects($this->at(0))
                        ->method("__get")
                        ->with($this->equalTo("mysqli_num_rows"))
                        ->will($this->returnValue(count($data)));


                    return $result;
                }else {
                    return is_null($data) ? false : 1;
                }
            }));
        return $link;
    }
}

When I run that code phpunit gives me the following error message:

C:\xampp\htdocs\werimanage\php>php phpunit-6.0.11.phar tests PHPUnit 6.0.11 by Sebastian Bergmann and contributors.

PHP Fatal error: Method Mock_mysqli_result_d3aa5482::__get() must take exactly 1 argument in phar://C:/xampp/htdocs/werimanage/php/phpunit-6.0.11.phar/phpunit-mock-objects/Generator.php(263) : eval()'d code on line 53

Fatal error: Method Mock_mysqli_result_d3aa5482::__get() must take exactly 1 argument in phar://C:/xampp/htdocs/werimanage/php/phpunit-6.0.11.phar/phpunit-mock-objects/Generator.php(263) : eval()'d code on line 53

So I do not know what the error is or how to fix it. I would really appreciate your help. Thanks!

2

2 Answers

1
votes

according to the php documentation the method __get requires 1 argument (more over this is the error phpUnit is returning).

You can see on the documentation :

public mixed __get ( string $name )

The $name argument is the name of the property being interacted with.

I imagine that you will be calling the __get method foreach of your column in your sql request. You then have to mock all the calls and the returns of the __get method with each argument (each column).

Hope this will help you.

0
votes

This error happens because magic method __get must accept one parameter -- the name of the property. When PHPUnit generates the code to declare the mock it uses the declaration of the method from the original class. Since there is no method __get declared for mysqli_result PHPUnit mock generator declares it as __get() without parameters, and this is encountered as error.

If you really want to do your coding and testing the way you do it, you can use the following approach:

class SuperMock extends mysqli_result {
    public function __get($name){

    }
}

class Mocks extends PHPUnit_Framework_TestCase
{
    public function testGet()
    {
        $data = ['one' => 1, 'two' => 2];
        $mock = $this->getMockBuilder('SuperMock')
                     ->disableOriginalConstructor()
                     ->setMethods(['__get', 'fetch_assoc'])
                     ->getMock();
        $mock->expects($this->once())
             ->method('__get')
             ->with('nonexistent')
             ->willReturn(42);
        $mock->expects($this->once())
             ->method('fetch_assoc')
             ->willReturn($data);
        $this->assertInstanceOf('mysqli_result', $mock);
        $this->assertSame(42, $mock->nonexistent);
        $this->assertSame($data, $mock->fetch_assoc());
    }

    public function testDirectProperty(){
        $mock = $this->getMockBuilder('SuperMock')
                     ->disableOriginalConstructor()
                     ->setMethods(['__get', 'fetch_assoc'])
                     ->getMock();
        $mock->nonexistent = 42;
        $this->assertSame(42, $mock->nonexistent);
    }
}

This will technically solve the issue. Still I would suggest revising your testing strategy. Because now it looks like you are testing how the interaction with a database is performed. But do you really need to test if data is fetched as an associative array, and that number of rows is figured out through mysqli_num_rows property? Going this way makes tests too much coupled to the production code. I believe this is the case when it is more suffucient to test the result being retrieved rather than the internal details of this process.