Jump to content

print the output inside a function/method...


Go to solution Solved by Danishhafeez,

Recommended Posts

Posted (edited)

Many say that is not recommended to print (text, data, info...) in a function/method, because it makes it difficult to test (unless inelegant stratagems are used).

Here's a demonstration

$ cat FileTest.php 
<?php
	function copyFileExample($echoOutput) {
    $verboseText = "Copying file... please wait...\n";
    
    if ($echoOutput) echo $verboseText;
	    /*
     * since it is an example, instead of copy(), 
     * the sleep(5) function is used, to simulate copying a file, 
     * which let's pretend it takes 5 seconds
     */
    sleep(5); // replaces copy() function
    
    if (!$echoOutput) return $verboseText;
}
	class FileTest extends PHPUnit\Framework\TestCase {
	    static function copyProvider() {
        return [
            [
                "echoOutput" => true // not recommended [HIGHLIGHT]
            ],
            [
                "echoOutput" => false // recommended [HIGHLIGHT]
            ],
        ];
    }
	    /**
     * @dataProvider copyProvider
     */
    function testCopy($echoOutput) {
        $this->assertEquals(
            "Copying file... please wait...\n",
            copyFileExample($echoOutput)
        );
    }
}

I highlight some parts

$ grep HIGHLIGHT FileTest.php 
                "echoOutput" => true // not recommended [HIGHLIGHT]
                "echoOutput" => false // recommended [HIGHLIGHT]

So let's launch the tests
 

$ phpunit FileTest.php 
PHPUnit 10.5.20 by Sebastian Bergmann and contributors.
Runtime:       PHP 8.1.2-1ubuntu2.17
    Copying file... please wait...
F.                                                                  2 / 2 (100%)
    There was 1 failure:
    1) FileTest::testCopy with data set #0 (true)
Failed asserting that null matches expected 'Copying file... please wait...\n
'.
    FileTest.php:37
FAILURES!
Tests: 2, Assertions: 2, Failures: 1.

As you can see, one of the two tests fails (the one in which true is given for the parameter), for the reason above.

For the other test, however, nothing is reported, so it means that it's fine, but... but... but... all of this hides an irregularity.

I don't know if I'll be able to explain it....

In practice, if you run

copyFileExample(echoOutput: true); // BAD PRACTICE (difficult to test output), BUT GOOD RESULT
  • "Copying file... please wait..." is displayed
  • the process stops for 5 seconds (pretending to copy some file)
  • the process ends normally

Instead if you execute

echo copyFileExample(echoOutput: false); // GOOD PRACTICE, BUT...
  • the process stops (hangs) for 5 seconds: it seems to have frozen, no output...
  • but then suddenly it says "Copying file... please wait..."; when in reality the waiting is already over (there is nothing left to wait for): the time sequence is not respected

So is there a way to reconcile the two?

Edited by rick645
  • Solution

Here’s how you can achieve that:

Separate Logic from Output: Move the output logic outside the core function.

Use Dependency Injection: Inject a callable (e.g., a function or a method) that handles the output. This allows you to control the output during testing.

<?php

function copyFileExample($outputCallback = null) {
    $verboseText = "Copying file... please wait...\n";
    
    if ($outputCallback) {
        call_user_func($outputCallback, $verboseText);
    }
    
    // Simulate the file copy with sleep(5)
    sleep(5); // replaces copy() function
    
    return $verboseText;
}

class FileTest extends PHPUnit\Framework\TestCase {
    
    static function copyProvider() {
        return [
            [
                "outputCallback" => null // recommended [HIGHLIGHT]
            ],
            [
                "outputCallback" => function($text) { echo $text; } // not recommended for testing [HIGHLIGHT]
            ],
        ];
    }

    /**
     * @dataProvider copyProvider
     */
    function testCopy($outputCallback) {
        $expectedOutput = "Copying file... please wait...\n";

        ob_start();
        $result = copyFileExample($outputCallback);
        $output = ob_get_clean();

        if ($outputCallback) {
            $this->assertEquals($expectedOutput, $output);
        } else {
            $this->assertEquals($expectedOutput, $result);
        }
    }
}
?>

Function Definition:

The copyFileExample function now accepts an optional $outputCallback parameter, which can be any callable (function, method, etc.).

If $outputCallback is provided, it is called with the verbose text. This decouples the printing logic from the core function.

Test Class:

The copyProvider method returns different scenarios, including one without any callback and one with a callback that echoes the text.

The testCopy method captures any output printed by the callback using output buffering (ob_start, ob_get_clean).

The assertions check the output if a callback is provided and the return value otherwise.

 

$ phpunit FileTest.php
PHPUnit 10.5.20 by Sebastian Bergmann and contributors.
Runtime:       PHP 8.1.2-1ubuntu2.17
..
OK (2 tests, 2 assertions)

With this approach, both tests should pass, ensuring that the function behaves correctly with and without output, and making it easier to test and maintain.

 

Best Regard

Danish hafeez | QA Assistant

ICTInnovations

Posted (edited)

Nor is it convenient to always return all the output in the end, and then print it all in one fell swoop.

Often, in fact, as already mentioned, it is a good thing to have intermediate outputs (avoiding embarrassing mute scenes), to understand what the software is doing running (also respecting the time sequence)

fwrite($outputStream, Downloading...\n”);
// do something
fwrite($outputStream, Unpacking...\n”);
// do something
fwrite($outputStream, Install...\n”);
// do something

What do you think?

Edited by rick645
12 hours ago, rick645 said:
fwrite($outputStream, Downloading...\n”);
// do something
fwrite($outputStream, Unpacking...\n”);
// do something
fwrite($outputStream, Install...\n”);
// do something

Of course, to keep the global namespace in order, it would be preferable to encapsulate everything in a function main(), front_controller(), etc., and call it.

A lot of people would probably say that many of these types of issues can be solved when you make use of the Dependency Injection (aka Inversion of Control) pattern.  There are logging libraries like the well known "Monolog" component library:  https://packagist.org/packages/monolog/monolog

You would do something similar in regards to a copy operation, and not put sleep code in, which isn't a simulation of anything.

You have a contrived example, and are using a unit test for something that, I guess appears to be a functional test.

Unit tests are meant to insure that all code is executed, and the intention you had when writing a function or method can be proven to meet the design expectations.  It is not something that people are expected to sit and watch.  In fact, quite the opposite in situations where people are using the unit tests in Continuous Integration (CI) and possibly continuous delivery (CD).  

The very fact that you are trying to simulate the expectation that an end user would be sitting tailing a log or looking at a console tells you that you are in the realm of user experience and not unit testing.

 

 

 

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.