unit-testing
Inyección de dependencia
Buscar..
Observaciones
Un enfoque que puede adoptarse para escribir software es crear dependencias a medida que se necesitan. Esta es una forma bastante intuitiva de escribir un programa y es la forma en que la mayoría de las personas tienden a aprender, en parte porque es fácil de seguir. Uno de los problemas con este enfoque es que puede ser difícil de probar. Considere un método que realice algún procesamiento basado en la fecha actual. El método podría contener algún código como el siguiente:
if (DateTime.Now.Date > processDate)
{
// Do some processing
}
El código tiene una dependencia directa de la fecha actual. Este método puede ser difícil de probar porque la fecha actual no se puede manipular fácilmente. Un método para hacer que el código sea más verificable es eliminar la referencia directa a la fecha actual y, en cambio, proporcionar (o inyectar) la fecha actual al método que realiza el procesamiento. Esta inyección de dependencia puede hacer que sea mucho más fácil probar aspectos del código mediante el uso de pruebas dobles para simplificar el paso de configuración de la prueba de la unidad.
Sistemas IOC
Otro aspecto a considerar es la vida de las dependencias; En el caso de que la clase en sí cree sus propias dependencias (también conocidas como invariantes), entonces es responsable de eliminarlas. La inyección de dependencia invierte esto (y es por eso que a menudo nos referimos a una biblioteca de inyección como un sistema de "Inversión de Control") y significa que en lugar de que la clase sea responsable de crear, administrar y limpiar sus dependencias, un agente externo (en este caso, el sistema IoC) lo hace en su lugar.
Esto hace que sea mucho más sencillo tener dependencias que se comparten entre instancias de la misma clase; por ejemplo, considere un servicio que recupera datos de un punto final HTTP para que una clase los consuma. Dado que este servicio no tiene estado (es decir, no tiene ningún estado interno), por lo tanto, solo necesitamos una única instancia de este servicio en toda nuestra aplicación. Si bien es posible (por ejemplo, usar una clase estática) hacerlo manualmente, es mucho más simple crear la clase y decirle al sistema IoC que se creará como un Singleton , por lo que solo existe una instancia de la clase.
Otro ejemplo sería los contextos de base de datos utilizados en una aplicación web, en los que se requiere un nuevo contexto por solicitud (o hilo) y no por instancia de un controlador; esto permite que el contexto sea inyectado en cada capa ejecutada por ese hilo, sin tener que pasarlo manualmente.
Esto libera a las clases consumidoras de tener que gestionar las dependencias.
Inyección Constructor
La inyección de constructor es la forma más segura de inyectar dependencias de las que depende toda una clase. Tales dependencias a menudo se denominan invariantes , ya que una instancia de la clase no se puede crear sin proporcionarlas. Al requerir que la dependencia se inyecte en la construcción, se garantiza que un objeto no se puede crear en un estado incoherente.
Considere una clase que necesita escribir en un archivo de registro en condiciones de error. Depende de un ILogger
, que puede inyectarse y usarse cuando sea necesario.
public class RecordProcessor
{
readonly private ILogger _logger;
public RecordProcessor(ILogger logger)
{
_logger = logger;
}
public void DoSomeProcessing() {
// ...
_logger.Log("Complete");
}
}
A veces, mientras escribe pruebas, puede notar que el constructor requiere más dependencias de las que realmente se necesitan para un caso que se está probando. Mientras más pruebas de este tipo tenga, más probable es que su clase rompa el Principio de Responsabilidad Único (SRP). Por eso no es una buena práctica definir el comportamiento predeterminado para todas las simulaciones de dependencias inyectadas en la fase de inicialización de la clase de prueba, ya que puede enmascarar la posible señal de advertencia.
La prueba de unidad para esto se vería como la siguiente:
[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());
}
Inyección de propiedad
La inyección de propiedad permite que las clases dependientes se actualicen después de que se haya creado. Esto puede ser útil si desea simplificar la creación de objetos, pero aún así permitir que las dependencias sean anuladas por sus pruebas con pruebas dobles.
Considere una clase que necesita escribir en un archivo de registro en una condición de error. La clase sabe cómo construir un Logger
predeterminado, pero permite anularlo mediante la inyección de propiedades. Sin embargo, vale la pena señalar que al usar la inyección de propiedades de esta manera, está ILogger
esta clase con una implementación exacta de ILogger
que es ConcreteLogger
en este ejemplo dado. Una posible solución podría ser una fábrica que devuelva la implementación necesaria de ILogger.
public class RecordProcessor
{
public RecordProcessor()
{
Logger = new ConcreteLogger();
}
public ILogger Logger { get; set; }
public void DoSomeProcessing()
{
// ...
_logger.Log("Complete");
}
}
En la mayoría de los casos, la inyección de constructor es preferible a la inyección de propiedad porque brinda mejores garantías sobre el estado del objeto inmediatamente después de su construcción.
Método de inyección
La inyección de métodos es una forma detallada de inyectar dependencias en el procesamiento. Considere un método que realice algún procesamiento basado en la fecha actual. Es difícil cambiar la fecha actual de una prueba, por lo que es mucho más fácil pasar una fecha al método que desea probar.
public void ProcessRecords(DateTime currentDate)
{
foreach(var record in _records)
{
if (currentDate.Date > record.ProcessDate)
{
// Do some processing
}
}
}
Contenedores / Marcos DI
Si bien extraer las dependencias de su código para poder inyectarlas hace que su código sea más fácil de probar, empuja el problema hacia la jerarquía y también puede generar objetos que son difíciles de construir. Se han escrito varios marcos de inyección de dependencia / contenedores de inversión de control para ayudar a superar este problema. Éstos permiten registrar mapeos de tipo. Estos registros se utilizan para resolver dependencias cuando se solicita al contenedor que construya un objeto.
Considera estas clases:
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)
{
// ...
}
}
Para construir SomeProcessor
, tanto una instancia de ILogger
y SimpleClass
son obligatorios. Un contenedor como Unity ayuda a automatizar este proceso.
Primero se debe construir el contenedor y luego se registran las asignaciones. Esto generalmente se hace solo una vez dentro de una aplicación. El área del sistema donde ocurre esto se conoce comúnmente como la Raíz de composición
// 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());
Una vez que se configura el contenedor, se puede usar para crear objetos, resolviendo automáticamente las dependencias según sea necesario:
var processor = container.Resolve<SomeProcessor>();