AntonioGanci

Il blog di Antonio Ganci
posts - 201, comments - 420, trackbacks - 31

Testare un oggetto senza esporre il suo stato interno

Il post di Luka Micro-esercizio di TDD mi fornisce l'occasione di parlare di un errore di design che vedo in molte codebase e che anche io soprattutto all'inizio commettevo.

Partiamo dalla classe Alarm dell'esempio:

  public class Alarm

  {

    private const double LowPressureTreshold = 17;

    private const double HighPressureTreshold = 21;

 

    Sensor _sensor = new Sensor();

 

    bool _alarmOn = false;

    private long _alarmCount = 0;

 

    public void Check()

    {

      double psiPressureValue = _sensor.PopNextPressurePsiValue();

 

      if (psiPressureValue < LowPressureTreshold || HighPressureTreshold < psiPressureValue)

      {

        _alarmOn = true;

        _alarmCount += 1;

      }

    }

 

    public bool AlarmOn

    {

      get { return _alarmOn; }

    }

  }

Il problema è la property AlarmOn. Questa property ha senso solo per i test; per usare la classe Alarm dovrò fare altri if sul valore della property. Infine la classe Alarm non modella un comportamento, ma solo uno stato. Mi spiego meglio: cosa deve fare la nostra applicazione se scatta un allarme? Non credo che debba settare una variabile boolean in memoria a true. Magari deve visualizzare su schermo un led rosso e scrivere in un log il momento in cui l'allarme è scattato: deve compiere un comportamento.

Come potremmo modificare il design per renderlo più object oriented? Usando gli oggetti ;-)

Per prima cosa estraiamo le due istruzioni all'interno dell'if in un altra classe:

  public class AlarmCounter : IAlarmReport

  {

    private bool _alarmOn;

    private int _alarmCount;

 

    public void Report()

    {

      _alarmOn = true;

      _alarmCount += 1;

    }

  }

 

  public interface IAlarmReport

  {

    void Report();

  }

Modifichiamo la clase Alarm in modo che usi la nuova interfaccia:

  public class Alarm

  {

    private const double LowPressureTreshold = 17;

    private const double HighPressureTreshold = 21;

 

    private readonly IAlarmReport m_alarmReport;

    Sensor _sensor = new Sensor();

 

    public Alarm() : this(new AlarmCounter()) {}

    public Alarm(IAlarmReport alarmReport)

    {

      m_alarmReport = alarmReport;

    }

 

    public void Check()

    {

      double psiPressureValue = _sensor.PopNextPressurePsiValue();

      if (psiPressureValue < LowPressureTreshold || HighPressureTreshold < psiPressureValue)

      {

        m_alarmReport.Report();

      }

    }

  }

Alcune considerazioni importanti: noterete il doppio costruttore; quello senza parametri è stato aggiunto in modo che i chiamanti non si accorgano della modifica, mentre il secondo serve per poter testare la classe Alarm.

Abbiamo reso l'architettura molto più malleabile ed abbiamo anche accontentato il nostro amico OCP. Infatti qualunque cosa debba fare la nostra applicazione quando scatta un allarme per la classe Alarm è indifferente e non verrà modificata.

Lo stesso disaccoppiamento si sarebbe potuto ottenere con il sensore che lascio come esercizio al lettore.

Per finire alcune domande per riflettere su questo codice:

  • Se volessi eseguire più di un'azione quando scatta l'allarme, ma senza modificare la classe Alarm come potrei fare?
  • Quanti test devo scrivere per testare la classe Alarm? Esiste un'altra implementazione che renda necessario scrivere meno test?
  • Come potrei modificare l'implementazione per rendermi indipendente dal tipo di sensore?
  • Se avessi voluto testare la prima implementazione di Alarm senza modificare la classe Alarm come avrei potuto fare?

Print | posted on martedì 8 giugno 2010 16:31 |

Feedback

Gravatar

# re: Testare un oggetto senza esporre il suo stato interno

Come sempre bel post!!

Provo a rispondere alle domande:
- Creo un CompositeAlarmReport che nasconde ad Alarm il fatto che si voglia eseguire più di un'azione.
- Se lascio la classe così come l'hai postata, io farei 2 test, uno che verifica la chiamata al metodo Report nel caso il sensore fornisca un valore da notificare ed uno che testa il contrario.
- Applico lo stesso principio che hai applicato per AlarmReport, ovvero, estraggo un'interfaccia che specifica il contratto/resposonsabilità del sensore, creo le varie implementazioni concrete per il codice di produzione (es PsiSendor) e le implementazioni stub/mock/fake per i tests.
- se per "prima implementazione di Alarm" intendi quella proposta da Luca, mmmhhh...non saprei.

10/06/2010 20:42 | Matteo Baglini
Comments have been closed on this topic.

Powered by:
Powered By Subtext Powered By ASP.NET