TDD: rompe tutti i casi di test esistenti refactoring del codice

ho iniziato a seguire TDD nel mio progetto. ma da quando ho iniziato dopo aver letto alcuni articoli, sono un po ‘confuso dal momento che lo sviluppo ha rallentato. ogni volta che rifattore il mio codice, ho bisogno di cambiare i casi di test esistenti che ho scritto prima perché inizieranno a fallire. segue l’esempio:

    public class SalaryManager
    {
        public string CalculateSalaryAndSendMessage(int daysWorked, int monthlySalary)
        {
            int salary = 0, tempSalary = 0;
            if (daysWorked < 15)
            {
                tempSalary = (monthlySalary / 30) * daysWorked;
                salary = tempSalary - 0.1 * tempSalary;
            }
            else
            {
                tempSalary = (monthlySalary / 30) * daysWorked;
                salary = tempSalary + 0.1 * tempSalary;
            }

            string message = string.Empty;
            if (salary < (monthlySalary / 30))
            {
                message = "Salary cannot be generated. It should be greater than 1 day salary.";
            }
            else
            {
                message = "Salary generated as per the policy.";
            }

            return message;
        }
    }

ma ho trovato ora sto facendo molte cose in un unico metodo in modo da seguire il principio SRP ho rifactored a qualcosa di simile al seguito:

    public class SalaryManager
    {
        private readonly ISalaryCalculator _salaryCalculator;        
        private readonly SalaryMessageFormatter _messageFormatter;
        public SalaryManager(ISalaryCalculator salaryCalculator, ISalaryMessageFormatter _messageFormatter){
            _salaryCalculator = salaryCalculator;
            _messageFormatter = messageFormatter;
        }

        public string CalculateSalaryAndSendMessage(int daysWorked, int monthlySalary)
        {
            int salary = _salaryCalculator.CalculateSalary(daysWorked, monthlySalary);
            string message = _messageFormatter.FormatSalaryCalculationMessage(salary);

            return message;
        }
    }

    public class SalaryCalculator
    {
        public int CalculateSalary(int daysWorked, int monthlySalary)
        {
            int salary = 0, tempSalary = 0;
            if (daysWorked < 15)
            {
                tempSalary = (monthlySalary / 30) * daysWorked;
                salary = tempSalary - 0.1 * tempSalary;
            }
            else
            {
                tempSalary = (monthlySalary / 30) * daysWorked;
                salary = tempSalary + 0.1 * tempSalary;
            }
            return salary;
        }
    }

    public class SalaryMessageFormatter
    {
        public string FormatSalaryCalculationMessage(int salary)
        {
            string message = string.Empty;
            if (salary < (monthlySalary / 30))
            {
                message = "Salary cannot be generated. It should be greater than 1 day salary.";
            }
            else
            {
                message = "Salary generated as per the policy.";
            }
            return message;
        }
    }

questo potrebbe non essere il più grande dell’esempio. ma quello che volevo dire qui è non appena ho fatto il refactoring miei casi di prova esistenti che ho scritto per il SalaryManager iniziato a fallire e ho dovuto risolverli utilizzando beffardo.

questo accade tutto il tempo in scenari di tempo di lettura e il tempo di sviluppo aumenta con esso. non sono sicuro se scrivo il modo di scrivere del TDD. per favore aiutateci a capire.

EN From: TDD : Breaks all the existing test cases while refactoring the code

More similar articles:

7 Comments

  1. qual è un esempio di un test che passerebbe per il primo caso e fallirebbe per il secondo caso? il refactoring sembra che dovrebbe essere ok a livello concettuale, anche se il codice in realtà non compila.

  2. CalculateSalaryAndSendMessage prende due interi e restituisce un string. non hanno altre dipendenze – quindi i test dovrebbero essere corretti con qualsiasi refactoring. solo possibile cambiamento che vedo – aggiungere nuovi argomenti per costruttore di SalaryManager. non hai nemmeno bisogno di scrivere mock – passare le implementazioni reali e le prove funziona.

  3. @Fabio: Ho cambiato il codice per introdurre le dipendenze e rendere la mia domanda più comprensibile. ora si può per favore aiutarmi con tre domande 1) Appena spostare il codice a SalaryCalculator, Ho bisogno di fare beffardo giusto? 2) Dopo aver preso in giro, casi di test di calcolo salariale non saranno rilevanti per il gestore stipendio in quanto la logica non esiste in classe SalaryManager più 3) Ho bisogno di spostare i casi di test dal test di calcolo salary diventa più rilevante per SalaryCalculator?

    1. non prendere in giro SalarayCalculator – passare al SalaryManager nelle prove. beffa solo le dipendenze che rendono i test lenti (lettura / scrittura di file, database, servizi web o altre risorse esterne). 3.. – senza prendere in giro i vostri test rimangono lo stesso e sarete liberi di rifattore SalaryManager classe senza toccare i test.
  4. Whenever I refactor my code, I need to change the existing test cases I have written before because they will start failing.

    questo è certamente un’indicazione che qualcosa sta andando storto. la definizione popolare di refactoring va qualcosa di simile a questo

    REFACTORING is a disciplined technique for restructuring an existing body of code, altering its internal structure without changing its external behavior.

    parte del punto di avere i test unità, è che i test unità stanno valutando il comportamento esterno della vostra implementazione. un test di unità che fallisce indica che un cambiamento di implementazione ha cambiato il comportamento osservabile esternamente in qualche modo.

    in questo caso particolare, sembra che hai cambiato la tua API – in particolare, è stato rimosso il costruttore di default che era stato parte delle API per la creazione di istanze di SalaryManager; che non è un “refactoring”, è un cambiamento di rottura all’indietro.

    non c’è niente di sbagliato nell’introdurre nuovi collaboratori durante il refactoring, ma si dovrebbe farlo in un modo che non rompe l’attuale contratto API.

    public class SalaryManager
    {
        public SalaryManager(ISalaryCalculator salaryCalculator, ISalaryMessageFormatter _messageFormatter){
            _salaryCalculator = salaryCalculator;
            _messageFormatter = messageFormatter;
        }
    
        public SalaryManager() {
            this(new SalaryCalculator(), new SalaryMessageFormatter())
        }

    dove SalaryCalculator e SalaryMessageFormatter dovrebbero essere implementazioni che producono lo stesso comportamento osservabile che aveva originariamente.

    naturalmente, ci sono occasioni in cui abbiamo bisogno di introdurre un cambiamento di rottura all’indietro. tuttavia, “refactoring” non è lo strumento appropriato per quel caso. in molti casi, è possibile ottenere il risultato desiderato in diverse fasi: prima di estendere la vostra API con nuovi test (refactoring per rimuovere la duplicazione con l’implementazione esistente), quindi la rimozione dei test che valutano la vecchia API, e infine la rimozione della vecchia API.

  5. questo problema si verifica quando il refactoring cambia le responsabilità delle unità esistenti, specialmente introducendo nuove unità o rimuovendo unità esistenti.

    puoi farlo in stile TDD ma devi:

    1. fare piccoli passi (questo esclude i cambiamenti che estrae entrambe le classi contemporaneamente)
    2. refactor (questo include anche il codice di test refactoring!)

    punto di partenza

    nel tuo caso hai (io uso più astratta sintassi simile a Python per avere meno boilerplate, questo problema è il linguaggio indipendente):

    class SalaryManager:
        def CalculateSalaryAndSendMessage(daysWorked, monthlySalary):
          // code that has two responsibilities calculation and formatting

    avete classe di test per esso. se non si dispone di test è necessario creare questi test prima (qui si può trovare Lavorare in modo efficace con Legacy Code davvero utile) o in molti casi insieme ad alcuni refactoring per essere in grado di rifactorti codice ancora di più (refactoring sta cambiando la struttura del codice senza modificare la sua funzionalità, quindi è necessario avere test per essere sicuri di non modificare la funzionalità).

    class SalaryManagerTest:
        def test_calculation_1():
          // some test for calculation
    
        def test_calculation_2():
          // another test for calculation
    
        def test_formatting_1():
          // some test for formatting
    
        def test_formatting_2():
          // another test for calculation
    
        def test_that_checks_both_formatting_and_calculation():
          // some test for both

    calcolo dell’estrazione di una classe

    ora vediamo cosa estrarre la responsabilità di calcolo di una classe.

    si può fare subito senza cambiare API del SalaryManager. nel classico TDD lo fai in piccoli passi e fai dei test dopo ogni passo, qualcosa di simile:

    1. estrarre il calcolo da una funzione (diciamo calculateSalary) di SalaryManager
    2. creare una classe vuota SalaryCalculator
    3. creare istanza di classe SalaryCalculator in SalaryManager
    4. spostare calculateSalary a SalaryCalculator

    a volte (se SalaryCalculator è semplice e le sue interazioni con SalaryManager sono semplici) si può fermare qui e non cambiare i test a tutti. quindi i test per il calcolo saranno ancora parte di SalaryManager. con l’aumento della complessità di SalaryCalculator sarà difficile / impraticabile per testare via SalaryManager quindi sarà necessario fare il secondo passo – test di rifattore pure.

    test di rifattore

    vorrei fare qualcosa di simile:

    1. dividere SalaryManagerTest in SalaryManagerTest e SalaryCalculatorTest fondamentalmente copiando la classe
    2. rimuovere test_calculation_1 e test_calculation_1 da SalaryManagerTest
    3. lasciare solo test_calculation_1 e test_calculation_1 in SalaryCalculatorTest

    ora i test in SalaryCalculatorTest funzionalità di test per il calcolo, ma farlo tramite SalaryManager. dovete fare due cose:

    1. assicurarsi di avere il test di integrazione che verifica che il calcolo avviene a tutti
    2. cambiare SalaryCalculatorTest in modo che non usi SalaryManager

    test di integrazione

    1. se non si dispone di tale test già (test_that_checks_both_formatting_and_calculation può essere un tale test) creare un test che fa qualche usecase semplice quando il calcolo è coinvolto da SalaryManager
    2. si consiglia di spostare tale test a SalaryManagerIntegrationTest se si desidera

    fare SalaryCalculatorTest utilizzare SalaryCalculator

    i test in SalaryCalculatorTest sono tutti di calcolo, quindi anche se si occupano di manager la loro essenza e parte importante sta fornendo input per il calcolo e quindi controllare il risultato di esso.

    ora il nostro obiettivo è quello di rifattore i test in un modo in modo che sia facile passare manager per la calcolatrice.

    il test per il calcolo potrebbe assomigliare a questo:

    class SalaryCalculatorTest:
    
        def test_short_period_calculation(self):
           manager = new SalaryManager()
           DAYS_WORKED = 1
           result = manager.CalculateSalaryAndSendMessage(DAYS_WORKED, SALARY)
           assertEquals(result.contains('Salary cannot be generated'), True)

    ci sono tre cose qui:

    1. preparazione degli oggetti per le prove
    2. invocazione dell’azione
    3. controllo del risultato

    si noti che tale test verificherà l’esito del calcolo in qualche modo. può essere confuso e fragile, ma lo farà in qualche modo. come ci dovrebbe essere qualche modo esternamente visibile per distinguere come il calcolo finito. altrimenti (se non ha alcun effetto visibile) tale calcolo non ha senso.

    si può rifare in questo modo:

    1. estrarre la creazione del manager a una funzione createCalculator (è bene chiamarlo così come l’oggetto che viene creato dalla prospettiva di prova è la calcolatrice)
    2. rinominare manager – > sut (sistema in esame)
    3. estrarre manager.CalculateSalaryAndSendMessage invocazione in una funzione ‘calcolare (calcolatrice, giorni, stipendio)
    4. estrarre il controllo in una funzione assertPeriodIsTooShort(result)

    ora il test non ha alcun riferimento diretto al manager, riflette l’essenza di ciò che viene testato.

    tale refactoring dovrebbe essere fatto con tutti i test e le funzioni in questa classe di test. non perdere l’occasione di riutilizzare alcuni di loro come createCalculator.

    ora è possibile modificare quale oggetto viene creato in createCalculator e quale oggetto è previsto (e come il controllo è fatto) in assertPeriodIsTooShort. il trucco qui è quello di controllare ancora la dimensione di quel cambiamento. se è troppo grande (cioè non si può fare test verde dopo il cambiamento in pochi minuti in TDD classico) potrebbe essere necessario creare una copia del createCalculator e assert... e usarli in un solo test prima, ma poi gradualmente sostituire vecchio con uno in altri test.

    1. grazie Roman per il vostro sforzo nello spiegare in modo TDD. penso di farlo nello stesso modo in cui avete spiegato. l’unica cosa che sento è test di unità sono troppo fragili in natura e refactoring ha anche bisogno di refactoring test in casi complessi che lo rende più tempo. sto gestendo una squadra in questo momento e diverse persone gestiscono in modo diverso, rendendo la pratica uniforme da seguire.

Leave a Reply

Your email address will not be published. Required fields are marked *