Asked  7 Months ago    Answers:  5   Viewed   62 times

I have a PHPUnit test case, in which I am puzzled by the following snippet. I want to check that the method actionUpload calls the function exposeAndSaveDataLines correctly, i.e. that the first argument is an array as I expect it to be.

public function test_actionUpload()
{
    $sut = $this->getMockBuilder('MasterdataController')
                ->setMethods(array('exposeAndSaveDataLines', 'render'))
                ->disableOriginalConstructor()
                ->getMock();

    $expectedLines = require_once ($this->dataDir . 'expectedLines.php');

    $sut->expects($this->once())
        ->method('exposeAndSaveDataLines')
        ->with($this->equalTo($expectedLines),
            $this->anything(),
            $this->anything(),
            $this->anything(),
            $this->anything());

    $sut->actionUpload();
}

The expected data is a printout of the current array, made with a temporary print_r (var_export($lines)) in the actual code. I return it in the file expectedLines.php, and when I manually print it, it is correct.

Now, when I run the test case with a single character deliberately misspelled in expectedLines, I get the following error (as expected).

Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
             3 => 'Colour Group Code'
-            4 => '{2F30E832-D3DB-447E-B733-7BC5125CBCCc}'
+            4 => '{2F30E832-D3DB-447E-B733-7BC5125CBCCC}'
         )
     )
 )

However, when I correct the mistake, it still mentions that the two arrays are not equal. However, it now prints the entire array (at least the start of it, it is a long array), but it doesn't show any differences (no - and + in front of any line). Why does the expects method not recognize that the two arrays are the same? How am I able to test this properly?

EDIT 1

I have shortened the array, such that it prints the entire array when they are not equal. Still no + or - signs in the comparison.

This is the end of my expectation PHP file.

    'RetTarget Area' => array(
        0 => array(
            0 => '',
            1 => '',
            2 => '{C19D52BC-834C-45DA-B17F-74D73A2EC0BB}
'
        ),
        1 => array(
            0 => '1',
            1 => '1',
            2 => '{5E25C44F-C18A-4F54-B6B1-248955A82E59}'
        )
    )
);

This is the end of my comparison output in the console.

     'RetTarget Area' => Array (
         0 => Array (
             0 => ''
             1 => ''
             2 => '{C19D52BC-834C-45DA-B17F-74D73A2EC0BB}
             '
         )
         1 => Array (...)
     )
 )

I find it suspicious that the last Array is not fully shown in the comparison.

EDIT 2

I find here that the order of the arrays is important. I am pretty sure though I have all elements in the same order, if PHP is not doing something secret under the hood. The solution mentioned there I cannot copy, since I don't have a $this->assertEquals but a ->with($this->equalTo syntax.

EDIT 3

I read here about an undocumented parameter $canonicalize that orders arrays before comparing. When I use it like this:

$sut->expects($this->once())
    ->method('exposeAndSaveDataLines')
    ->with($this->equalTo($expectedLines, $delta = 0.0, $maxDepth = 10, $canonicalize = true, $ignoreCase = false),
        $this->anything(),
        $this->anything(),
        $this->anything(),
        $this->anything());

I see that the order of the arrays is indeed changed, but I still see the same error. Also, still one array is 'collapsed', which I suspect causes this failure. Besides, I don't want to order all my subarrays, they should be in the same order in the real and expected result.

--- Expected
+++ Actual
@@ @@
 Array (
     0 => Array (
         0 => Array (
             0 => ''
             1 => ''
             2 => '{C19D52BC-834C-45DA-B17F-74D73A2EC0BB}
             '
         )
         1 => Array (...)
     )

EDIT 4

When I use identicalTo instead of equalTo, I get a more elaborate error message, saying that the one array is not identical to the other array, while printing both of them. I copy-pasted them both into a text file, and used the command diff to check for any differences, but there were none. Still, PHPUnit claims that the two arrays are not equal/identical.

EDIT 5

When I use greaterThanOrEqual or even greaterThan instead of equalTo, then the test passes. This does not happen for lessThanOrEqual. This implies that there is a difference between the two arrays.

If I manually change the expected outcome into something with a string that is alphabetically before the correct string, I can lessThan pass as well, but then of course greaterThanOrEqual fails.

EDIT 6

I am getting convinced that the line ending of the strings in my array are making this comparison to fail, which doesn't show up in all comparisons.

I now have the following assertion.

public function test_actionUpload_v10MasterdataFile()
{
    ....
    $sut->expects($this->once())
        ->method('exposeAndSaveDataLines')
        ->will($this->returnCallback(function($lines) {
            $expectedLines = include ($this->dataDir . 'ExpectedLines.php');
            $arrays_similar = $this->similar_arrays($lines, $expectedLines);
            PHPUnit_Framework_Assert::assertTrue($arrays_similar);
        }));
    $sut->actionUpload();
}

private function similar_arrays($a, $b)
{
    if(is_array($a) && is_array($b))
    {
        if(count(array_diff(array_keys($a), array_keys($b))) > 0)
        {
            print_r(array_diff(array_keys($a), array_keys($b)));
            return false;
        }

        foreach($a as $k => $v)
        {
            if(!$this->similar_arrays($v, $b[$k]))
            {
                return false;
            }
        }

        return true;
    }
    else
    {
        if ($a !== $b)
        {
            print_r(PHP_EOL . 'A: '. $a. PHP_EOL . 'Type: ' . gettype($a) . PHP_EOL);
            print_r(PHP_EOL . 'B: '. $b. PHP_EOL . 'Type: ' . gettype($b) . PHP_EOL);
        }
        return $a === $b;
    }
}

With the following result.

A: {72C2F175-9F50-4C9C-AF82-9E3FB875EA82}

Type: string

B: {72C2F175-9F50-4C9C-AF82-9E3FB875EA82}

Type: string

 Answers

45

I finally got it to work, although it is a bit of a compromise. I am now removing newlines before I compare the arrays. This cannot be done in the with method, so I have made the following construction.

public function test_actionUpload_v10MasterdataFile()
{
    /*
     * Create a stub to disable the original constructor.
     * Exposing data and rendering are stubbed.
     * All other methods behave exactly the same as in the real Controller.
     */
    $sut = $this->getMockBuilder('MasterdataController')
                ->setMethods(array('exposeAndSaveDataLines', 'render'))
                ->disableOriginalConstructor()
                ->getMock();

    $sut->expects($this->once())
        ->method('exposeAndSaveDataLines')
        ->will($this->returnCallback(function($lines) {
            $expectedLines = include ($this->dataDir . 'ExpectedLines.php');
            PHPUnit_Framework_Assert::assertTrue($this->similar_arrays($lines, $expectedLines));
        }));

    // Execute the test
    $sut->actionUpload();
}

...

private function similar_arrays($a, $b)
{
    /**
     * Check if two arrays have equal keys and values associated with it, without
     * looking at order of elements, and discarding newlines.
     */
    if(is_array($a) && is_array($b))
    {
        if(count(array_diff(array_keys($a), array_keys($b))) > 0)
        {
            return false;
        }

        foreach($a as $k => $v)
        {
            if(!$this->similar_arrays($v, $b[$k]))
            {
                return false;
            }
        }
        return true;
    }
    else
    {
        $a = rtrim($a);
        $b = rtrim($b);
        $extended_output = false;
        if ($extended_output && ($a !== $b))
        {
            print_r(PHP_EOL . 'A: '. $a. PHP_EOL . 'Type: ' . gettype($a) . PHP_EOL);
            print_r(PHP_EOL . 'B: '. $b. PHP_EOL . 'Type: ' . gettype($b) . PHP_EOL);
        }
        return $a === $b;
    }
}
Saturday, May 29, 2021
 
muaddhib
answered 7 Months ago
72

Best answer here so far is I was doing it wrong. Service Providers run way before a controller is even loaded, and, when unit testing, Laravel's IlluminateFoundationTestingTestCase loads the application (calling all service providers' both boot and register methods) during execution of the setUp method, way before any calls can be made out during the execution of any individual test.

I tried finding a solution by moving the logic down and got something to work, something along the lines of:

class MyTestClass extends TestCase
{

    public function setUp()
    {
        // No call to parent::setUp()

        // From: IlluminateFoundationTestingTestCase
        $this->app = $this->createApplication();
        $this->client = $this->createClient();
        // Not this one!
        //$this->app->setRequestForConsoleEnvironment();
        $this->app->boot();

        // ...
    }

    public function testWhatever()
    {
        // Calls to this will now actually have a request object
        $this->call('GET', '/api/v1/courses');
    }
}

But that just can't be right, at least it doesn't feel so.

Instead, I figure it's probably best not to rely on anything in the Request object from within Service Providers. Instead, why not just inject an object that can do what I need it to do in the controller I want it to? No Service Provider necessary, and I can easily mock any object other than Request in Unit Tests. I should've believed the docs.

Update

Taking this answer to my own question a bit further, I believe my original mistake was that I was utilizing the Request object within a Service Provider. I think, on retrospection, that you should probably never use the Request object at all within a service provider, because providers get loaded for everything related to laravel (including artisan commands, which of course should have no request). My original code worked in the browser, but I probably would have noticed issues had I tried to run any artisan commands.

Saturday, May 29, 2021
 
Saurabh
answered 7 Months ago
69

The PHPUnit Mocking library (by default) determines whether an expectation matches based solely on the matcher passed to expects parameter and the constraint passed to method. Because of this, two expect calls that only differ in the arguments passed to with will fail because both will match but only one will verify as having the expected behavior. See the reproduction case after the actual working example.


For you problem you need to use ->at() or ->will($this->returnCallback( as outlined in another question on the subject.

Example:

<?php

class DB {
    public function Query($sSql) {
        return "";
    }
}

class fooTest extends PHPUnit_Framework_TestCase {


    public function testMock() {

        $mock = $this->getMock('DB', array('Query'));

        $mock
            ->expects($this->exactly(2))
            ->method('Query')
            ->with($this->logicalOr(
                 $this->equalTo('select * from roles'),
                 $this->equalTo('select * from users')
             ))
            ->will($this->returnCallback(array($this, 'myCallback')));

        var_dump($mock->Query("select * from users"));
        var_dump($mock->Query("select * from roles"));
    }

    public function myCallback($foo) {
        return "Called back: $foo";
    }
}

Reproduces:

phpunit foo.php
PHPUnit 3.5.13 by Sebastian Bergmann.

string(32) "Called back: select * from users"
string(32) "Called back: select * from roles"
.

Time: 0 seconds, Memory: 4.25Mb

OK (1 test, 1 assertion)


Reproduce why two ->with() calls dont' work:

<?php

class DB {
    public function Query($sSql) {
        return "";
    }
}

class fooTest extends PHPUnit_Framework_TestCase {


    public function testMock() {

        $mock = $this->getMock('DB', array('Query'));
        $mock
            ->expects($this->once())
            ->method('Query')
            ->with($this->equalTo('select * from users'))
            ->will($this->returnValue(array('fred', 'wilma', 'barney')));

        $mock
            ->expects($this->once())
            ->method('Query')
            ->with($this->equalTo('select * from roles'))
            ->will($this->returnValue(array('admin', 'user')));

        var_dump($mock->Query("select * from users"));
        var_dump($mock->Query("select * from roles"));
    }

}

Results in

 phpunit foo.php
PHPUnit 3.5.13 by Sebastian Bergmann.

F

Time: 0 seconds, Memory: 4.25Mb

There was 1 failure:

1) fooTest::testMock
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-select * from roles
+select * from users

/home/.../foo.php:27

FAILURES!
Tests: 1, Assertions: 0, Failures: 1
Tuesday, July 27, 2021
 
Saxophlutist
answered 5 Months ago
38

In my case the answer turned out to be quite simple:

$this->expects($this->at(0))
    ->method('write')
    ->with(/* first set of params */);

$this->expects($this->at(1))
    ->method('write')
    ->with(/* second set of params */);

The key is to use $this->at(n), with n being the Nth call of the method. I couldn't do anything with any of the logicalOr() variants I tried.

Sunday, August 1, 2021
 
cegfault
answered 4 Months ago
31

Very good question! It's a common problem when you're new to testing.

Firstly, make a distinction between unit and integration testing:

  • unit testing - testing a "unit", most often a class, in isolation. It's achieved by mocking or stubbing unit's dependencies most of the time. No infrastructure (network, filesystem etc) should be used at this level.
  • integration testing - testing how components interact with each other. You might hit the infrastrcuture, but you can still choose not to (to optimise).

I'd do the following:

  • Implement the API client as a library and write integration tests for it. These integration tests will actually hit the API and will prove the client interacts with the API as expected. I'd run them whenever the API client changes, or periodically to make sure I'm still compatible with the API. These tests wouldn't be run as often as the application tests, as they'd be part of a separate test suite.
  • Introduce an abstraction in the app that will let me to provide alternative implementations for whatever interacts with the API. This way I'd be able to write acceptance or other kind of integration tests with a simpler implementation (for example an in-memory one).
  • Make sure that if I'm making an assumption in the app for how the API client works, I've got an integration tests proving this assumption is correct. For example, if I call a method with a valid ID, it returns an object. Otherwise it throws an exception. I can rely on these rules only if I've got an integration test somewhere verifying them.

Mocking responses is a tricky business. If you try doing it one day you'll run into troubles when the 3rd party API changes. If you still want to go this path, have a look at https://github.com/coduo/tutu.

Wednesday, November 10, 2021
 
Anders Brohäll
answered 3 Weeks ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :
 
Share