#19578·cakephp

Impossible to mock ConsoleIo::createFile() via ConsoleIntegrationTestTrait

Author: beporterCreated Aug 1, 2026Updated Aug 5, 2026
Labelsenhancement

Description

Background

The docs call out $io->createFile() as an "often important part of many console commands", but Cake's ConsoleIntegrationTestTrait doesn't provide a mechanism to stub/mock the ConsoleIo class it passes to the command runner.

Current code

https://github.com/cakephp/cakephp/blob/ceeda19a42be42e72ed2a6e5ac5d4633308943c5/src/Console/TestSuite/ConsoleIntegrationTestTrait.php#L84-L101

Issue

Because the above trait code works entirely with variables local to the method call ($args and $io), there's no opportunity to inject a mocked ConsoleIo class for the purpose of avoiding writing real files to disk during test runs.

Suggested Fixes

One option would be to just expose the ConsoleIo instance (somehow) so developers could override the $io instance passed to $runner->run($args, $io);. One possible implementation would be to use Cake's service container to build the ConsoleIo instance. That would allow developers to use the existing ->mockService() hook to inject a modified ConsoleIo instance into the exec() method itself. A downside to using the service registry for fetching modified/mocked class instances gets a little 'meta' though.

The more extreme (but probably much nicer experience) approach would be to refactor ::exec() to pre-stub the createFile method and always block disk writes by default (when using ConsoleIntegrationTestTrait::exec() at least.) This pattern is already in place for things like Caches and Io messages, so it doesn't seem like a huge stretch to me to bundle createFile in with those.

Example pseudo code (untested):

diff
    public function exec(string $command, array $input = []): void
    {
        $runner = $this->makeRunner();

        $this->_out ??= new StubConsoleOutput();
        $this->_err ??= new StubConsoleOutput();
        if ($this->_in === null || $input) {
            $this->_in = new StubConsoleInput($input);
        }
        $this->_out->clear();
        $this->_err->clear();

        $args = $this->commandStringToArgs("cake {$command}");
-       $io = new ConsoleIo($this->_out, $this->_err, $this->_in);
+       $this->_io ??= $this->getMockBuilder(ConsoleIo::class)
+           ->setConstructorArgs([$this->_out, $this->_err, $this->_in])
+           ->onlyMethods(['createFile'])
+           ->getMock();

        try {
-           $this->_exitCode = $runner->run($args, $io);
+           $this->_exitCode = $runner->run($args, $this->_io);
        } catch (MissingConsoleInputException $e) {
            $messages = $this->_out->messages();
            if ($messages !== []) {
                $e->setQuestion($messages[count($messages) - 1]);
            }
            throw $e;
        } catch (StopException $exception) {
            $this->_exitCode = $exception->getCode();
        }
    }

There are loads of possible variations, of course. I think the key part is switching from $io = to $this->_io ??= since this gives developers the opportunity to pre-set the ConsoleIo instance before $runner->run($args, $this->_io) is called. (Yes I know ??= might not be available in all php versions that Cake still supports-- this is just the most convenient syntax with which to share the idea of, "if not already set, use this mock".)

Happy to help brainstorm or test other possible avenues-- I'm in these weeds myself already.

CakePHP Version

5.4

PHP Version

8.5