Panoramica sul pattern singleton

Un singleton è una classe per la quale viene garantita l’esistenza di un’unica istanza. E’ utile ad esempio per rappresentare configurazioni globali o in generale strutture dati che si vuole garantire che restino “allineate” in tutta l’applicazione, oppure per strumenti come logger, formatter, ecc..

Tipicamente, un singleton viene implementato con una classe che contiene un campo privato dello stesso tipo della classe, un costruttore privato e un metodo statico che ritorna quel campo, istanziandolo se non lo è ancora:

public class A
{
    private static A instance;

    private A()...

    public static A GetInstance() => instance ?? (instance = new A);
}

L’esempio sopra è in C# per sfruttarne la sintassi più concisa rispetto a linguaggi come Java. In ogni caso, il punto è che se instance è diverso da null ritorno quell’istanza, altrimenti ne creo una nuova, la assegno al campo privato e la ritorno.

Sempre in C#, se si preferisce si può sostituire il metodo con una property statica (senza neanche dover dichiarare esplicitamente il campo privato):

public static A Instance { get; } = new A();

O, se si preferisce un’istanziazione “lazy”:

private static A instance;

public static A Instance { get => instance ?? (instance = new A(); }

Lo svantaggio della property è che non si possono passare eventuali parametri da usare nel costruttore.

Singleton e test automatici

Una caratteristica dei test automatici ben fatti è che devono essere riproducibili e indipendenti, ovvero devono dare lo stesso risultato indipendentemente da quante volte e in che ordine vengano eseguite, e dal fatto che vengano eseguiti sequenzialmente o in parallelo. Ciò implica che i test non condividano informazioni di stato (mutabili) tra di loro.

L’utilizzo di un singleton all’interno dei test o dei SUT (System Under Test) possono violare il vincolo enunciato sopra, qualora l’istanza contenga uno stato che viene modificato da qualche parte.

Le soluzioni consistono nel dare la possibilità di “resettare” in singleton o nell’usare tecniche alternative per garantire l’uso di una singola istanza. In tutti i casi tranne il primo, è richiesto che si possa iniettare l’istanza nel SUT (quindi non bisogna chiamare GetInstance all’interno del SUT):

  • Esistono mocking framework che permettono di creare mock di metodi statici (e.g. GitHub – powermock/powermock: PowerMock is a Java framework that allows you to unit test code normally regarded as untestable. per java). Se si usa uno di questi, si può fare il mock di GetInstance in modo che ritorni sempre una nuova istanza.
  • In alternativa, si può trasformare il costruttore da privato a protected, e nei test definire una classe derivata dal singleton, in cui si aggiunge un costruttore pubblico che a sua volta chiama quello protected. Così facendo, ogni test può creare una nuova istanza. Si noti che il SUT non è a conoscenza del costruttore pubblico perché non sa della classe derivata.
  • Infine, si può evitare del tutto di implementare il pattern singleton, e affidarsi ad altre tecniche per garantire l’uso di una singola istanza in produzione. In particolare, gran parte dei container per la dependency injection prevedono API come RegisterSingleton, AsSingleInstance, ecc. per iniettare sempre la stessa istanza. In questi casi, i test (a meno che non siano end-to-end) tipicamente non usano lo stesso container, per cui si possono invece iniettare istanze diverse.

Conclusioni

Il pattern singleton permette di garantire l’uso di una singola istanza di una classe all’interno di un intero processo. E’ particolarmente utile quando è necessario (o conveniente, per questioni di prestazioni), avere un’unica copia di uno stato. Ciò tuttavia può impedire di avere test ripetibili e indipendenti, specie se lo stato condiviso è mutabile.

Per ovviare al problema sono state illustrate tre tecniche che permettono di avere istanze diverse per ciascun test, senza compromettere l’univocità dell’istanza in produzione.

Per approfondimenti o consulenze in ambito testing e quality assurance, scrivere a info@dvisentin.com.