I'm playing with the console component of Symfony and I'm facing an issue when it comes to testing.
I have a command that can move some files and display messages depending on the actions performed. I'm using SymfonyStyle to format my output.
I'm using the CommandTester to test my command but if I'm able to test whether the command did something or not, I can't find a efficient way to test its output.
Here is what I'm trying to do :
<?php
public function testIgnoreSamples()
{
$container = $this->application->getContainer();
$container['config'] = [
'source_directory' => vfsStream::url('Episodes/From'),
'target_directory' => vfsStream::url('Episodes/To'),
'ignore_if_nuked' => false,
'delete_nuked' => false,
'search_subtitles' => false,
'prefer_move_over_copy' => false
];
copy(
__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Fixtures/breakdance.mp4',
vfsStream::url('Episodes/From/sample-angie.tribeca.s01e07.720p.hdtv.x264-killers.mkv')
);
$commandTester = new CommandTester($this->application->find('episodes:move'));
$commandTester->execute([]);
$this->assertContains('because it\'s a sample', $commandTester->getDisplay());
$this->assertEquals([], vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure()['Episodes']['To'], 'Target directory is empty');
}
The issue here is that depending on the console size, my output can be displayed on one or two lines, which makes it hard to write tests that can be executed in different environment.
For example in my environment it's displayed like this :
and in travis like this :
which brakes the tests.
Do you know if the component provides a workaround for this case?
Use the wordwrap function to constrain all output to 75 or so columns.
$output->writeln(wordwrap($long_string));
I finally figured how to fix the terminal size for the tests.
Starting in v3.2, symfony/console allows us to fix the terminal size, which is used by the SymfonyStyle to build the output.
Just call putenv('COLUMNS=80') before executing a command to fix its terminal size.
My test is now :
public function testIgnoreSamples()
{
$container = $this->application->getContainer();
$container['config'] = [
'source_directory' => vfsStream::url('Episodes/From'),
'target_directory' => vfsStream::url('Episodes/To'),
'ignore_if_nuked' => false,
'delete_nuked' => false,
'search_subtitles' => false,
'prefer_move_over_copy' => false
];
copy(
__DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'Fixtures/breakdance.mp4',
vfsStream::url('Episodes/From/sample-angie.tribeca.s01e07.720p.hdtv.x264-killers.mkv')
);
putenv('COLUMNS=80');
$commandTester = new CommandTester($this->application->find('episodes:move'));
$commandTester->execute([]);
$expected = <<<'EXPECTED'
! [NOTE] File sample-angie.tribeca.s01e07.720p.hdtv.x264-killers.mkv ignored
! because it's a sample
EXPECTED;
$this->assertContains($expected, $commandTester->getDisplay());
$this->assertEquals([], vfsStream::inspect(new vfsStreamStructureVisitor())->getStructure()['Episodes']['To'], 'Target directory is empty');
}
and the tests are green in travis and in my local environment :).
Related
Using Symfony 5.4, I have created a command to run several commands (just to refresh my app quickly), here is my code:
// #see https://symfony.com/doc/current/console/calling_commands.html
$commands = [
'doctrine:schema:drop' => [
'--force' => true,
],
'doctrine:schema:update' => [
'--force' => true,
],
'doctrine:fixtures:load' => [
'-n' => true,
'--group' => ['dev'],
],
'fos:elastica:populate' => []
];
foreach ($commands as $command => $arguments) {
$output->writeln([
'Execute command',
$command,
'========================',
'',
]);
$command = $this->getApplication()->find($command);
$command->run(new ArrayInput($arguments), $output);
}
It's working fine, except the command doctrine:fixtures:load is asking for:
Careful, database "store" will be purged. Do you want to continue? (yes/no)
I have to set y to continue.
Looking at the help of the command, it seems -n (or --no-interaction) is what I want, and indeed, launching:
bin/console doctrine:fixtures:load --group=dev -n
manualy works fine.
Why the $arguments are not working for this command ? Did I miss something ?
Try this :
$nonInteractiveArguments = new ArrayInput($arguments);
$nonInteractiveArguments->setInteractive(false);
$command->run($nonInteractiveArguments, $output);
to set Input interactivity to false.
i'm having a bit of a problem with my PHPUnit integration tests, i have a method which handles a form upload for a video file as well as a preview image for that video.
public function store($request)
{
/** #var Video $resource */
$resource = new $this->model;
// Create a new Content before creating the related Photo
$contentRepo = new ContentRepository();
$content = $contentRepo->store($request);
if($content->isValid()) {
$resource->content_id = $content->id;
$directory = 'frontend/videos/assets/'.date("Y").'/'.date('m').'/'.time();
\File::makeDirectory($directory, 0755, true);
$request->video->move($directory.'/', $request->video->getClientOriginalName());
$resource->video = '/'.$directory.'/'.$request->video->getClientOriginalName();
$request->preview_image->move($directory.'/', $request->preview_image->getClientOriginalName());
$resource->preview_image = '/'.$directory.'/'.$request->preview_image->getClientOriginalName();
$resource->highlighted = intval($request->input('highlighted') == 'on');
$resource->save();
return $resource;
}
else {
return $content;
}
}
The important part to keep is the $request->video->move() call which i probably need to replace in order to use Virtual Filesystem.
and then the test
public function testVideoUpload(){
File::put(__DIR__.'/frontend/videos/assets/image.mp4', 'test');
$file = new UploadedFile(__DIR__.'/frontend/videos/assets/image.mp4', 'foofile.mp4', 'video/mp4', 100023, null, $test=true);
File::put(__DIR__.'/frontend/images/assets/image.jpg', 'test');
$preview = new UploadedFile(__DIR__.'/frontend/images/assets/image.jpg', 'foofile.jpg', 'image/jpeg', 100023, null, $test=true);
$this->post('/admin/videos', [
'title' => 'My Video #12',
'description' => 'This is a description',
'actors' => [$this->actor->id, $this->actor2->id],
'scenes' => [$this->scene->id, $this->scene2->id],
'payment_methods' => [$this->paymentMethod->id],
'video' => $file,
'preview_image' => $preview
])->seeInDatabase('contents', [
'title' => 'My Video #12',
'description' => 'This is a description'
]);
}
As you can see, i need to create a dummy file in some local directory and then use that in the HTTP request to the form's endpoint, then after that, that file would be moved and i need to delete the created folder and the new moved file... it's an authentic mess.
As such i want to use Virtual Filesystem instead, but i have no idea how to set it up in this particular case, i've already downloaded a package and set it up, but the questions are, first, which package have you used/recommend and how would you tweak the class and the test to support the Virtual Filesystem? Would i need to switch over to using the Storage facade instead of the $request->video->move() call? If so how would that be done exactly?
Thank you in advance for your help
I couldn't figure out the VFS system, however i do have somewhat of an alternative that's still kinda messy but gets the job done.
Basically i set up two methods on my PHPUnit base class to setup and teardown the temp folders i need on any test that requires them, because i'm using Database Transactions the files get deleted on every test run and i need to create new dummy files every time i run the test.
So i have two methods setupTempDirectories and teardownTempDirectories which i will call at the beginning and at the end of each test that requires those temporary directories.
I put my temp files in the Storage directory because sometimes i run my tests individually through PHPStorm and the __DIR__ command gets messed up and points to different directories when i do that, i also tried __FILE__ with the same result, so i just resorted to using Laravel's storage_path instead and that works fine.
Then that leaves the problem of my concrete class which tries to move files around and create directories in the public folder for them... so in order to fix that i changed the code to use the Storage facade, then i Mock the Storage facade in my tests
So in my concrete class
$directory = 'frontend/videos/assets/'.date("Y").'/'.date('m').'/'.time();
Storage::makeDirectory($directory, 0755, true);
Storage::move($request->video, $directory . '/' . $request->video->getClientOriginalName());
$resource->video = '/'.$directory.'/'.$request->video->getClientOriginalName();
Storage::move($request->preview_image, $directory . '/' . $request->preview_image->getClientOriginalName());
$resource->preview_image = '/'.$directory.'/'.$request->preview_image->getClientOriginalName();
And then in my test i mock both the makeDirectory and the move methods like such
// Override the Storage facade with a Mock version so that we don't actually try to move files around...
Storage::shouldReceive('makeDirectory')->once()->andReturn(true);
Storage::shouldReceive('move')->twice()->andReturn(true);
That makes my tests work and does not actually leave files behind after it's done...i hope someone has a better solution but for the time being this is what i came up with.
I was actually trying to use VFS but it never worked out... i keep getting errors that the original file in the storage directory is not found even though it's right there...
I'm not even sure the Storage facade was using VFS in the background to begin with even though it should...
For development we have a single Symfony console command that executes other console commands in order to rebuild db, run fixtures etc.
As part of the process I need to run a few cherry-picked doctrine migration commands, but for some reason I'm unable to run multiple execute commands within the same process.
To confirm, I can run these tasks without issue manually, and can run one of either command within the console execute and then the other manually without issue.
$this->getApplication()->run(new ArrayInput(array(
'command' => 'doctrine:migrations:execute',
'version' => '20140310162336',
'--no-interaction' => true
)), $output);
$this->getApplication()->run(new ArrayInput(array(
'command' => 'doctrine:migrations:execute',
'version' => '20140310170437',
'--no-interaction' => true
)), $output);
The error returned is:
[Doctrine\DBAL\Migrations\MigrationException]
Migration version 20140310162334 already registered with class Doctrine\DBAL\Migrations\Version
The version being the first version file that exists, can confirm that one is not in the migration_versions table, nor is it wanted in this scenario. Suggesting it is just loaded into the migrations object.
Can anyone offer input if I'm doing something wrong of if this is perhaps a bug somewhere.
Running Symfony 2.2.* and migrations bundle using dev-master.
I had the same problem on symfony 2.6 and the solution described by Alexei Tenitski didn't work althought it seemed a valid one.
This is the solution that worked for me.
/**
* Loop thorugh the config and path config for migrations
* and execute migrations for each connection
*/
foreach (array_keys($this->migrationsConfig) as $configEm) {
if (
(empty($ems) || in_array($configEm, $ems))
&& !in_array($configEm, $ignoreEms)
) {
try {
// new instance of the command you want to run
// to force reload MigrationsConfig
$command = new MigrateSingleCommand($this->migrationsConfig);
$command->setApplication($this->getApplication());
$arguments = [
'command' => $commandString,
'--em' => $configEm,
];
$input = new ArrayInput($arguments);
$command->run($input, $output);
} catch (\Exception $e) {
$output->writeln(sprintf("<error>Error: %s</error>", $e->getMessage()));
}
}
}
if you use $this->getApplication()->run() it will take the command from $this->application->commands where the commands are initialized only once and (when the command calling is initialized) so the MigrationsConfig will stay the same on all iterations.
The problem is that application uses same instance of command for each call and Doctrine migrate commands are not designed to work in such environment. One way to work around it is to clone command and work with its instance directly:
$commandName = 'doctrine:migrations:execute';
$prototypeCommand = $this->getApplication()->get($commandName);
// This is required to avoid merging of application definition for each cloned command
$prototypeCommand->mergeApplicationDefinition();
// Create a clone for a particular run
$command1 = clone $prototypeCommand;
// Run the command with specific params
$command1->run($input1, $output)
// Create another clone
$command2 = clone $prototypeCommand;
// Run the command with another set of params
$command2->run($input2, $output)
My guess is that it is because you are trying to run the migration command multiple times at once.
You might want to try using a work queue system, there is probably even a bundle that does that.
I have a Laravel 4 application with sqlite db configured for testing.
I am working in a workbench package
I have a problem testing my models in a PHPUnit Test, because i defined some unique properties on my model. I run Artisan::call('migrate', array('--bench' => 'vendor/webshop')); from my Basic Test Class from which i extend other Tests.
I think this runs the database migrations, but in my opinion it does not delete the models in the database.
Because if i do
public function setUp() {
parent::setUp();
$this->order = Order::create(array(
"uniquekey" = "123"
));
}
I get an error saying, can not insert model because of violation of unique key rule.
How should i clean the database before every test?
You should define environment for testing purposes.
Actually Laravel does have one for you - notice testing folder inside your app/config.
Create app/config/testing/database.php (or modify if exists) and place this:
return array(
'default' => 'sqlite',
'connections' => array(
'sqlite' => array(
'driver' => 'sqlite',
'database' => ':memory:', // this will do the trick ;)
'prefix' => '',
),
),
);
When you run tests, Laravel sets it's environment to 'testing' automaticaly - no need to play with this.
SQLite in-memory DB is memory stored withing RAM, thus it's fast - and yes, tests start with empty database as well.
I am trying to learn phpunit while I work on a project, this project requires some files to be copied so I thought I would use vfsStream to mock the filesystem for my unit tests.
as it turns out whenever I include and use the vfsStreamWrapper class, phpunit doesn't seem to care about calling functions that don't exist or any other output for that matter.
the function below I copied over from an example and I changed the second function being called to something I am certain doesn't exist:
protected function setUp()
{
vfsStreamWrapper::register();
vfsStreamWrapper::yowkes(array('Core' => array('AbstractFactory' => array('test.php' => 'some text content',
'other.php' => 'Some more text content',
'Invalid.csv' => 'Something else',
),
'AnEmptyFolder' => array(),
'badlocation.php' => 'some bad content',
)
));
}
All phpunit does is print the following output:
PHPUnit 3.7.21 by Sebastian Bergmann.
I am running phpunit from a terminal through ssh on a vragant box.
Any help is appreciated.
edit:
public function testFilesAreCopiedCorrectly() {
$remoteDirectory = 'files/remoteFiles/';
$correctFilePaths = array('20130627-1830-01.xml',
'20130627-1830-02.xml',
'20130627-1830-03.xml',
'20130627-1830-04.xml',
'20130627-1830-05.xml',
'20130627-1830-06.xml',
'20130627-1830-07.xml',
'20130627-1830-08.xml',
'20130627-1830-09.xml',
);
$remoteImportFileGetter = new RemoteImportFileGetter($remoteDirectory,
$correctFilePaths,
vfsStream::url('localDir'));
$remoteImportFileGetter->copyFilesToLocalDirectory();
$localFiles = $this->root->getChildren();
$this->assertCount(9, $localFiles);
$this->assertEquals($correctFilePaths, $localFiles);
$i = 0;
foreach($correctFilePaths as $remoteFile){
$this->assertEquals(file_get_contents($remoteFile), file_get_contents($localFiles[$i]));
$i++;
}
}