unit-testing
Iniezione di dipendenza
Ricerca…
Osservazioni
Un approccio che può essere preso per scrivere software è quello di creare le dipendenze come sono necessarie. Questo è un modo abbastanza intuitivo di scrivere un programma ed è il modo in cui molte persone tendono ad essere insegnate, in parte perché è facile da seguire. Uno dei problemi con questo approccio è che può essere difficile da testare. Considera un metodo che elabora alcune elaborazioni in base alla data corrente. Il metodo potrebbe contenere codice simile al seguente:
if (DateTime.Now.Date > processDate)
{
// Do some processing
}
Il codice ha una dipendenza diretta dalla data corrente. Questo metodo può essere difficile da testare perché la data corrente non può essere facilmente manipolata. Un approccio per rendere il codice più testabile è rimuovere il riferimento diretto alla data corrente e invece fornire (o iniettare) la data corrente al metodo che esegue l'elaborazione. Questa iniezione di dipendenza può rendere molto più facile testare aspetti del codice utilizzando i duplicati di test per semplificare la fase di configurazione del test dell'unità.
Sistemi IOC
Un altro aspetto da considerare è la durata delle dipendenze; nel caso in cui la classe stessa crei le proprie dipendenze (note anche come invarianti), è quindi responsabile della loro eliminazione. Dipendenza Iniezione inverte questo (ed è per questo che spesso ci riferiamo a una libreria di iniezione come sistema "Inversion of Control") e significa che invece della classe è responsabile della creazione, gestione e pulizia delle sue dipendenze, un agente esterno (in questo caso, il sistema IoC) lo fa invece.
Ciò rende molto più semplice avere dipendenze condivise tra istanze della stessa classe; ad esempio, considera un servizio che recupera i dati da un endpoint HTTP affinché una classe consumi. Dal momento che questo servizio è senza stato (cioè non ha uno stato interno), quindi abbiamo solo bisogno di una singola istanza di questo servizio in tutta la nostra applicazione. Mentre è possibile (ad esempio, utilizzando una classe statica) per eseguire questa operazione manualmente, è molto più semplice creare la classe e dire al sistema IoC che deve essere creata come Singleton , in cui esiste una sola istanza della classe.
Un altro esempio potrebbero essere i contesti di database utilizzati in un'applicazione Web, per cui è necessario un nuovo contesto per richiesta (o thread) e non per istanza di un controller; ciò consente di iniettare il contesto in ogni livello eseguito da quel thread, senza dover essere passato manualmente.
Ciò consente alle classi che consumano di non dover gestire le dipendenze.
Costruttore di iniezione
L'iniezione del costruttore è il modo più sicuro per iniettare dipendenze da cui dipende un'intera classe. Tali dipendenze vengono spesso definite invarianti , poiché non è possibile creare un'istanza della classe senza fornirle. Richiedendo che la dipendenza venga iniettata durante la costruzione, è garantito che non è possibile creare un oggetto in uno stato incoerente.
Considera una classe che deve scrivere su un file di log in condizioni di errore. Ha una dipendenza da un ILogger
, che può essere iniettato e utilizzato quando necessario.
public class RecordProcessor
{
readonly private ILogger _logger;
public RecordProcessor(ILogger logger)
{
_logger = logger;
}
public void DoSomeProcessing() {
// ...
_logger.Log("Complete");
}
}
A volte durante la scrittura di test si può notare che il costruttore richiede più dipendenze di quanto sia effettivamente necessario per un caso da testare. Più questi test hai, più è probabile che la tua classe rompa il Principio di Responsabilità Singola (SRP). Questo è il motivo per cui non è una buona pratica definire il comportamento predefinito per tutti i mock di dipendenze iniettate nella fase di inizializzazione della classe di test in quanto può mascherare il potenziale segnale di allarme.
L'unittest per questo sarebbe simile al seguente:
[Test]
public void RecordProcessor_DependencyInjectionExample()
{
ILogger logger = new FakeLoggerImpl(); //or create a mock by a mocking Framework
var sut = new RecordProcessor(logger); //initialize with fake impl in testcode
Assert.IsTrue(logger.HasCalledExpectedMethod());
}
Proprietà dell'iniezione
L'iniezione di proprietà consente di aggiornare le dipendenze delle classi dopo che è stata creata. Questo può essere utile se vuoi semplificare la creazione dell'oggetto, ma permetti comunque che le dipendenze vengano sovrascritte dai test con raddoppia test.
Considera una classe che deve scrivere su un file di log in una condizione di errore. La classe sa come costruire un Logger
predefinito, ma consente di eseguirne l'override tramite l'iniezione di proprietà. Tuttavia vale la pena notare che usando l'injection di proprietà in questo modo si sta strettamente accoppiando questa classe con un'esecuzione esatta di ILogger
che è ConcreteLogger
in questo esempio. Una soluzione possibile potrebbe essere una fabbrica che restituisce l'implementazione necessaria di ILogger.
public class RecordProcessor
{
public RecordProcessor()
{
Logger = new ConcreteLogger();
}
public ILogger Logger { get; set; }
public void DoSomeProcessing()
{
// ...
_logger.Log("Complete");
}
}
Nella maggior parte dei casi, Iniezione costruttore è preferibile a Iniezione proprietà perché fornisce migliori garanzie sullo stato dell'oggetto immediatamente dopo la sua costruzione.
Metodo di iniezione
L'iniezione del metodo è un modo a grana fine per iniettare dipendenze nell'elaborazione. Considera un metodo che elabora alcune elaborazioni in base alla data corrente. È difficile modificare la data corrente da un test, quindi è molto più semplice passare una data nel metodo che si desidera testare.
public void ProcessRecords(DateTime currentDate)
{
foreach(var record in _records)
{
if (currentDate.Date > record.ProcessDate)
{
// Do some processing
}
}
}
Contenitori / Quadri DI
Mentre estraendo le dipendenze dal codice in modo che possano essere iniettate rende il codice più facile da testare, spinge il problema più in alto nella gerarchia e può anche generare oggetti difficili da costruire. Sono stati scritti vari framework di iniezione delle dipendenze / Inversion of Control Containers per aiutare a superare questo problema. Ciò consente la registrazione dei tipi di mapping. Queste registrazioni vengono quindi utilizzate per risolvere le dipendenze quando viene richiesto al contenitore di costruire un oggetto.
Considera queste classi:
public interface ILogger {
void Log(string message);
}
public class ConcreteLogger : ILogger
{
public ConcreteLogger()
{
// ...
}
public void Log(string message)
{
// ...
}
}
public class SimpleClass
{
public SimpleClass()
{
// ...
}
}
public class SomeProcessor
{
public SomeProcessor(ILogger logger, SimpleClass simpleClass)
{
// ...
}
}
Per costruire SomeProcessor
, sono richieste sia un'istanza di ILogger
che SimpleClass
. Un contenitore come Unity aiuta ad automatizzare questo processo.
Innanzitutto il contenitore deve essere costruito e quindi i mapping vengono registrati con esso. Questo di solito viene fatto solo una volta all'interno di un'applicazione. L'area del sistema in cui si verifica ciò è comunemente nota come Radice di composizione
// Register the container
var container = new UnityContainer();
// Register a type mapping. This allows a `SimpleClass` instance
// to be constructed whenever it is required.
container.RegisterType<SimpleClass, SimpleClass>();
// Register an instance. This will use this instance of `ConcreteLogger`
// Whenever an `ILogger` is required.
container.RegisterInstance<ILogger>(new ConcreteLogger());
Dopo aver configurato il contenitore, può essere utilizzato per creare oggetti, risolvendo automaticamente le dipendenze come richiesto:
var processor = container.Resolve<SomeProcessor>();