I am new to unit testing. Started working on unit test using PHPUnit. But it seems to be taking too much time. If consider I take 3 hours to write a Class, its taking my 7 hours to write test case for it. There are a few factors behind it.
- As I told, I am new to this stuff, so have to do lot of R&D.
- Sometimes I get confused what to test in it.
- Mocking some function takes time.
- There are lots of permutation and combination in a big function, so it gets difficult and pretty time consuming to mock those functions.
Any idea how do I write test cases in faster way? Any ideas for actual code so it gets faster to write test cases?
What are the best practices I should follow in my code below?
<?php namespace Api\Core;
use Api\Exceptions\APICoreException;
use Api\Exceptions\APITransformationException;
use Api\Exceptions\APIValidationException;
use CrmValidation;
use Component;
use DB;
use App\Traits\Api\SaveTrait;
use App\Traits\Api\FileTrait;
use App\Repositories\Contract\MigrationInterface;
use App\Repositories\Contract\ClientFeedbackInterface;
use Mockery\CountValidator\Exception;
use Api\Libraries\ApiResponse;
use App\Repositories\Contract\FileInterface;
use App\Repositories\Contract\MasterInterface;
use App\Traits\Api\ApiDataConversionTrait;
use ClientFeedback;
use MigrationMapping;
use Migration;
use ComponentDetail;
use FavouriteEditorCore;
/**
* Class ClientFeedbackCore
*
* @package Api\Core
*/
class ClientFeedbackCore
{
use SaveTrait, FileTrait, ApiDataConversionTrait;
/**
* @var array
*/
private $request = [];
/**
* @var
*/
private $migrationFlag;
/**
* @var string
*/
private $table = 'client_feedback';
/**
* @var MigrationInterface
*/
public $migrationRepo;
/**
* @var ClientFeedbackInterface
*/
public $clientFeedbackRepo;
/**
* @var MasterInterface
*/
public $masterRepo;
/**
* @var FileInterface
*/
public $fileRepo;
/**
* ClientFeedbackCore constructor.
*
* @param MigrationInterface $migrationInterface
* @param ClientFeedbackInterface $clientFeedbackInterface
* @param MasterInterface $masterInterface
* @param FileInterface $fileInterface
*/
public function __construct(
MigrationInterface $migrationInterface,
ClientFeedbackInterface $clientFeedbackInterface,
MasterInterface $masterInterface,
FileInterface $fileInterface
) {
$this->clientFeedbackRepo = $clientFeedbackInterface;
$this->migrationRepo = $migrationInterface;
$this->masterRepo = $masterInterface;
$this->fileRepo = $fileInterface;
}
/**
* @author pratik.joshi
*/
public function init()
{
$this->migrationFlag = getMigrationStatus($this->table);
}
/**
* @param $request
* @return array
* @author pratik.joshi
* @desc stores passed data into respective entities and then stores into migration tables. If any issue while insert/update exception is thrown.
*/
public function store($request)
{
if ($request == null || empty($request))
{
throw new APIValidationException(trans('messages.exception.validation',['reason'=> 'request param is not provided']));
}
$clientFeedbackId = $migrationClientFeedbackId = $favouriteEditorId = null;
$errorMsgWhileSave = null;
$clientFeedback = [];
$filesSaved = [];
$categoryNamesForFiles = [];
$operation = config('constants.op_type.INSERT');
$this->init();
if(
keyExistsAndissetAndNotEmpty('id',$request)
&& CrmValidation::getRowCount($this->table, 'id', $request['id'])
) {
$operation = config('constants.op_type.UPDATE');
}
//Step 1: set up data based on the operation
$this->request = $this->convertData($request,$operation);
//Step 2: Save data into repo, Not using facade as we cant reuse it, every facade will repeat insert update function
if ($operation == config('constants.op_type.INSERT'))
{
$clientFeedback = $this->insertOrUpdateData($this->request, $this->clientFeedbackRepo);
}
else if($operation == config('constants.op_type.UPDATE'))
{
$clientFeedback = $this->insertOrUpdateData($this->request, $this->clientFeedbackRepo,$this->request['id']);
}
if ( !keyExistsAndissetAndNotEmpty('client_feedback_id',$clientFeedback[ 'data' ]) )
{
throw new APICoreException(trans('messages.exception.data_not_saved'));
}
//If no exception thrown, save id
$clientFeedbackId = $clientFeedback[ 'data' ][ 'client_feedback_id' ];
//Step 3: prepare array for mig repo & save()
if($this->migrationFlag && $operation == config('constants.op_type.INSERT'))
{
$this->saveMigrationDataElseThrowException($this->table, $clientFeedback[ 'data' ][ 'client_feedback_id' ], 'client_feedback', $this->request['name']);
}
//If no exception thrown, save id
$paramsForFileSave = [
'entity_id' => $clientFeedbackId,
'entity_type' => $this->clientFeedbackRepo->getModelName(),
];
//Step 4: Save datainto file, Save job feedback files with params : files array to save, migration data for files
//The method prepareFileData will be called by passing multiple files, and some needed params for file which internally calls prepareData
//$filePreparedData will be in format : $filePreparedData['field_cf_not_acceptable_four'][0] => whole file array(modified)
$filePreparedData = $this->fileRepo->prepareFileData($this->request[ 'files' ], $this->masterRepo, $paramsForFileSave);
$filesSaved = $this->fileRepo->filesInsertOrUpdate($filePreparedData);
//If any file is not saved, it returns false, throw exception here
if($filesSaved == false)
{
throw new APICoreException(trans('messages.exception.data_not_saved'));
}
//Step 5: Save data for file in migra repo.
//For each file type and each file in it, loop, Check for insert data
if(getMigrationStatus('file') && array_key_exists('insert',$filesSaved) && count($filesSaved['insert']))
{
foreach ($filesSaved['insert'] as $singleFileSaved)
{
$fileId = $singleFileSaved['data']['file_id'];
$wbTitle = $filesSaved['extra'][$fileId];
$this->saveMigrationDataElseThrowException('file', $singleFileSaved['data']['file_id'], 'files', $wbTitle);
}
}
//We get created by or last modified by
$createdOrLastModifiedBy = keyExistsAndissetAndNotEmpty('created_by',$this->request) ? $this->request['created_by'] : $this->request['last_modified_by'];
//Calling FavouriteEditorCore as we want to save favorite or un-favorite editor
$favouriteEditor = FavouriteEditorCore::core(
$this->request[ 'component_id' ],
$this->request[ 'rating' ],
$this->request[ 'wb_user_id' ], $createdOrLastModifiedBy,
$this->request[ 'same_editor_worker' ]
);
if ( !issetAndNotEmpty($favouriteEditor[ 'data' ][ 'favourite_editor_id' ]) )
{
throw new APICoreException(trans('messages.exception.data_not_saved'));
}
//If no exception thrown, save id
$favouriteEditorId = $favouriteEditor[ 'data' ][ 'favourite_editor_id' ];
//repare array for mig repo & save()
if(getMigrationStatus('favourite_editor') && $operation == 'insert')
{
$this->saveMigrationDataElseThrowException('favourite_editor', $favouriteEditor[ 'data' ][ 'favourite_editor_id' ], 'favourite_editor', null);
}
// Check if any error while saving
$dataToSave = [
'client_feedback_id' => $clientFeedbackId,
'files' => keyExistsAndissetAndNotEmpty('extra',$filesSaved) ? array_keys($filesSaved['extra']) : null,
'favourite_editor' => $favouriteEditorId
];
//@todo : return standard response
// Return final response to the WB.
return [
'data' => $dataToSave,
'operation' => $operation,
'status' => ApiResponse::HTTP_OK,
'error_message' => isset($errorMsgWhileSave) ? $errorMsgWhileSave : null
];
}
/**
* @param $request
* @param $operation
* @return array
* @author pratik.joshi
*/
public function convertData($request,$operation)
{
if(
($request == null || empty($request)) ||
($operation == null || empty($operation))
)
{
throw new APIValidationException(trans('messages.exception.validation',['reason'=> 'either request or operation param is not provided']));
}
//If blank
echo ' >> request';echo json_encode($request);
echo ' >> operation';echo json_encode($operation);
//Normal data conversion
$return = $this->basicDataConversion($request, $this->table, $operation);
echo ' >> return after basicDC';echo json_encode($return);
//Custom data conversion
$return[ 'client_code' ] = $request[ 'client_code' ];
$return[ 'component_id' ] = $request[ 'component_id' ];
if (isset( $request[ 'rating' ] ) )
{
$return[ 'rating' ] = $request[ 'field_cf_rating_value' ] =$request[ 'rating' ];
}
//Add client feedback process status, in insert default it to unread
if($operation == config('constants.op_type.INSERT'))
{
$return[ 'processing_status' ] = config('constants.processing_status.UNREAD');
}
else if($operation == config('constants.op_type.UPDATE'))
{
//@todo : lumen only picks config() in lumen only, explore on how to take it from laravel
//if its set and its valid
$processing_status_config = array_values(config('app_constants.client_feedback_processing_status')); // Get value from app constant
if (isset( $request[ 'processing_status' ] ) && in_array($request['processing_status'],$processing_status_config))
{
$return[ 'processing_status' ] = $request[ 'field_cf_status_value' ] = $request[ 'processing_status' ] ;
}
}
//@todo : check for NO
if (isset($request[ 'same_editor_worker' ])) {
if($request[ 'same_editor_worker' ] == 'no')
{
$return[ 'wb_user_id' ] = null;
}
else
{
$return[ 'wb_user_id' ] = ComponentDetail::getLastWorkerId($request[ 'component_id' ]);
}
}
//Get job title and prepend with CF
$return[ 'name' ] = 'CF_'.Component::getComponentTitleById($request[ 'component_id' ]);
//@todo check with EOS team for params
$dataFieldValues = setDataValues(config('app_constants.data_fields.client_feedback'), $request);
// unset which field we are storing in column
$return[ 'data' ] = json_encode($dataFieldValues);
echo ' >> return '.__LINE__;echo json_encode($return);
echo ' >> request & return '.__LINE__;echo json_encode(array_merge($request, $return));
return array_merge($request, $return);
}
/**
* @param $crmTable
* @param $crmId
* @param $wbTable
* @param $wbTitle
* @return mixed
* @throws APICoreException
* @author pratik.joshi
*/
public function saveMigrationDataElseThrowException($crmTable, $crmId, $wbTable, $wbTitle)
{
$dataToSave = Migration::prepareData([
'crm_table' => $crmTable,
'crm_id' => $crmId,
'whiteboard_table' => $wbTable,
'whiteboard_title' => $wbTitle
]);
//Save into migration repo
$migrationData = $this->insertOrUpdateData($dataToSave, $this->migrationRepo);
if ( !keyExistsAndissetAndNotEmpty('migration_id',$migrationData[ 'data' ]) )
{
throw new APICoreException(trans('messages.exception.data_not_saved'));
}
return $migrationData[ 'data' ]['migration_id'];
}
}
//And test case
<?php
use Api\Core\ClientFeedbackCore;
use App\Repositories\Contract\MigrationInterface;
use App\Repositories\Contract\ClientFeedbackInterface;
use App\Repositories\Contract\FileInterface;
use App\Repositories\Contract\MasterInterface;
class ClientFeedbackCoreTest extends TestCase
{
public $mockClientFeedbackCore;
public $requestForConvertData;
public $returnBasicDataConversion;
public $operation;
public $convertedData;
public $mockMigrationRepo;
public $mockClientFeedbackRepo;
public $mockMasterRepo;
public $mockFileRepo;
public $clientFeedbackCore;
public $table;
public $saveFailedData;
public function setUp()
{
parent::setUp();
$this->requestForConvertData = [
'client_code' => 'SHBI',
'component_id' => '4556',
'same_editor_worker' => 'yes',
'created_by' => '83767',
'rating' => 'not-acceptable',
'files' =>
[
'field_cf_not_acceptable_four' =>
[
0 =>
[
'created_by' => '83767',
'status' => '1',
'filename' => 'manuscript_0115.docx',
'filepath' => 'sites/all/files/15-01-17/client_feedback/1484497552_manuscript_011512.docx',
'filemime' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'filesize' => '116710',
'timestamp' => '1484497552',
],
],
],
];
$this->returnBasicDataConversion = [
'crm_table' => 'client_feedback',
'active' => true,
'last_modified_date' => '2017-03-30 11:21:23',
'created_date' => '2017-03-30 11:21:23',
'created_by' => '83767',
'last_modified_by' => '83767',
];
$this->convertedData = [
'client_code' => 'SHBI',
'component_id' => '4556',
'same_editor_worker' => 'yes',
'created_by' => '83767',
'rating' => 'not-acceptable',
'files' =>
[
'field_cf_not_acceptable_four' =>
[
0 =>
[
'created_by' => '83767',
'status' => '1',
'filename' => 'manuscript_0115.docx',
'filepath' => 'sites/all/files/15-01-17/client_feedback/1484497552_manuscript_011512.docx',
'filemime' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'filesize' => '116710',
'timestamp' => '1484497552',
],
],
],
'field_cf_rating_value' => 'not-acceptable',
'crm_table' => 'client_feedback',
'active' => true,
'last_modified_date' => '2017-03-30 11:21:23',
'created_date' => '2017-03-30 11:21:23',
'last_modified_by' => '83767',
'processing_status' => 'unread',
'wb_user_id' => 1131,
'name' => 'CF_SHBI350',
'data' => '{"field_cf_acceptable_one":null,"field_cf_acceptable_two":null,"field_cf_acceptable_four":null,"field_cf_outstanding_one":null,"field_cf_outstanding_two":null,"field_cf_acceptable_three":null,"field_cf_outstanding_three":null,"field_cf_not_acceptable_one":null,"field_cf_not_acceptable_two":null,"field_cf_not_acceptable_three":null,"field_cf_acceptable_same_editor":null,"field_cf_outstanding_same_editor":null}',
];
$this->table = 'client_feedback';
$this->saveFailedData =
[
'status' => 400,
'data' => null,
'operation' => 'insert',
'error_message' => 'data save failed error'
];
//Mocking start
$this->mockMigrationRepo = Mockery::mock(MigrationInterface::class);
$this->mockClientFeedbackRepo = Mockery::mock(ClientFeedbackInterface::class);
$this->mockMasterRepo = Mockery::mock(MasterInterface::class);
$this->mockFileRepo = Mockery::mock(FileInterface::class);
//Set mock of the Core class
$this->mockClientFeedbackCore = Mockery::mock(ClientFeedbackCore::class,
[$this->mockMigrationRepo,
$this->mockClientFeedbackRepo,
$this->mockMasterRepo,
$this->mockFileRepo])->makePartial();
//Set expectations
$this->mockClientFeedbackRepo
->shouldReceive('getModelName')->andReturn($this->table);
//For insert data
$this->mockClientFeedbackCore->shouldReceive('convertData')
->with($this->requestForConvertData, 'insert')
->andReturn($this->convertedData);
}
public function tearDown()
{
// DO NOT DELETE
Mockery::close();
parent::tearDown();
}
/**
* @test
*/
public function method_exists()
{
$methodsToCheck = [
'init',
'store',
'convertData',
];
foreach ($methodsToCheck as $method) {
$this->checkMethodExist($this->mockClientFeedbackCore, $method);
}
}
/**
* @test
*/
public function validate_convert_data_for_insert()
{
//Mock necessary methods
$this->mockClientFeedbackCore->shouldReceive('basicDataConversion')
->with($this->requestForConvertData, 'client_feedback', 'insert')
->andReturn($this->returnBasicDataConversion);
ComponentDetail::shouldReceive('getLastWorkerId')
->with($this->requestForConvertData[ 'component_id' ])
->andReturn(1131);
Component::shouldReceive('getComponentTitleById')
->with($this->requestForConvertData[ 'component_id' ])
->andReturn('SHBI350');
$actual = $this->mockClientFeedbackCore->convertData($this->requestForConvertData, 'insert');
$this->assertEquals($this->convertedData, $actual);
}
/**
* @test
*/
public function validate_convert_data_without_params()
{
$errorMessage = '';
try{
$this->mockClientFeedbackCore->convertData(null, null);
}
catch (Exception $e){
$errorMessage = $e->getMessage();
}
$this->assertEquals('API Validation Error: Reason: either request or operation param is not provided', $errorMessage);
}
/**
* @test
*/
public function validate_store_without_params()
{
$errorMessage = '';
try{
$this->mockClientFeedbackCore->store(null);
}
catch (Exception $e){
$errorMessage = $e->getMessage();
}
$this->assertEquals('API Validation Error: Reason: request param is not provided', $errorMessage);
}
/**
* @test
*/
public function validate_store_client_feedback_save_fail()
{
$errorMessage = '';
/* $this->mockClientFeedbackCore->shouldReceive('convertData')
->with($this->requestForConvertData, 'insert')
->andReturn($this->convertedData);*/
//For insert, mock separately
//@todo : with() attribute does not work here : ->with($this->convertedData,$this->mockClientFeedbackRepo)
$this->mockClientFeedbackCore->shouldReceive('insertOrUpdateData')
->andReturn($this->saveFailedData);
try {
$this->mockClientFeedbackCore->store($this->convertedData);
} catch
(Exception $e) {
$errorMessage = $e->getMessage();
}
$this->assertEquals('MigrationError: Data not saved',
$errorMessage);
}
public function validate_store_migration_save_fail()
{
//saveMigrationDataElseThrowException
$this->mockClientFeedbackCore->shouldReceive('saveMigrationDataElseThrowException')
->with('crmTable', 123, 'wbTable', 'wbTitle')
->andReturn($this->saveFailedData);
try {
$this->mockClientFeedbackCore->store($this->convertedData);
} catch
(Exception $e) {
$errorMessage = $e->getMessage();
}
$this->assertEquals('MigrationError: Data not saved',
$errorMessage);
}
public function validate_store_file_save_fail()
{
}
public function validate_store_favourite_editor_save_fail()
{
}
public function validate_store_proper_save()
{
}
}
Please help me as I am exceeding deadlines due to not completing testcases in time.