I test case delle unità di scrittura richiedono tempo, qualche consiglio?

-2

Sono nuovo al test delle unità. Iniziato a lavorare sul test delle unità usando PHPUnit. Ma sembra che ci stia impiegando troppo tempo. Se considero che mi prendo 3 ore per scrivere una classe, sta prendendo le mie 7 ore per scrivere un caso di test per questo. Ci sono alcuni fattori dietro di esso.

  1. Come ho detto, sono nuovo di questa roba, quindi devo fare molta R & D.
  2. A volte mi viene confuso su cosa testare.
  3. Prendere in giro alcune funzioni richiede tempo.
  4. Ci sono molte permutazioni e combinazioni in una grande funzione, quindi diventa difficile e richiede molto tempo per prendere in giro queste funzioni.

Qualche idea su come scrivere casi di test in modo più veloce? Qualche idea per il codice reale in modo che sia più veloce scrivere casi di test?

Quali sono le migliori pratiche che dovrei seguire nel mio codice qui sotto?

<?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'];
    }
}

// E 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()
    {

    }

}

Aiutatemi perché sto superando le scadenze a causa del mancato completamento dei test in tempo.

    
posta Pratik C Joshi 01.04.2017 - 11:57
fonte

2 risposte

7

Non è insolito scrivere dei buoni test per un tempo considerevole - rigorosamente dovresti essere in grado di scrivere i tuoi test solo dalle specifiche e dall'interfaccia che è ciò che viene fatto in alcuni test formali ambienti e otteniamo comunque una copertura del 100%.

Un grande fattore è se gli sviluppatori non sono abituati a scrivere codice verificabile. In aziende come Aviation ci sono normalmente limiti specificati alla complessità del codice consentita, (un McCabe di < 10 è una linea guida usuale e un requisito per giustificare qualsiasi cosa oltre questo con un limite di 20 è quello a cui sono abituato a dovermi attenermi) .

Se guardi una sezione di codice ci sono alcune regole pratiche per quanti test ci vorranno per fully testarlo:

  • 1 per una semplice esecuzione quindi

Controlli dei parametri:

  • +1 per ogni valore di un parametro enumerato in alcune lingue +1 per valori di enumerazione non validi.
  • +6 per ogni parametro float, (0.0, > 0.0, < 0.0, + INF, -INF, NaN)
  • +5 per ogni parametro int, (0, > 0, < 0, MAXINT, -MAXINT) ecc.

Quindi per ciascuna decisione:

  • +1 per ogni caso in un passaggio
  • +2 per ciascuno di <, & gt ;, ==,! =
  • +3 per ciascuno di < =, > =

Questo si aggiunge velocemente.

Alcuni framework di test hanno la capacità di produrre automaticamente stub o mock che ti fanno iniziare bene.

Mentre procedi scoprirai che:

  1. Devi fare meno ricerche
  2. Creerai una libreria di mock / stub / smippet che puoi riutilizzare
  3. Diventerete per familiarità con gli strumenti
  4. Potresti persino essere in grado di istruire gli sviluppatori su come scrivere testabile, manutenibile, codice.
risposta data 01.04.2017 - 13:29
fonte
10

Scrivere casi di test richiede in genere un tempo se non si utilizza lo sviluppo basato su test, a causa di diversi fattori che vengono spiegati molto bene in Lavorare efficacemente con il codice legacy . Nel tuo caso specifico i metodi store e convertData sono lunghi e complicati, segnalando che alcune divisioni (ad esempio in metodi pubblici su DataStore e Converter classi) sono necessarie per rendere testabile il codice. Un odore di codice comune che mostra che il tuo codice è difficile da testare è il fatto che hai un metodo setUp molto lungo.

    
risposta data 01.04.2017 - 12:34
fonte

Leggi altre domande sui tag