我正在尝试测试一个自定义的Artisan命令,该命令可以执行多项操作,然后最后执行csv导入。我在工匠命令中手动new CsvDirectDatabaseImporter
实例化该对象。这将运行称为import()
的方法,该方法使用SQLite不支持的LOAD DATA LOCAL INFILE
从csv导入数据库。因为我想让测试在内存中运行,所以我想覆盖CsvDirectDatabaseImporter类上的import方法(或模拟/存根不知道正确的术语是什么),以便在导入调用期间不执行任何操作。这样,我的其余测试将可以正常工作(我现在知道我没有测试实际的导入),我将如何解决这个问题:
这是我的工匠类的简化版本:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use App\Services\CsvDirectDatabaseImporter\CsvDirectDatabaseImporter;
use App\Services\CsvDirectDatabaseImporter\MyColumns;
class DataMartImport extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'myimport:import
{year : The year of processing} ';
/**
* The console command description.
*
* @var string
*/
protected $description = 'My Import';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$year = $this->argument('year');
// Copy the file to processing location.
File::copy($files[0], $processing_file);
// Import the CSV File.
$csvImporter = new CsvDirectDatabaseImporter($processing_file, 'myTable', new MyColumns());
$csvImporter->import();
}
}
运行我的自定义工匠命令的功能测试的简化版本:
<?php
namespace Tests\Feature\Console\DataMart;
use Illuminate\Support\Facades\File;
use Tests\TestCase;
use Illuminate\Support\Facades\Config;
use Mockery as m;
use App\Services\CsvDirectDatabaseImporter\DataMartColumns;
use App\Services\CsvDirectDatabaseImporter\CsvDirectDatabaseImporter;
use Illuminate\Support\Facades\Artisan;
class MyImportTest extends TestCase
{
public function testImportFoldersGetCreatedIfNoDirectory()
{
$year = 2019;
$this->artisan('myimport:import', ['year' => $year]);
// Assertions of items go here unrelated to the actual database import.
}
}
CSVImorter类
<?php
namespace App\Services\CsvDirectDatabaseImporter;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
use Symfony\Component\HttpFoundation\File\File as CSV_File;
class CsvDirectDatabaseImporter {
/**
* File to import.
*
* @var \Symfony\Component\HttpFoundation\File\File
*/
private $file;
/**
* Table name.
*
* @var string
*/
private $table;
/**
* Fields terminated by.
*
* @var string
*/
public $fieldsTerminatedBy = '\',\'';
/**
* Enclosed by.
*
* @var string
*/
public $enclosedBy = '\'"\'';
/**
* Lines terminated by.
*
* @var string
*/
public $linesTerminatedBy = '\'\n\'';
/**
* Ignore first row.
*
* @var bool
*/
public $ignoreFirstRow = true;
/**
* Csv Import columns.
*
* @var array
*/
public $columns;
/**
* CsvImporter constructor.
*
* @param string $path
* The full temporary path to the file
*/
public function __construct(string $path, $table, CsvDirectDatabaseImportColumns $columns)
{
$this->file = new CSV_File($path);
$this->table = $table;
$this->columns = $columns->getColumns();
}
/**
* Import method used for saving file and importing it using database query.
*/
public function import()
{
// Normalize line endings
$normalized_file = $this->normalize($this->file);
// Import contents of the file into database
return $this->importFileContents($normalized_file, $this->table, $this->columns);
}
/**
* Convert file line endings to uniform "\r\n" to solve for EOL issues
* Files that are created on different platforms use different EOL characters
* This method will convert all line endings to Unix uniform
*
* @param string $file_path
* @return string $file_path
*/
protected function normalize($file_path)
{
// Load the file into a string.
$string = @file_get_contents($file_path);
if (!$string) {
return $file_path;
}
// Convert all line-endings using regular expression.
$string = preg_replace('~\r\n?~', "\n", $string);
file_put_contents($file_path, $string);
return $file_path;
}
/**
* Import CSV file into Database using LOAD DATA LOCAL INFILE function
*
* NOTE: PDO settings must have attribute PDO::MYSQL_ATTR_LOCAL_INFILE => true
*
* @param string $file_path
* File path.
* @param string $table_name
* Table name.
* @param array $columns
* Array of columns.
*
* @return mixed Will return number of lines imported by the query
*/
private function importFileContents($file_path, $table_name, $columns)
{
$prefix = config('database.connections.mysql.prefix');
$query = '
LOAD DATA LOCAL INFILE \'' . $file_path . '\' INTO TABLE `' . $prefix . $table_name . '`
FIELDS TERMINATED BY ' . $this->fieldsTerminatedBy . '
ENCLOSED BY ' . $this->enclosedBy . '
LINES TERMINATED BY ' . $this->linesTerminatedBy . '
';
if ($this->ignoreFirstRow) {
$query .= ' IGNORE 1 ROWS ';
}
if ($columns) {
$query .= '(' . implode(",\n", array_keys($columns)) . ')';
$query .= "\nSET \n";
$sets = [];
foreach ($columns as $column) {
$sets[] = $column['name'] . ' = ' . $column['set'];
}
$query .= implode(",\n", $sets);
}
return DB::connection()->getPdo()->exec($query);
}
}
CsvDirectDatabaseImportColumns接口
<?php
namespace App\Services\CsvDirectDatabaseImporter;
interface CsvDirectDatabaseImportColumns
{
/**
* Returns array of columns.
*
* Ex:
* '@user_id' => [
* 'name' => 'user_id',
* 'set' => '@user_id',
* ],
* '@agent_number' => [
* 'name' => 'agent_number',
* 'set' => 'LEFT(@agent_number, 7)',
* ],
*
* The key should be the column name of the csv but add @ in front. The name
* will be the database table. The set will be what it s se
*
* @return array
*/
public function columns();
/**
* Returns columns.
*
* @return array
* Columns.
*/
public function getColumns();
}
我尝试过的事情
$mock = $this->createMock(CsvDirectDatabaseImporter::class);
$mock->method('import')->willReturn(true);
$this->app->instance(CsvDirectDatabaseImporter::class, $mock);
$this->artisan('datamart:import', ['year' => $year]);
但是那里没有运气。它仍然运行常规的import()方法。
答案 0 :(得分:1)
因此,我尝试将我认为您需要的内容复制到一个简单的示例中
假设我们有此命令
<?php
namespace App\Console\Commands;
use Exception;
use Illuminate\Console\Command;
class Foo extends Command
{
protected $signature = 'foo';
public function __construct()
{
parent::__construct();
}
public function handle()
{
if ($this->import()) {
$this->info('Success');
} else {
$this->error('Failed');
}
}
public function import()
{
throw new Exception('An exception that should not be thrown');
}
}
import方法会引发异常,但这是模拟它以返回true的方法
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Console\Commands\Foo;
class FooCommandTest extends TestCase
{
public function testExample()
{
$mock = $this->getMockBuilder(Foo::class)->setMethods(['import'])->getMock();
$mock->method('import')->willReturn(true);
$this->app->instance('App\Console\Commands\Foo', $mock);
$this->artisan('foo')
->expectsOutput('Success')
->assertExitCode(0);
}
}
此测试通过了两次成功的断言,因此您可以调整命令代码以使用专用的导入方法
希望这会有所帮助