28

I'm writing an unit test for my PHP project,

the unit test is to simulate a php://input data,

and I read the manual, it says:

php://input is a read-only stream that allows you to read raw data from the request body.

How do I simulate the php://input, or write the request body in my PHP?


Here's my source code and unit test, both are simplified.

Source:

class Koru
{
    static function build()
    {
        // This function will build an array from the php://input.
        parse_str(file_get_contents('php://input'), $input);

        return $input;
    }

    //...

Unit Test:

function testBuildInput()
{
    // Trying to simulate the `php://input` data here.
    // NOTICE: THIS WON'T WORK.
    file_put_contents('php://input', 'test1=foobar&test2=helloWorld');

    $data = Koru::build();

    $this->assertEquals($data, ['test1' => 'foobar',
                                'test2' => 'helloWorld']);
}
Yami Odymel
  • 1,782
  • 3
  • 22
  • 48
  • 1
    @CBroe — That would involve writing rather a lot of scaffolding for the unit test. – Quentin Jul 05 '16 at 07:14
  • 4
    I think the point here is that you cannot *unit test* something that relies on `php://input` directly. – zerkms Jul 05 '16 at 07:25
  • @CBroe I would like to edit or create the `php://input` directly in my unit test and pass it to the function which I wanted to test instead of making a real *request*, it's much easier for me to manage my unit tests. – Yami Odymel Jul 05 '16 at 07:28
  • 1
    `php://input` is global state, which is [evil](http://programmers.stackexchange.com/questions/148108/why-is-global-state-so-evil). I propose you pass the source of the file to the `build()` function to make it's dependency clear. – Pieter van den Ham Jul 05 '16 at 08:01
  • See https://stackoverflow.com/questions/9158155/how-to-write-unit-tests-for-interactive-console-app – 8ctopus Apr 27 '23 at 10:25

5 Answers5

11

Use a test double

Given the code in the question, the simplest solution is to restructure the code:

class Koru
{
    static function build()
    {
        parse_str(static::getInputStream(), $input);
        return $input;
    }

    /**
     * Note: Prior to PHP 5.6, a stream opened with php://input could
     * only be read once;
     *
     * @see http://php.net/manual/en/wrappers.php.php
     */
    protected static function getInputStream()
    {
        return file_get_contents('php://input');
    }

And use a test double:

class KoruTestDouble extends Koru
{
    protected static $inputStream;

    public static function setInputStream($input = '')
    {
        static::$inputStream = $input;
    }

    protected static function getInputStream()
    {
        return static::$inputStream;
    }
}

The test method then uses the test double, not the class itself:

function testBuildInput()
{
    KoruTestDouble::setInputStream('test1=foobar&test2=helloWorld');

    $expected = ['test1' => 'foobar', 'test2' => 'helloWorld'];
    $result = KoruTestDouble::build();

    $this->assertSame($expected, $result, 'Stuff be different');
}

Avoid static classes if possible

Most of the difficulties with the scenario in the question are caused by the use of static class methods, static classes make testing hard. If at all possible avoid the use of static classes and use instance methods which allows solving the same sort of problem using mock objects.

AD7six
  • 63,116
  • 12
  • 91
  • 123
9

See vfsStream package and this SO question and answers.

Basically, you would want to parametrize your service that reads data to accept a path:

public function __construct($path)
{
    $data = file_get_contents($path); // you might want to use another FS read function here
}

And then, in a test, provide an vfsStream stream path:

\vfsStreamWrapper::register();
\vfsStream::setup('input');

$service = new Service('vfs://input') 

In your code you would provide php://input as per usual.

Finwe
  • 6,372
  • 2
  • 29
  • 44
  • 1
    This example does not illustrate how to define the content that would be returned from `vfs://input`, and will error when `file_get_contents('vfs://input')` is called. To define content, you must define a vfsStream file. It is also unnecessary to call `vfsStreamWrapper::register()` prior to calling `vfsStream:setup()`. – Courtney Miles Feb 28 '17 at 09:01
2

This sort of extreme decomposition gains nothing and leads very brittle code. Your tests should express the expectations of your interfaces, and not the data you've supplied them with: Is PHP truly not free to return ["test2"=>"helloWorld","test1"=>"foobar"] in some future version? Is your code broken if it does? What exactly do you think you are testing?

I think you're overcomplicating this.

$a->doit should take $input as an argument and not call Koru::build as part of its initialisation. Then you can test $a->doit instead of testing parse_str.

If you insist on pressing on this example, then Koru::build needs to take an argument of 'php://input' – this is often called dependency injection, where you tell your functions everything they need to know. Then, when you want to "test" things, you can simply pass in some other file (or e.g. a data url).

geocar
  • 9,085
  • 1
  • 29
  • 37
  • I'm creating a class which helps me convert the array into an object, for example if I have `$_POST`, I can build it with `Koru` like `$data = Koru::build($_POST)`, so I can use it like `$data->username` instead of `$_POST['username']`, and I can even add some helper functions, which makes me more convenient to process with the datas. – Yami Odymel Jul 05 '16 at 08:12
  • That's what `$data = (object)$_POST` does. – geocar Jul 05 '16 at 08:13
  • The reason I *build* the data from `php://input` is because PHP doesn't have `$_DELETE[]`, `$_PUT[]`, `$_PATCH[]`, and I make the test of this is to make sure if `build()` is working for parse `php://input`. – Yami Odymel Jul 05 '16 at 08:14
  • I couldn't add any helper functions like `$data->remove('username, password')` (which will remove the `username` and `password` in the $data) if I used `$data = (object)$_POST`. – Yami Odymel Jul 05 '16 at 08:16
  • 1
    You're looking for `unset($data->username);` – geocar Jul 05 '16 at 08:17
  • You are making things more complicated than they need to be. `parse_str()` doesn't need tests in your code unless you're changing `parse_str()`. Focus on the code you actually need to write, not on things that you think you need to write (because you might not!) – geocar Jul 05 '16 at 08:17
  • 2
    The world is not made a better place by more code (which is just more code to write, more code to debug, and more code for your future selves to read) but by doing more with less. Stop writing "helper functions" until you're both sure nobody else has written it for you (stack overflow can help), and you need it more than once. – geocar Jul 05 '16 at 08:20
  • No offense but I'm not sure about `parse_str() doesn't need tests in your code unless you're changing parse_str().`, isn't it part of the `build()`, if it is, then I have to test my `build()` function with my unit test isn't it? – Yami Odymel Jul 05 '16 at 08:24
  • 1
    `Is PHP truly not free to return ["test2"=>"helloWorld","test1"=>"foobar"] in some future version?` The premise of this answer seems pretty flawed, that is never going to happen, but if it did and the code was as you suggest tests would pass but the code would still fail. – AD7six Jul 05 '16 at 08:38
  • 3
    No. You do not write tests to exercise lines of code, but to verify interfaces so that when you *change the code* you can preserve the interface. – geocar Jul 05 '16 at 08:48
  • We probably have the same opinion, However: You are defining the method interface to expect a string - so that's what you'd be testing. You are then saying to anticipate the output of `file_get_contents` may in some future version be an array (which is unrealistic) - so that's what'd get passed to the method at run time. To me, in context, that's putting an implementation detail outside the SUT. In short I feel you've picked the wrong battle for "you should use dependency injection" _especially_ given the limitation of only being able to read php input once in < 5.6. . – AD7six Jul 05 '16 at 09:04
1

With Kahlan you can monkey patch the file_get_contents function directly like so:

use My\Name\Space\Koru;

describe("::build()", function() {

    it("parses data", function() {

        allow('file_put_contents')->toBeCalled()->andRun(function() {
            return 'test1=foobar&test2=helloWorld';
        });
        expect(Koru::build())->toBe([
            'test1' => 'foobar',
            'test2' => 'helloWorld'
        ]);

    });

});
Jails
  • 370
  • 1
  • 4
  • 9
1

Use a Zend\Diactoros\Stream

https://zendframework.github.io/zend-diactoros/usage/

$_POST['foo'] = 'bar';
use Zend\Diactoros\ServerRequestFactory;
$psrRequest = ServerRequestFactory::fromGlobals();
var_dump($psrRequest->getParsedBody()); // foo => bar
var_dump($_POST); // foo => bar

more info https://laracasts.com/discuss/channels/general-discussion/psr-7?page=1

fearis
  • 424
  • 3
  • 15