Comencé la búsqueda muy larga y ardua para aprender y aplicar TDD a mi flujo de trabajo. Tengo la impresión de que TDD encaja muy bien con los principios de IoC.
Después de examinar algunas de las preguntas etiquetadas con TDD aquí en SO, leí que es una buena idea programar contra interfaces, no objetos.
¿Puede proporcionar ejemplos de código simples de qué es esto y cómo aplicarlo en casos de uso reales? Los ejemplos simples son clave para mí (y otras personas que desean aprender) para comprender los conceptos.
c#
.net
tdd
inversion-of-control
RUMANIA_engineer
fuente
fuente
interface
en Java o C #. De hecho, cuando se escribió el libro del que se tomó esta cita, ni Java ni C # existían.List
dice que después deadd
agregar un elemento a la lista, el elemento está en la lista y la longitud de la lista aumenta en1
. ¿Dónde dice realmente eso en [interface List
] ( Download.Oracle.Com/javase/7/docs/api/java/util/List.html#add )?Respuestas:
Considerar:
class MyClass { //Implementation public void Foo() {} } class SomethingYouWantToTest { public bool MyMethod(MyClass c) { //Code you want to test c.Foo(); } }
Debido a que
MyMethod
solo acepta aMyClass
, si desea reemplazarMyClass
con un objeto simulado para realizar una prueba unitaria, no puede. Mejor es usar una interfaz:interface IMyClass { void Foo(); } class MyClass : IMyClass { //Implementation public void Foo() {} } class SomethingYouWantToTest { public bool MyMethod(IMyClass c) { //Code you want to test c.Foo(); } }
Ahora puede probar
MyMethod
, porque usa solo una interfaz, no una implementación concreta en particular. Luego, puede implementar esa interfaz para crear cualquier tipo de simulación que desee con fines de prueba. Incluso hay bibliotecas como Rhino Mocks 'Rhino.Mocks.MockRepository.StrictMock<T>()
, que toman cualquier interfaz y crean un objeto simulado sobre la marcha.fuente
interface
palabras clave en todo el código no significa que esté programando contra interfaces . Experimento mental: tome un horrible código incohesivo estrechamente acoplado. Para cada clase, simplemente cópielo y péguelo, elimine todos los cuerpos del método, reemplace laclass
palabra clave coninterface
y actualice todas las referencias en el código a ese tipo. ¿El código ahora es mejor?Todo es cuestión de intimidad. Si codifica a una implementación (un objeto realizado), está en una relación bastante íntima con ese "otro" código, como consumidor de él. Significa que tienes que saber cómo construirlo (es decir, qué dependencias tiene, posiblemente como parámetros de constructor, posiblemente como establecedores), cuándo deshacerte de él, y probablemente no puedas hacer mucho sin él.
Una interfaz frente al objeto realizado le permite hacer algunas cosas:
** ACTUALIZACIÓN ** Se solicitó un ejemplo de contenedor IOC (fábrica). Hay muchos disponibles para casi todas las plataformas, pero en esencia funcionan así:
Inicializa el contenedor en la rutina de inicio de sus aplicaciones. Algunos marcos hacen esto a través de archivos de configuración o código o ambos.
Usted "registra" las implementaciones que desea que el contenedor cree para usted como una fábrica para las interfaces que implementan (por ejemplo: registrar MyServiceImpl para la interfaz de servicio). Durante este proceso de registro, normalmente hay alguna política de comportamiento que puede proporcionar, como si se crea una nueva instancia cada vez o si se usa una sola (tonelada) instancia
Cuando el contenedor crea objetos para usted, inyecta cualquier dependencia en esos objetos como parte del proceso de creación (es decir, si su objeto depende de otra interfaz, a su vez se proporciona una implementación de esa interfaz y así sucesivamente).
Pseudo-codishly podría verse así:
IocContainer container = new IocContainer(); //Register my impl for the Service Interface, with a Singleton policy container.RegisterType(Service, ServiceImpl, LifecyclePolicy.SINGLETON); //Use the container as a factory Service myService = container.Resolve<Service>(); //Blissfully unaware of the implementation, call the service method. myService.DoGoodWork();
fuente
Al programar contra una interfaz, escribirá código que use una instancia de una interfaz, no un tipo concreto. Por ejemplo, puede usar el siguiente patrón, que incorpora inyección de constructor. La inyección del constructor y otras partes de la inversión de control no son necesarias para poder programar contra interfaces, sin embargo, dado que viene desde la perspectiva de TDD e IoC, lo he conectado de esta manera para darle un contexto que espero familiar con.
public class PersonService { private readonly IPersonRepository repository; public PersonService(IPersonRepository repository) { this.repository = repository; } public IList<Person> PeopleOverEighteen { get { return (from e in repository.Entities where e.Age > 18 select e).ToList(); } } }
El objeto del repositorio se pasa y es un tipo de interfaz. El beneficio de pasar una interfaz es la capacidad de "intercambiar" la implementación concreta sin cambiar el uso.
Por ejemplo, se podría suponer que en tiempo de ejecución, el contenedor de IoC inyectará un repositorio que está conectado a la base de datos. Durante el tiempo de prueba, puede pasar un repositorio simulado o de código auxiliar para ejercitar su
PeopleOverEighteen
método.fuente
Significa pensar en genérico. No específico.
Suponga que tiene una aplicación que notifica al usuario enviándole algún mensaje. Si trabaja usando una interfaz IMessage por ejemplo
interface IMessage { public void Send(); }
puede personalizar, por usuario, la forma en que reciben el mensaje. Por ejemplo, alguien quiere ser notificado con un correo electrónico y entonces su IoC creará una clase concreta EmailMessage. Otro quiere SMS y usted crea una instancia de SMSMessage.
En todos estos casos, el código de notificación al usuario nunca se modificará. Incluso si agrega otra clase concreta.
fuente
La gran ventaja de programar contra interfaces al realizar pruebas unitarias es que le permite aislar un fragmento de código de cualquier dependencia que desee probar por separado o simular durante la prueba.
Un ejemplo que he mencionado aquí antes en alguna parte es el uso de una interfaz para acceder a los valores de configuración. En lugar de mirar directamente a ConfigurationManager, puede proporcionar una o más interfaces que le permitan acceder a los valores de configuración. Normalmente, proporcionaría una implementación que lea desde el archivo de configuración, pero para las pruebas puede usar una que solo devuelva valores de prueba o arroje excepciones o lo que sea.
Considere también su capa de acceso a datos. Tener su lógica de negocio estrechamente acoplada a una implementación de acceso a datos en particular dificulta la prueba sin tener a mano una base de datos completa con los datos que necesita. Si su acceso a los datos está oculto detrás de las interfaces, puede proporcionar solo los datos que necesita para la prueba.
El uso de interfaces aumenta el "área de superficie" disponible para las pruebas, lo que permite realizar pruebas más detalladas que realmente prueban unidades individuales de su código.
fuente
Pruebe su código como alguien que lo usaría después de leer la documentación. No pruebe nada basándose en los conocimientos que tiene porque ha escrito o leído el código. Desea asegurarse de que su código se comporte como se espera.
En el mejor de los casos, debería poder usar sus pruebas como ejemplos, las pruebas de documentación en Python son un buen ejemplo de esto.
Si sigue estas pautas, cambiar la implementación no debería ser un problema.
También en mi experiencia es una buena práctica probar cada "capa" de su aplicación. Tendrás unidades atómicas, que en sí mismas no tienen dependencias y tendrás unidades que dependerán de otras unidades hasta que finalmente llegues a la aplicación que en sí misma es una unidad.
Debe probar cada capa, no confíe en el hecho de que al probar la unidad A, también prueba la unidad B de qué unidad A depende (la regla se aplica también a la herencia). Esto también debe tratarse como un detalle de implementación, incluso aunque puede sentir como si se estuviera repitiendo.
Tenga en cuenta que es poco probable que cambien las pruebas una vez escritas, mientras que el código que prueban cambiará casi definitivamente.
En la práctica, también existe el problema de IO y el mundo exterior, por lo que desea utilizar interfaces para poder crear simulaciones si es necesario.
En lenguajes más dinámicos, esto no es un gran problema, aquí puede usar la escritura pato, herencia múltiple y mixins para componer casos de prueba. Si empieza a no gustarle la herencia en general, probablemente lo esté haciendo bien.
fuente
Este screencast explica el desarrollo ágil y TDD en la práctica para c #.
Codificar contra una interfaz significa que en su prueba, puede usar un objeto simulado en lugar del objeto real. Al usar un buen marco simulado, puede hacer en su objeto simulado lo que quiera.
fuente