software2

Rienda suelta a la prueba ejército

2017-05-29, Comentarios

Recientemente he descrito una solución para el problema de dividir una lista en trozos de tamaño uniforme. Es un simple problema bastante con solo dos entradas: la lista (o de otros sliceable contenedor) xs y el número de fragmentos n. Sin embargo, hay trampas a evitar y casos especiales a considerar — lo que si n es mayor que el de la lista, por ejemplo? Deben los trozos comprenden contiguos de los elementos de la lista original?

Las pruebas que se me ocurrió son sencillas y monótonas. Ellos fueron desarrollados en el contexto de mi propia hipótesis acerca de la solución y los casos especiales que me podía imaginar. Fueron escritos después de la aplicación — es decir, el desarrollo no fue impulsado por las pruebas. Son whitebox pruebas, diseñadas para cubrir las diferentes rutas a través del código basado en mi conocimiento de información privilegiada.

Son estas pruebas adecuadas? Ciertamente, ellos no representan con exactitud los datos que llegará el algoritmo en la práctica. Podemos estar seguros de que no hemos olvidado algo? Haría las pruebas de cubrir todas las rutas, si la ejecución ha cambiado?

David R MacIver describe otra, complementaria, enfoque en una charla que asistí en ACCU 2016. En la charla abstracto que caracteriza a la (clase de) las pruebas que yo había escrito como anecdótica — “déjenme decirles acerca de este tiempo me llama una función … y entonces volvió este .. y luego plantea una excepción … etc. etc.”

Cómo acerca de si el conjunto de pruebas en su lugar se describen las propiedades requeridas del sistema bajo prueba y, a continuación, evoca insumos diseñados para ver si estas propiedades mantenga bajo estrés? Así, en lugar de nuestra suite de pruebas de ser un conjunto limitado de entrada/salida de los pares, se convierte en un ejecutable especificación validado por un robot ejército.

China's Robot Army

Este enfoque suena convincente, pero yo tenía mis dudas. Yo también tenía mis dudas sobre la idoneidad de mi código y pruebas. Una perfecta oportunidad, entonces, para probar Hipótesis, una fuente abierta basada en la propiedad biblioteca de pruebas desarrollado por David MacIver.

He utilizado la versión de Python de la biblioteca, que es el principal de la aplicación. El resto de este artículo describe mi experiencia en el uso de hipótesis para la primera vez: yo no estoy diciendo experiencia.

Excelente!

Instalación habitual era el pip de la invocación. La documentación está escrita con cuidado. Es evidente que la biblioteca es maduro, apoyado y desarrollado activamente. Está licenciado bajo la Licencia Pública de Mozilla.

Recordemos que el código quería poner a prueba lee:

def chunk(xs, n): ""Dividir la lista, xs, en n trozos de tamaño uniforme"' L = len(xs) afirmar 0 < n <= L s, r = divmod(L, n) t = s + 1 retorno ([xs[p:p+t] para p en el intervalo(0, r*t, t)] + [xs[p:p+s] para p en el rango(r*t, L, s)]) 

Yo también propuso una segunda itertools versión basada en:

de itertools de importación se acumulan, la cadena, la repetición, el tee def chunk(xs, n): ""Dividir la lista, xs, en n trozos de tamaño uniforme"' afirmar n > 0 L = len(xs) s, r = divmod(L, n) anchos = cadena(repetir(s+1, r), repetir(s, n-r)) compensaciones = acumulan(de la cadena((0,), anchos)) b, e = tee(offsets)
siguiente(e) volver [xs[s] de s en el mapa(slice, b, e)] 

La primera cosa que usted nota cuando se piensa acerca de una propiedad en función de la prueba es que la especificación de la función docstring — no describe la forma exacta de la salida. De hecho, como un comentario en el artículo señalado, mi propia interpretación de la especificación no es el único, y permitiendo que los trozos se han formado a partir de elementos no contiguos permite una particular solución elegante.

También, si la lista no divide exactamente en n trozos, lo que debe ser el resultado? Bien, aunque yo hubiera sido feliz con cualquier uniformemente fragmentada solución, mi convencionales de la unidad de pruebas supone una aplicación que coloca los trozos más grandes primero.

def test_chunk(): afirmar pedazo(", 1) == ["] afirmar chunk('ab', 2) == ['a', 'b'] afirmar chunk('abc', 2) == ['ab', 'c'] xs = list(range(8)) afirmar chunk(xs, 2) == [[0, 1, 2, 3], [4, 5, 6, 7]] afirmar chunk(xs, 3) == [[0, 1, 2], [3, 4, 5], [6, 7]] afirmar chunk(xs, 5) == [[0, 1], [2, 3], [4, 5], [6], [7]] rs = rango(1000000) afirmar chunk(rs, 2) == [rango(500000), rango(500000, 1000000)] 

Aviso, por cierto, que aunque el docstring sólo menciona las listas, no me puedo resistir lo que demuestra que el algoritmo funciona también para las cadenas y los rangos para cualquier sliceable secuencia, de hecho.

He aquí lo que se inició con cuando traté de especificar el “tamaño uniforme de” propiedad mediante hipótesis.

def test_evenly_chunked(xs, n): trozos = chunk(xs, n) afirmar len(trozos) == n chunk_lens = {len(c) para c en trozos} afirmar len(chunk_lens) en {1, 2} afirmar max(chunk_lens) - min(chunk_lens) en {0, 1} 

Este primer caso de prueba se define “de tamaño uniforme”, que indica que el resultado se compone de n trozos, que el conjunto de las longitudes de estos fragmentos es 1 (todos los trozos del mismo tamaño) o 2), y el máximo pedazo de longitud es igual o más que el mínimo fragmento de longitud.

Esto no especificar completamente la función. También necesitamos afirmaciones que confirman que la recombinación de los trozos se produce la secuencia original.

def test_combining_chunks(xs_n): paso # volveremos a esto! 

Volveremos a esto más adelante!

Ahora, test_evenly_chunked() muy parecido a una prueba convencional de la función. Sólo necesita algunos valores de entrada. En lugar de meter a la función con una mano-valores elegidos, podemos dejar que la hipótesis de tener un ir.

Basado en una lectura de la guía de inicio Rápido he intentado esto:

importación de hipótesis como ht
importación de hipótesis.estrategias como st @ht.dado(xs=pt.listas(), n=pt.enteros())
def test_evenly_chunked(xs, n): trozos = chunk(xs, n) afirmar len(trozos) == n chunk_lens = {len(c) para c en trozos} afirmar len(chunk_lens) en {1, 2} afirmar max(chunk_lens) - min(chunk_lens) en {0, 1} 

Como se puede ver, la función de prueba de pre-condiciones se encapsula en una hipótesis.dado decorador, en el que se especifica el uso de hipótesis.estrategias.(listas) y la hipótesis.estrategias.enteros() para generar los valores de la prueba de xs y nrespectivamente.

El resultado fue un largo pero provechoso fracaso, que imprime la documentación de las listas() la estrategia y el uso de error:

hipótesis.errores.InvalidArgument: No se puede crear no-vacía la lista sin un tipo de elemento 

Bien entonces. La función no se preocupan realmente por el tipo de elemento. Enteros va a hacer.

@ht.dado(xs=pt.listas(pt.enteros()), n=pt.enteros())
def test_evenly_chunked(xs, n):
.... 

Esto me dio un error, junto con un mínimo de caso de prueba que la produce.

xs = [], n = 0 def chunk(xs, n): ""Dividir la lista, xs, en n trozos de tamaño uniforme"' L = len(xs)
> hacer valer 0 < n <= L
E AssertionError 

Nuestra función, fragmento() requiere que el valor de n para ser encerrada en el intervalo (0, len(xs)]. Mirando más de cerca el fracaso, podemos ver que la función de sujeto de la prueba, fragmento(), no es muy grande, ya que no será capaz de dividir una lista vacía en cualquier número de trozos, ya que, en este caso, L es cero y no hay ningún valor de n que satisface 0 < n <= L.

En este punto tuve que hace que algunas de las opciones:

  • deben mis pruebas confirman chunk() es la comprobación de pre-condiciones (por la captura de la AssertionError)?
  • en caso de que mi función de manejar el caso cuando n > L? No es la intención de uso de la función, pero puede ser manejado.
  • ¿qué pasa cuando n == 0? La división de un no-lista vacía en 0 trozos es imposible, pero supongo que una lista vacía se puede dividir en 0trozos.

He tomado algunas decisiones.

  • Decidí no probar la pre-condición de afirmaciones. En su lugar, me gustaría modificar la estrategia de prueba a pasar en las entradas válidas.
  • Decidí que iría con el itertools pedazo función de la cual, naturalmente, asas n > L.
  • Decidí que mi función no tiene por qué manejar n == 0, incluso cuando xs == [].

Aquí está la versión modificada de la prueba de código

@ht.dado(xs=pt.listas(pt.enteros()),
n=pt.enteros(min_value=1))
def test_evenly_chunked(xs, n): trozos = chunk(xs, n) afirmar len(trozos) == n chunk_lens = {len(c) para c en trozos} afirmar len(chunk_lens) en {1, 2} afirmar max(chunk_lens) - min(chunk_lens) en {0, 1}
.... 

Cuando traté de ejecutar los tests, que parecía colgar hasta que los interrumpe.

> py.prueba
============================= sesión de la prueba inicia =============================
la plataforma win32 -- Python 3.5.3, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
....
plugins: hipótesis-3.8.3
recogido 1 elementos test_chunk.py C-c C-c !!!!!!!!!!!!!!!!!!!!!!!!!!!!!! KeyboardInterrupt !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
para mostrar una completa trazabilidad en KeyboardInterrupt uso --fulltrace 

Ahora, yo tenía la sospecha de que parte() no podía manejar cualquier entrada de números enteros — fue diseñado para un valor de n igual a multiprocesamiento.cpu_count() — pero yo quería ver lo que iba a suceder sin límites superiores. Aquí fue mi respuesta. Ejecución e interrumpir de nuevo con --fulltrace conjunto, tengo varias páginas de salida que termina con la prueba entradas:

xs = [50], n = 67108865 

Evidentemente el código se ha de tomar un tiempo para crear una lista integrada por una sola lista [50] y más de 67 millones de vacío listas [], [], [], ....

Una vez más, tuve que tomar una decisión. Tal como era de esperar, es una decisión que ya había enfrentado. Yo podría hacer chunk() una función de generador, produciendo los trozos uno a la vez — un trivial y cambio natural a la itertools basado en la aplicación — o yo podría limitar las pruebas más adecuados valores de n.

En este caso, decidí seguir con lo que tenía: mi función sería aceptar una lista y devuelve una lista (de las listas). En un intento de conseguir algunas de pasar las pruebas, puedo establecer un valor máximo de n.

@ht.dado(xs=pt.listas(pt.enteros()), n=pt.enteros(min_value=1, max_value=100))
def test_evenly_chunked(xs, n):
.... 

En el pasado, tuve una prueba que pasa.

py.prueba ============================= sesión de la prueba inicia =============================
la plataforma win32 -- Python 3.5.3, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: //trabajo en rodajas-python, inifile:
plugins: hipótesis-3.8.3
recogido 1 elementos test_chunk.py . ========================== 1 aprobada en 0.23 segundos =========================== 

Basándose en este éxito, quería confirmar la función también se tratan otros sliceable tipos de cadenas de caracteres y bytes específicamente. Hipótesis proporciona una one_of estrategia para la combinación de otras estrategias.

@ht.dado(xs=pt.one_of(pt.text(),
st.binario(),
st.listas(pt.enteros())), n=pt.enteros(min_value=1, max_value=100))
def test_evenly_chunked(xs, n): trozos = chunk(xs, n) afirmar len(trozos) == n chunk_lens = {len(c) para c en trozos} afirmar len(chunk_lens) en {1, 2} afirmar max(chunk_lens) - min(chunk_lens) en {0, 1} 

De nuevo, pasa la prueba.

py.prueba
============================= sesión de la prueba inicia =============================
la plataforma win32 -- Python 3.5.3, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: //trabajo en rodajas-python, inifile:
plugins: hipótesis-3.8.3
recogido 1 elementos test_chunk.py . ========================== 1 pasa en 0.30 segundos =========================== 

Este resultado es bastante hermética. En general, pasando las pruebas no debe llamar la atención a sí mismos, pero ¿qué aportaciones tenía mis estrategias de prueba generados? Fueron suficientes?

Un modificador de línea de comandos proporciona un poco más de detalle.

py.prueba --hipótesis mostrar estadísticas .... test_chunk.py::test_evenly_chunked: - 200 pasando ejemplos, 0 fallando ejemplos, 0 inválido ejemplos - Típico de tiempos de ejecución: < 1ms - Se detuvo debido a que las configuraciones.max_examples=200 

También es posible echar un vistazo a los ejemplos producidos por la estrategia de prueba.

>>> s=pt.one_of(pt.text(), st.binario(), st.listas(pt.enteros()))
>>> s).ejemplo()
b"
>>> s).ejemplo()
b'\xc2\xfd6['
>>> s).ejemplo() ':\nú&\U000ea7e8'
>>> s).ejemplo()
b'\xe7z'
>>> s).ejemplo() "
>>> s).ejemplo()
[184, 36, a-205, 1486638017]

Aquí mi última prueba de la suite. En lugar de codificar un valor máximo de n, he utilizado un compuesto de la estrategia que se adapta a n a el tamaño de xs. También he añadido una prueba que confirma el resultado no comprenden fragmentos de la secuencia de entrada.

importación functools importación de hipótesis como ht
importación de hipótesis.estrategias como st @st.compuesto
def items_and_chunk_count(empate): xs = dibuja(pt.one_of(pt.text(),
st.binario(),
st.listas(pt.enteros()))) n = dibuja(pt.enteros(min_value=1, max_value=max(1, len(xs)))) volver xs, n @ht.dado(xs_n=items_and_chunk_count())
def test_evenly_chunked(xs_n): "'Compruebe que no hay n uniformemente trozos del tamaño de una"' xs, n = xs_n trozos = chunk(xs, n) afirmar len(trozos) == n chunk_lens = {len(c) para c en trozos} afirmar len(chunk_lens) en {1, 2} afirmar max(chunk_lens) - min(chunk_lens) en {0, 1} @ht.dado(xs_n=items_and_chunk_count())
def test_combining_chunks(xs_n): "'Verificar la recombinación de los trozos se reproduce la secuencia original."' xs, n = xs_n trozos = chunk(xs, n) afirmar functools.reducir(lambda x, y: x+y, trozos) == xs 

En los comentarios a mi artículo original Mike Edey presentado en una elegante solución para el problema original de manera uniforme la subdivisión de una secuencia en un número exacto de fragmentos:

def chunk(xs, n): volver [xs[index::n] para el índice en el rango(n)] 

Esta es una deliciosa pieza pieza de código, y un enfoque que simplemente no habían considerado. Si la entrada de la lista xs representa un número de tareas que se distribuirán entre los n de los trabajadores, esto hace el trabajo de manera uniforme. En mi ejemplo muy motivador, aunque, sin embargo, la secuencia de entrada fue un documento que causó un problema, y lo que yo quería hacer era dividir ese documento en una serie de secciones y ver cuales de estos mostraron el mismo problema: que es, yo necesitaba que los trozos sean contiguos, bloques de texto del documento original. Esta es la propiedad que test_combining_chunks()comprueba.

Ejecución de Mike Edey la aplicación a través de la suite de prueba, obtenemos:

py.prueba
============================= sesión de la prueba inicia =============================
la plataforma win32 -- Python 3.5.3, pytest-3.0.7, py-1.4.33, pluggy-0.4.0
rootdir: //trabajo en rodajas-python, inifile:
plugins: hipótesis-3.8.3
recogido 2 elementos test_chunk.py .F ================================== FALLOS ===================================
____________________________ test_combining_chunks ____________________________ @ht.dado(xs_n=items_and_chunk_count())
> def test_combining_chunks(xs_n): test_chunk.py:29: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
d:\venvs\slackbot\lib\site-packages\hypothesis\core.py:634: en wrapped_test
estado.ejecutar()
d:\venvs\slackbot\lib\site-packages\hypothesis\core.py:531: en ejecución print_example=True, is_final=True
d:\venvs\slackbot\lib\site-packages\hypothesis\executors.py:58: en default_new_style_executor función de devolución(de datos)
d:\venvs\slackbot\lib\site-packages\hypothesis\core.py:113: en ejecución retorno de prueba(*args, **kwargs)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ xs_n = ('001', 2) @ht.dado(xs_n=items_and_chunk_count()) def test_combining_chunks(xs_n): "'Verificar la recombinación de los trozos se reproduce la secuencia original."' xs, n = xs_n trozos = chunk(xs, n)
> assert functools.reducir(lambda x, y: x+y, trozos) == xs
E AssertionError: assert '010' == '001'
E - 010
E + 001 test_chunk.py:33: AssertionError
--------------------------------- Hipótesis ----------------------------------
Falsificación de ejemplo: test_combining_chunks(xs_n=('001', 2))
===================== 1 fracasado, 1 aprobada en 0.52 segundos ====================== 

Hipótesis ha descubierto un mínimo ejemplo de fallo: la cadena 001 divide en 2 trozos como 01, 0.

Hipótesis funcionado bien para este ejemplo en particular.

  • me obligó a definir la especificación de una función
  • Tuve que considerar los casos especiales: sería la función de comportarse en la cara de lógicamente admisible entradas, y no solo la que yo tenía en mente cuando escribí esto
  • ha aumentado mi confianza la función era la correcta
  • y particularmente atractivo, en este caso las pruebas no estaban atados a un detalle de la aplicación, y seguirá trabajando si, por ejemplo, los trozos más grandes fueron para que aparezca al final de los resultados.

Más en general, he encontrado la hipótesis de la biblioteca de sólidos. Es bien diseñado y documentado, así como ser un buen ejemplo de cómo el uso de Python decoradores.

Yo diría que la propiedad basada en pruebas complementa ejemplo basado en pruebas. Ejemplo basado en la unidad de pruebas que muestran cómo una función se utiliza, por ejemplo; con la hipótesis, esta demostración útil que sucede detrás de las escenas (aunque tenga en cuenta que sus pruebas de hipótesis puede incluir explícita @ejemplos). Ejemplo basado en las pruebas de unidad son por lo general uno o dos órdenes de magnitud más rápido de ejecutar. No es un problema si un par de pruebas de tomar la mitad de un segundo para correr, pero lo que si usted tiene un par de miles de pruebas?

En mi caso, la incorporada en las estrategias fueron lo suficientemente buenos como para generar entradas para la función. Me imagino que no es el caso para las funciones más arriba en una pila de software. Prueba de las funciones de configuración puede ser un trabajo duro y sospecho que la configuración de prueba estrategias sería más difícil.

Para terminar, me gustaría citar una parte de la sección de la Hipótesis de la documentación que describe su propósito.

El Software es, como dicen, comer el mundo. El Software también es terrible. Es buggy, inseguros y generalmente mal pensado. Esta combinación es claramente una receta para el desastre.

Y el estado de las pruebas de software es aún peor. Aunque es bastante polémica en este punto que usted debe probar su código, se puede realmente decir con una cara seria que la mayoría de los proyectos en los que he trabajado son debidamente probado?

Una gran parte del problema aquí es que es muy difícil escribir un buen pruebas. Las pruebas codificar exactamente en los mismos supuestos y las falacias que tenía cuando escribió el código, de modo que se pierda exactamente los mismos errores con los que se perdió cuando se escribió el código.

El Propósito de la Hipótesis

Software almacen de Cea Ordenadores

Comentarios desactivados en Rienda suelta a la prueba ejército