Test Comando della classe eseguendo i comandi secondari

1

Sto usando Command Pattern per eseguire azioni e così ho sviluppato molte classi di comando ( CommandA , CommandB , ecc.). Ora ho bisogno di un SuperCommand che riceva una raccolta di dati dal codice precedente , lo attraversi e chiami uno dei precedenti comandi definiti in base al campo dati type .

Lo pseudo codice di SuperCommand dovrebbe essere qualcosa del tipo:

class SuperCommand: public ICommand {
    public:
        SuperCommand(LegacyDataList datas): datas_(datas) {}

        void execute() {
            foreach(LegacyData data : datas_) {
                switch(data.type) {
                    case A:
                        CommandA(data).execute();
                    case B:
                        CommandB(data).execute();
                    default:
                        CommandC(data).execute();
                }
            }
        }
    private:
        LegacyDataList datas_;
}

La domanda è:

Sto imparando il TDD e vorrei codificare execute methd della classe SuperCommand a partire dai test. Ma non riesco a capire come testare la logica foreach-switch per verificare che venga eseguito il comando corretto.

L'unica idea che ho avuto è di iniettare comandi in SuperCommand constructor in modo da potermi prendere in giro, ma sembra una soluzione scadente dato che il significato della classe sta scegliendo tra CommandA , CommandB e CommandC (NON verranno mai passate altre implementazioni a questa classe).

Ci sono soluzioni migliori? Devo cambiare il design di SuperCommand ?

    
posta Marco Stramezzi 22.06.2017 - 11:07
fonte

4 risposte

0

La responsabilità della classe SuperCommand è di eseguire comandi basati sui dati forniti.

Il metodo

ICommand :: execute non restituisce il valore, quindi non puoi farci valere contro di esso, quindi devi verificare che alcuni stati siano stati aggiornati correttamente. Ma dal momento che usi comandi diversi e SuperCommand non ha bisogno di preoccuparsi di quale stato è stato aggiornato - puoi semplicemente controllare che l'implementazione corretta di ICommand sia eseguita.

Quindi dovrai affrontare prima un problema con i test di scrittura, perché vuoi / fai creare istanze di comandi effettivi all'interno del metodo SuperCommand::execute() . Ciò significa che non puoi prendere in giro il loro comportamento - è necessario testare il loro codice reale. Il che significa che devi testare il codice a cui SuperCommand non è responsabile.

Poiché il comando è stato implementato in modo che i dati richiesti siano passati attraverso il costruttore, ma non è possibile utilizzare il costruttore a causa dei test. Quindi è necessario introdurre un ulteriore livello di astrazione che sarà responsabile della creazione dell'istanza corretta del comando - CommandFactory

Quindi ora - sembra che SuperCommand responsabilità diventi il loop di tutti i dati, usa factory per il comando di creazione ed eseguilo.

    
risposta data 12.07.2017 - 12:30
fonte
1

Considera di estendere ICommand per includere un metodo canHandle che accetta come argomento un'istanza di LegacyData .

Quindi nella tua classe SuperCommand , mantieni un elenco di ICommand di istanze da chiamare. Per ogni LegacyData che trovi, chiama canHandle che passa l'istanza corrente di LegacyData e se restituisce true, lo chiami e lo interrompi.

Puoi quindi testarlo semplicemente istanziando la tua lista ICommand con stub contenenti solo ICommand s che non fanno nulla e ICommand s che non dovrebbero essere eseguiti e lanciare un'eccezione se lo sono.

Facendolo in questo modo disaccoppia anche SuperCommand dai comandi che esegue in un pizzico. Potresti semplicemente aggiungere un nuovo metodo addCommand che consente al chiamante di decidere quali comandi sono attribuiti a SuperCommand . Se puoi, puoi creare una versione estesa di SuperCommand che già aggiunge specifiche istanze di ICommand che stai utilizzando ( CommandA , CommandB e CommandC nell'esempio). Sì, anche se è vero che stai usando solo questi tre comandi, è anche vero che SuperCommand in realtà non cura di essere CommandA , CommandB e CommandC in esecuzione e così facendo in questo modo, rendi la classe flessibile e facilmente testata.

    
risposta data 22.06.2017 - 11:28
fonte
1

Per prima cosa, lasciami dire che davvero non vedo un motivo per cui SuperCommand dovrebbe implementare l'interfaccia ICommand. A meno che non faccia parte di un quadro più ampio che non è menzionato in questa domanda, è assolutamente inutile. SuperCommand qui serve puramente come un wrapper attorno a diverse classi di comando eseguite.

Successivamente, ciò che @Neil ha descritto è molto, molto simile alla Catena di responsabilità modello di progettazione. Ciò significa che dovresti implementare CommandA, CommandB, CommandC in modo tale che i loro metodi Execute vengano eseguiti solo per i tipi di dati corretti che vengono inviati a loro. In questo modo, puoi concatenare i comandi e lasciare che si preoccupino se qualcosa debba essere eseguito o meno. SuperCommand sarebbe solo un wrapper e un host per le tue classi di comando. Qualcosa del genere:

public class LegacyData
{
    public string Type;
}

public abstract class Command
{
    protected abstract bool DataCheck(LegacyData data);
    public abstract void Execute(LegacyData data);       
    public Command Next { protected get; set; }

}

class CommandA : Command
{
    protected override bool DataCheck(LegacyData data)
    {
        return data.Type == "A";
    }
    public override void Execute(LegacyData data)
    {
        if (DataCheck(data))
        {
            //Do something with data

            //This return statement is here only because the conditions in this example
            //are mutually exclusive. In case there is data that can be processed by multiple
            //commands, the return statement would not be put here.
            return;
        }

        if (Next != null)
        {
            Next.Execute(data);
        }
    }
}

class CommandB : Command
{
    protected override bool DataCheck(LegacyData data)
    {
        return data.Type == "B";
    }
    public override void Execute(LegacyData data)
    {
        if (DataCheck(data))
        {
            //Do something with data

            //This return statement is here only because the conditions in this example
            //are mutually exclusive. In case there is data that can be processed by multiple
            //commands, the return statement would not be put here.
            return;
        }

        if (Next != null)
        {
            Next.Execute(data);
        }
    }
}

class CommandC : Command
{
    protected override bool DataCheck(LegacyData data)
    {
        return data.Type != "A" && data.Type != "B";
    }
    public override void Execute(LegacyData data)
    {
        if (DataCheck(data))
        {
            //Do something with data

            //This return statement is here only because the conditions in this example
            //are mutually exclusive. In case there is data that can be processed by multiple
            //commands, the return statement would not be put here.
            return;
        }

        if (Next != null)
        {
            Next.Execute(data);
        }
    }
}

class SuperCommand
{
    private List<LegacyData> datas;
    public SuperCommand(List<LegacyData> datas){ this.datas = datas; }

    private Command initialCommand;

    public SuperCommand()
    {
        CommandA commandA = new CommandA();
        CommandB commandB = new CommandB();
        CommandC commandC = new CommandC();

        commandA.Next = commandB;
        commandB.Next = commandC;

        initialCommand = commandA;
    }

    public void Execute()
    {
        foreach (LegacyData data in datas)
        {
            initialCommand.Execute(data);
        }
    }       
}

Con questo approccio, avresti completamente disaccoppiato SuperCommand dalla logica nelle classi di comando. L'unica cosa che lega SuperCommand con le classi di comando concrete sarebbe la loro istanziazione e il collegamento nel costruttore. La logica su quale comando verrà eseguito verrebbe ricollocata nelle classi Command, che è possibile testare facilmente.

    
risposta data 22.06.2017 - 12:17
fonte
0

I comandi rispondono alle azioni dell'utente (una persona o un altro sistema). Non dovrebbero essere a conoscenza né eseguire altri comandi. Il modello di strategia è più adatto al tuo scenario. CommandA , CommandB e CommandC diventano strategie e puoi implementarle in questo modo:

interface IStrategy
{
    void Execute(LegacyData data);
    bool AppliesTo(LegacyData data);
}

class StrategyA
{
    public void Execute(LegacyData data)
    {
        // ...
    }

    public bool AppliesTo(LegacyData data)
    {
        return data.type == A;
    }
}

In questo modo puoi testare unitamente il comportamento delle tue strategie e il processo di selezione.

    
risposta data 22.06.2017 - 13:50
fonte

Leggi altre domande sui tag