¿Vale la pena escribir pruebas unitarias para códigos de investigación científica?


89

Estoy firmemente convencido del valor de usar pruebas que verifiquen un programa completo (por ejemplo, pruebas de convergencia), incluido un conjunto automatizado de pruebas de regresión . Después de leer algunos libros de programación, tengo la sensación de que "debo" escribir pruebas unitarias (es decir, pruebas que verifican la corrección de una sola función y no equivalen a ejecutar todo el código para resolver un problema) . Sin embargo, las pruebas unitarias no siempre coinciden con los códigos científicos y terminan sintiéndose artificiales o como una pérdida de tiempo.

¿Deberíamos escribir pruebas unitarias para códigos de investigación?


2
Esta es una pregunta un poco abierta, ¿no?
qubyte

2
Como con todas las "reglas", siempre se aplica una dosis de pensamiento crítico. Pregúntese si cierta rutina tiene una forma obvia de someterse a pruebas unitarias. Si no, entonces una prueba unitaria no tiene sentido en ese punto o el diseño del código era deficiente. Idealmente, una rutina realiza una tarea tan independiente de otras rutinas como sea posible, pero eso tiene que ser intercambiado ocasionalmente.
Lagerbaer

Hay una buena discusión en una línea similar sobre una pregunta sobre stackoverflow .
naught101

Respuestas:


85

Durante muchos años tuve la idea errónea de que no tenía tiempo suficiente para escribir pruebas unitarias para mi código. Cuando escribí las pruebas, estaban hinchadas, cosas pesadas que solo me animaron a pensar que solo debería escribir pruebas unitarias cuando sabía que eran necesarias.

Luego comencé a usar Test Driven Development y descubrí que era una revelación completa. Ahora estoy firmemente convencido de que no tengo tiempo para no escribir pruebas unitarias .

En mi experiencia, al desarrollar teniendo en cuenta las pruebas, termina con interfaces más limpias, clases y módulos más enfocados y, en general , un código más SÓLIDO y comprobable.

Cada vez que trabajo con código heredado que no tiene pruebas unitarias y tengo que probar algo manualmente, sigo pensando "esto sería mucho más rápido si este código ya tuviera pruebas unitarias". Cada vez que tengo que probar y agregar funcionalidad de prueba unitaria al código con alto acoplamiento, sigo pensando "esto sería mucho más fácil si se hubiera escrito de forma desacoplada".

Comparando y contrastando las dos estaciones experimentales que apoyo. Uno ha existido por un tiempo y tiene una gran cantidad de código heredado, mientras que el otro es relativamente nuevo.

Cuando se agrega funcionalidad al antiguo laboratorio, a menudo se trata de ir al laboratorio y pasar muchas horas trabajando sobre las implicaciones de la funcionalidad que necesitan y cómo puedo agregar esa funcionalidad sin afectar ninguna de las otras funciones. El código simplemente no está configurado para permitir pruebas fuera de línea, por lo que casi todo tiene que ser desarrollado en línea. Si intentara desarrollar fuera de línea, terminaría con más objetos simulados de lo que sería razonable.

En el laboratorio más nuevo, generalmente puedo agregar funcionalidad desarrollándola fuera de línea en mi escritorio, burlándome solo de las cosas que se requieren de inmediato y luego solo pasando un corto tiempo en el laboratorio, solucionando los problemas restantes que no se solucionaron -línea.

Para mayor claridad, y desde @ naught101 preguntó ...

Tiendo a trabajar en el control experimental y el software de adquisición de datos, con algunos análisis de datos ad hoc, por lo que la combinación de TDD con control de revisión ayuda a documentar tanto los cambios en el hardware del experimento subyacente como los cambios en los requisitos de recopilación de datos a lo largo del tiempo.

Sin embargo, incluso en la situación de desarrollar código exploratorio, podría ver un beneficio significativo de tener supuestos codificados, junto con la capacidad de ver cómo evolucionan esos supuestos con el tiempo.


77
Mark, ¿de qué tipo de código estás hablando aquí? Modelo reutilizable? Encuentro que esta razón no es realmente tan aplicable a cosas como el código de análisis de datos exploratorio, donde realmente necesitas saltar mucho, y a menudo nunca esperas volver a usar el código en otro lugar.
naught101

35

Los códigos científicos tienden a tener constelaciones de funciones de enclavamiento con más frecuencia que los códigos comerciales en los que he trabajado, generalmente debido a la estructura matemática del problema. Por lo tanto, no creo que las pruebas unitarias para funciones individuales sean muy efectivas. Sin embargo, creo que hay una clase de pruebas unitarias que son efectivas y aún son bastante diferentes de las pruebas de todo el programa en que apuntan a una funcionalidad específica.

Solo defino brevemente lo que quiero decir con este tipo de pruebas. Las pruebas de regresión buscan cambios en el comportamiento existente (validado de alguna manera) cuando se realizan cambios en el código. Las pruebas unitarias ejecutan un fragmento de código y comprueban que proporciona un resultado deseado basado en una especificación. No son tan diferentes, ya que la prueba de regresión original era una prueba unitaria ya que tuve que determinar que la salida era válida.

Mi ejemplo favorito de una prueba de unidad numérica es probar la tasa de convergencia de una implementación de elementos finitos. Definitivamente no es simple, pero toma una solución conocida para un PDE, ejecuta varios problemas al disminuir el tamaño de malla , y luego ajusta la norma de error a la curva C h r donde r es la tasa de convergencia. Hago esto para el problema de Poisson en PETSc usando Python. Yo no busco a una diferencia, como en la regresión, pero sobre todo una tarifa r especificado para el elemento dado.hChrrr

Dos ejemplos más de pruebas unitarias, provenientes de PyLith , son la ubicación de puntos, que es una función única para la que es fácil producir resultados sintéticos y la creación de celdas cohesivas de volumen cero en una malla, que involucra varias funciones pero aborda una pieza circunscrita de funcionalidad en el código.

Hay muchas pruebas de este tipo, incluidas las pruebas de conservación y consistencia. La operación no es tan diferente de la regresión (ejecuta una prueba y compara el resultado con un estándar), pero el resultado estándar proviene de una especificación en lugar de una ejecución anterior.


44
Wikipedia dice "Las pruebas unitarias, también conocidas como pruebas de componentes, se refieren a pruebas que verifican la funcionalidad de una sección específica de código, generalmente a nivel de función". Las pruebas de convergencia en un código de elementos finitos claramente no pueden ser pruebas unitarias ya que involucran muchas funciones.
David Ketcheson el

Es por eso que dejé en claro en la parte superior de la publicación que tengo una visión amplia de las pruebas unitarias, y "generalmente" significa exactamente eso.
Matt Knepley

Mi pregunta fue hecha en el sentido de la definición más ampliamente aceptada de pruebas unitarias. Ahora he hecho esto completamente explícito en la pregunta.
David Ketcheson el

He aclarado mi respuesta
Matt Knepley

Sus últimos ejemplos son relevantes para lo que pretendía.
David Ketcheson

28

Desde que leí sobre el desarrollo basado en pruebas en Code Complete, segunda edición , he usado un marco de pruebas unitariascomo parte de mi estrategia de desarrollo, y ha aumentado drásticamente mi productividad al reducir la cantidad de tiempo que pasé depurando porque las diversas pruebas que escribo son de diagnóstico. Como beneficio adicional, tengo mucha más confianza en mis resultados científicos y he utilizado mis pruebas unitarias en varias ocasiones para defender mis resultados. Si hay un error en una prueba unitaria, generalmente puedo entender por qué con bastante rapidez. Si mi aplicación falla y todas las pruebas de mi unidad pasan, hago un análisis de cobertura de código para ver qué partes de mi código no se ejercitan, y paso por el código con un depurador para identificar la fuente del error. Luego escribo una nueva prueba para asegurarme de que el error permanezca solucionado.

Muchas de las pruebas que escribo no son pruebas unitarias puras. Estrictamente definido, se supone que las pruebas unitarias ejercen la funcionalidad de una función. Cuando puedo probar fácilmente una sola función utilizando datos simulados, lo hago. Otras veces, no puedo burlarme fácilmente de los datos que necesito para escribir una prueba que ejercite la funcionalidad de una función determinada, por lo que probaré esa función junto con otras en una prueba de integración. Pruebas de integraciónprueba el comportamiento de múltiples funciones a la vez. Como Matt señala, los códigos científicos son a menudo una constelación de funciones de enclavamiento, pero a menudo, ciertas funciones se llaman en secuencia y se pueden escribir pruebas unitarias para probar la salida en pasos intermedios. Por ejemplo, si mi código de producción llama a cinco funciones en secuencia, escribiré cinco pruebas. La primera prueba solo llamará a la primera función (por lo que es una prueba unitaria). Luego, la segunda prueba llamará a las funciones primera y segunda, la tercera prueba llamará a las tres primeras funciones, y así sucesivamente. Incluso si pudiera escribir pruebas unitarias para cada función en mi código, escribiría pruebas de integración de todos modos, porque pueden surgir errores cuando se combinan varias piezas modulares de un programa. Finalmente, después de escribir todas las pruebas unitarias y las pruebas de integración que creo que necesito, ' Envolveré mis estudios de caso en pruebas unitarias y las usaré para pruebas de regresión, porque quiero que mis resultados sean repetibles. Si no son repetibles y obtengo resultados diferentes, quiero saber por qué. El fracaso de una prueba de regresión puede no ser un problema real, pero me obligará a determinar si los nuevos resultados son al menos tan confiables como los resultados anteriores.

También vale la pena junto con las pruebas unitarias el análisis de código estático, los depuradores de memoria y la compilación con indicadores de advertencia del compilador para detectar errores simples y código no utilizado.



¿Considerarías suficientes pruebas de integración o crees que también necesitas escribir pruebas unitarias separadas?
siamii

Escribiría pruebas unitarias separadas siempre que sea posible y factible hacerlo. Facilita la depuración y aplica código desacoplado (que es lo que desea).
Geoff Oxberry

19

En mi experiencia, a medida que aumenta la complejidad de los códigos de investigación científica, es necesario tener un enfoque muy modular en la programación. Esto puede ser doloroso para los códigos con una base grande y antigua ( f77¿alguien?) Pero es necesario seguir adelante. A medida que se construye un módulo en torno a un aspecto específico del código (para aplicaciones CFD, piense en condiciones de contorno o termodinámica), las pruebas unitarias son muy valiosas para validar la nueva implementación y aislar problemas y desarrollos de software adicionales.

Estas pruebas unitarias deben estar un nivel por debajo de la verificación del código (¿puedo recuperar la solución analítica de mi ecuación de onda?) Y 2 niveles por debajo de la validación del código (¿puedo predecir los valores pico de RMS correctos en mi flujo de tubería turbulenta), simplemente asegurando que la programación (¿se pasan correctamente los argumentos, los punteros apuntan a lo correcto?) y las "matemáticas" (esta subrutina calcula el coeficiente de fricción. Si ingreso un conjunto de números y calculo la solución a mano, ¿la rutina produce lo mismo? resultado?) son correctos. Básicamente va un nivel por encima de lo que los compiladores pueden detectar, es decir, errores de sintaxis básicos.

Definitivamente lo recomendaría para al menos algunos módulos cruciales en su aplicación. Sin embargo, uno tiene que darse cuenta de que es extremadamente tedioso y consume mucho tiempo, por lo que a menos que tenga mano de obra ilimitada, no lo recomendaría para el 100% de un código complejo.


¿Tiene algún ejemplo o criterio específico para elegir qué piezas someter a prueba (y cuáles no)?
David Ketcheson, el

@DavidKetcheson Mi experiencia está limitada por la aplicación y el idioma que utilizamos. Entonces, para nuestro código CFD de propósito general con aproximadamente 200k líneas de F90 en su mayoría, hemos estado tratando durante el último año o dos para aislar realmente algunas funcionalidades del código. Hacer un módulo y usarlo en todo el código no es lograr esto, por lo que uno tiene que comparar verdaderamente estos módulos y prácticamente hacerlos bibliotecas. Entonces, solo unas pocas declaraciones USE y todas las conexiones con el resto del código se realizan a través de llamadas de rutina. Rutinas que puede probar por unidad, por supuesto, así como el resto de la biblioteca.
FrenchKheldar

@DavidKetcheson Como dije en mi respuesta, las condiciones de contorno y la termodinámica eran dos aspectos de nuestro código que logramos aislar realmente y, por lo tanto, probar estos elementos tenía sentido. De manera más general, comenzaría con algo pequeño y trataría de hacerlo limpiamente. Idealmente, este es un trabajo de 2 personas. Una persona escribe las rutinas y la documentación que describe la interfaz, otra debe escribir la prueba de la unidad, idealmente sin mirar el código fuente y seguir solo la descripción de la interfaz. De esa forma se prueba la intención de la rutina, pero me doy cuenta de que no es fácil organizarlo.
FrenchKheldar

1
¿Por qué no incluir los otros tipos de pruebas de software (integración, sistema) además de las pruebas unitarias? Además del tiempo y el costo, ¿no sería esta la solución técnica más completa? Mis referencias son 1 (Sec. 3.4.2) y 2 (página 5). En otras palabras, ¿no deberían probar el Código Fuente los niveles de prueba de software tradicionales 3 ("Niveles de prueba")?
ximiki

14

Las pruebas unitarias para códigos científicos son útiles por una variedad de razones.

Tres en particular son:

  • Las pruebas unitarias ayudan a otras personas a comprender las limitaciones de su código. Básicamente, las pruebas unitarias son una forma de documentación.

  • Las pruebas unitarias verifican para asegurarse de que una sola unidad de código está devolviendo resultados correctos, y verifican que el comportamiento de un programa no cambie cuando se modifican los detalles.

  • El uso de pruebas unitarias facilita la modularización de sus códigos de investigación. Esto puede ser particularmente importante si comienza a tratar de orientar su código en una nueva plataforma, por ejemplo, si está interesado en ponerlo en paralelo o ejecutarlo en una máquina GPGPU.

Sobre todo, las pruebas unitarias le dan la confianza de que los resultados de la investigación que está produciendo con sus códigos son válidos y verificables.

Observo que mencionas las pruebas de regresión en tu pregunta. En muchos casos, las pruebas de regresión se logran mediante la ejecución automatizada y regular de pruebas unitarias y / o pruebas de integración (que prueban que las piezas de código funcionan correctamente cuando se combinan; en la computación científica, esto a menudo se hace comparando la salida con los datos experimentales o resultados de programas anteriores de confianza). Parece que ya está utilizando pruebas de integración o pruebas unitarias a nivel de componentes complejos grandes con éxito.

Lo que diría es que a medida que los códigos de investigación se vuelven cada vez más complejos y dependen del código y las bibliotecas de otras personas, es importante comprender dónde ocurre el error cuando lo hace. Las pruebas unitarias permiten detectar el error con mucha más facilidad.

Puede encontrar útiles la descripción, la evidencia y las referencias en la Sección 7 "Plan para los errores" del documento que escribí en conjunto sobre las mejores prácticas para la computación científica . También introduce el concepto complementario de programación defensiva.


9

En mis clases deal.II enseño que el software que no tiene pruebas de que no funciona correctamente (y pasar a la tensión que a propósito dije " no , no funciona correctamente", no " puede no funcionar correctamente).

Por supuesto, vivo según el mantra, que es cómo deal.II ha venido a ejecutar 2.500 pruebas con cada confirmación ;-)

Más en serio, creo que Matt ya define bien las dos clases de pruebas. Escribimos pruebas unitarias para las cosas de nivel inferior y progresa naturalmente a pruebas de regresión para las cosas de nivel superior. No creo que pueda dibujar un límite claro que separe nuestras pruebas a un lado u otro, ciertamente hay muchos que pisan la línea donde alguien ha mirado el resultado y lo ha encontrado en gran medida razonable (¿prueba unitaria?) sin haberlo mirado hasta el último bit de precisión (¿prueba de regresión?).


¿Por qué propone esta jerarquía (unidad para menor, regresión para mayor) versus los niveles tradicionales en las pruebas de software?
ximiki

@ximiki: no es mi intención. Estoy diciendo que las pruebas existen en un espectro que incluiría todas las categorías enumeradas en su enlace.
Wolfgang Bangerth

8

Si y no. Ciertamente, realice pruebas unitarias de las rutinas fundamentales del conjunto de herramientas básicas que utiliza para facilitarle la vida, como rutinas de conversión, mapeos de cadenas, física básica y matemáticas, etc. Cuando se trata de clases o funciones de cálculo, generalmente pueden requerir tiempos de ejecución largos en realidad puede preferir probarlos como pruebas funcionales, en lugar de como unidades. Además, pruebe y enfatice mucho aquellas clases y entidades cuyo nivel y uso van a cambiar mucho (por ejemplo, para fines de optimización) o cuyos detalles internos van a cambiar por cualquier razón. El ejemplo más típico es una clase que envuelve una matriz enorme, mapeada desde el disco.


7

¡Absolutamente!

¿Qué, eso no es suficiente para ti?

En la programación científica más que cualquier otro tipo, estamos desarrollando en base a tratar de igualar un sistema físico. ¿Cómo sabrá si lo ha hecho además de las pruebas? Antes de comenzar a codificar, decida cómo va a usar su código y resuelva algunos ejemplos. Intenta atrapar cualquier posible caso de borde. Hágalo de forma modular; por ejemplo, para una red neuronal, puede realizar un conjunto de pruebas para una sola neurona y un conjunto de pruebas para una red neuronal completa. De esa manera, cuando comience a escribir código, puede asegurarse de que su neurona funcione antes de comenzar a trabajar en la red. Trabajar en etapas como esta significa que cuando se encuentra con un problema solo tiene que probar la 'etapa' más reciente del código, las etapas anteriores ya se han probado.

Además, una vez que tenga las pruebas, si necesita reescribir el código en un idioma diferente (convirtiendo a CUDA, por ejemplo), o incluso si solo lo está actualizando, ya tiene las cajas de prueba y puede usarlas para hacer asegúrese de que ambas versiones de su programa funcionen de la misma manera.


+1: "Trabajar en etapas como esta significa que cuando te encuentras con un problema solo tienes que probar la 'etapa' más reciente, las etapas anteriores ya se han probado".
ximiki

5

Si.

La idea de que cualquier código se escribe sin pruebas unitarias es anatema. A menos que demuestre que su código es correcto y luego demuestre que la prueba es correcta = P.


3
... y luego demuestras que la prueba de que la prueba es correcta, y ... ahora eso es una profunda madriguera.
JM

2
¡Las tortugas hasta abajo hacen que Dijkstra se sienta orgullosa!
aterrel

2
¡Solo resuelva el caso general, y luego haga que su prueba demuestre que es correcta! Toro de tortugas!
Aesin

5

Abordaría esta pregunta pragmáticamente en lugar de dogmáticamente. Hágase la pregunta: "¿Qué podría salir mal en la función X?" Imagine lo que sucede con la salida cuando introduce algunos errores típicos en el código: un prefactor incorrecto, un índice incorrecto, ... Y luego escriba pruebas unitarias que puedan detectar ese tipo de error. Si para una función determinada no hay forma de escribir tales pruebas sin repetir el código de la función en sí, entonces no lo haga, pero piense en las pruebas en el siguiente nivel superior.

Un problema mucho más importante con las pruebas unitarias (o de hecho cualquier prueba) en el código científico es cómo lidiar con las incertidumbres de la aritmética de punto flotante. Hasta donde sé, todavía no hay buenas soluciones generales.


Ya sea que pruebe manualmente o automáticamente usando pruebas unitarias, tiene exactamente los mismos problemas con la representación de coma flotante. Recomiendo encarecidamente la excelente serie de artículos de Richard Harris en la revista de sobrecarga de ACCU .
Mark Booth

"Si para una función dada no hay forma de escribir tales pruebas sin repetir el código de la función en sí, entonces no". ¿Puedes elaborar? Un ejemplo me lo aclararía.
ximiki

5

Siento pena por Tangurena, por aquí, el mantra es "El código no probado es código roto" y eso vino del jefe. En lugar de repetir todas las buenas razones para hacer pruebas unitarias, solo quiero agregar algunos detalles.

  • El uso de la memoria debe ser probado. Cada función que asigna memoria debe probarse asegurándose de que las funciones que almacenan y recuperan datos en esa memoria estén haciendo lo correcto. Esto es aún más importante en el mundo de las GPU.
  • Si bien se mencionó brevemente antes, probar casos extremos es extremadamente importante. Piense en estas pruebas de la misma manera que prueba el resultado de cualquier cálculo. Asegúrese de que el código se comporta en los bordes y falla correctamente (sin embargo, lo define en su simulación) cuando los parámetros de entrada o los datos caen fuera de los límites aceptables. El pensamiento involucrado en la redacción de este tipo de prueba ayuda a agudizar su trabajo y puede ser una de las razones por las que rara vez encuentra a alguien que haya escrito pruebas unitarias pero no encuentre útil el proceso.
  • Use un marco de prueba (como lo mencionó Geoff, quien proporcionó un buen enlace). He utilizado el marco de prueba BOOST junto con el sistema CTest de CMake y puedo recomendarlo como una forma fácil de escribir rápidamente pruebas unitarias (así como pruebas de validación y regresión).

+1: "Asegúrese de que el código se comporta en los bordes y falla correctamente (sin embargo, lo define en su simulación) cuando los parámetros de entrada o los datos caen fuera de los límites aceptables".
ximiki

5

He utilizado las pruebas unitarias con buenos resultados en varios códigos de pequeña escala (es decir, programador único), incluida la tercera versión de mi código de análisis de tesis en física de partículas.

Las dos primeras versiones se habían derrumbado por su propio peso y la multiplicación de las interconexiones.

Otros han escrito que la interacción entre módulos es a menudo el lugar donde se rompe la codificación científica, y tienen razón al respecto. Pero es mucho más fácil diagnosticar esos problemas cuando puede demostrar de manera concluyente que cada módulo está haciendo lo que debe hacer.


3

Un enfoque ligeramente diferente que utilicé al desarrollar un solucionador químico (para dominios geológicos complejos) fue lo que podría llamarse Prueba de unidad mediante el fragmento Copiar y pegar .

Construir un arnés de prueba para el código original incrustado en un gran modelador de sistemas químicos no era factible en el plazo.

Sin embargo, pude elaborar un conjunto cada vez más complejo de fragmentos que mostraban cómo funcionaba el analizador (Boost Spirit) para las fórmulas químicas, como pruebas unitarias para diferentes expresiones.

La prueba de unidad final, más compleja, estaba muy cerca del código necesario en el sistema, sin tener que cambiar ese código para que se pueda imitar. Por lo tanto, pude copiar el código probado de mi unidad.

Lo que hace que esto sea más que un simple ejercicio de aprendizaje y un verdadero conjunto de regresión son dos factores: las pruebas unitarias se mantienen en la fuente principal y se ejecutan como parte de otras pruebas para esa aplicación (y sí, detectaron un efecto secundario de Boost El espíritu cambió 2 años después), debido a que el código copiado y pegado se modificó mínimamente en la aplicación real, podría haber comentarios que se refirieran a las pruebas unitarias para ayudar a alguien a mantenerlos sincronizados.


2

Para bases de código más grandes, las pruebas (no necesariamente las pruebas unitarias) para las cosas de alto nivel son útiles. Las pruebas unitarias para algunos algoritmos más simples también son útiles para asegurarse de que su código no tenga sentido porque su función auxiliar está usando en sinlugar de cos.

Pero para el código de investigación general es muy difícil escribir y mantener pruebas. Los algoritmos tienden a ser grandes sin resultados intermedios significativos que pueden tener pruebas obvias y, a menudo, tardan mucho tiempo en ejecutarse antes de que haya un resultado. Por supuesto, puede probar contra ejecuciones de referencia que tuvieron buenos resultados, pero esta no es una buena prueba en el sentido de la prueba unitaria.

Los resultados a menudo son aproximaciones de la verdadera solución. Si bien puede probar sus funciones simples si son precisas hasta cierto épsilon, será muy difícil verificar si, por ejemplo, alguna malla de resultados es correcta o no, lo cual fue evaluado por inspección visual por el usuario (usted) antes.

En tales casos, las pruebas automatizadas a menudo tienen una relación costo / beneficio demasiado alta. Recomiendo algo mejor: escribir programas de prueba. Por ejemplo, escribí un script de python de tamaño mediano para crear datos sobre resultados, como histogramas de tamaños de bordes y ángulos de una malla, área del triángulo más grande y más pequeño y su relación, etc.

Puedo usarlo para evaluar mallas de entrada y salida durante el funcionamiento normal y usarlo para realizar una comprobación de cordura después de cambiar el algoritmo. Cuando cambio el algoritmo, no siempre sé si el nuevo resultado es mejor, porque a menudo no existe una medida absoluta de cuál es la mejor aproximación. Pero al generar tales métricas puedo decir acerca de algunos factores, como "La nueva variante finalmente tiene una mejor relación angular pero una tasa de convergencia peor".

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.