¿Cómo prueba la unidad de un codificador?

9

Tengo algo como esto:

public byte[] EncodeMyObject(MyObject obj)

He estado haciendo pruebas unitarias como esta:

byte[] expectedResults = new byte[3]{ 0x01, 0x02, 0xFF };
Assert.IsEqual(expectedResults, EncodeMyObject(myObject));

EDITAR: Las dos formas que he visto propuestas son:

1) Usar valores esperados codificados, como el ejemplo anterior.

2) Usar un decodificador para decodificar el conjunto de bytes codificados y comparar los objetos de entrada / salida.

El problema que veo con el método 1 es que es muy frágil y requiere muchos valores codificados.

El problema con el método 2 es que probar el codificador depende de que el decodificador funcione correctamente. Si el codificador / decodificador se rompen por igual (en el mismo lugar), entonces las pruebas podrían producir falsos positivos.

Estas pueden muy bien ser las únicas formas de probar este tipo de método. Si ese es el caso, entonces está bien. Estoy haciendo la pregunta para ver si hay mejores estrategias para este tipo de pruebas. No puedo revelar las partes internas del codificador particular en el que estoy trabajando. Le pregunto en general cómo resolvería este tipo de problema, y ​​no creo que las partes internas sean importantes. Suponga que un objeto de entrada dado siempre producirá la misma matriz de bytes de salida.

CondiciónRacer
fuente
44
¿Cómo myObjectva de myObjecta { 0x01, 0x02, 0xFF }? ¿Se puede descomponer y probar ese algoritmo? La razón por la que pregunto es ahora, parece que tienes una prueba que demuestra que una cosa mágica produce otra cosa mágica. Su única confianza es que la única entrada produce la única salida. Si puede desglosar el algoritmo, puede ganar más confianza en el algoritmo y depender menos de las entradas y salidas mágicas.
Anthony Pegram
3
@Codism ¿Qué pasa si el codificador y el decodificador se rompen en el mismo lugar?
ConditionRacer
2
Las pruebas son, por definición, hacer algo y verificar si obtuviste los resultados esperados, que es lo que hace tu prueba. Por supuesto, necesitará asegurarse de realizar suficientes pruebas como esa para asegurarse de ejercer todo su código y cubrir casos extremos y otras rarezas.
Blrfl
1
@ Justin984, bueno, ahora vamos más profundo. No expondría a esos internos privados como miembros de la API del codificador, ciertamente no. Los eliminaría por completo del codificador. O más bien, el codificador delegaría en otro lugar, una dependencia . Si se trata de una batalla entre un método monstruoso no comprobable o un montón de clases auxiliares, elijo las clases auxiliares cada vez. Pero de nuevo, estoy haciendo inferencias no informadas a su código en este momento, porque no puedo verlo. Pero si desea ganar confianza en sus pruebas, tener métodos más pequeños haciendo menos cosas es una forma de llegar allí.
Anthony Pegram
1
@ Justin984 Si la especificación cambia, cambia la salida esperada en su prueba y ahora falla. Luego, cambia la lógica del codificador para pasar. Parece exactamente cómo se supone que funciona TDD y solo fallará cuando debería. No veo cómo esto lo hace quebradizo.
Daniel Kaplan

Respuestas:

1

Estás en una situación un poco desagradable allí. Si tuviera un formato estático en el que estaba codificando, su primer método sería el camino a seguir. Si fuera solo su propio formato, y nadie más tuviera que decodificar, el segundo método sería el camino a seguir. Pero realmente no encajas en ninguna de esas categorías.

Lo que haría es tratar de desglosar las cosas por nivel de abstracción.

Entonces comenzaría con algo en el nivel de bits, que probaría algo como

bitWriter = new BitWriter();
bitWriter.writeInt(42, bits = 7);
assertEqual( bitWriter.data(), {0x42} )

Entonces, la idea es que el escritor de bits sepa cómo escribir los tipos de campos más primitivos, como los ints.

Se implementarían tipos más complejos usando y probando algo como:

bitWriter = new BitWriter();
writeDate(bitWriter, new Datetime(2001, 10, 4));

bitWriter2 = new BitWriter();
bitWriter2.writeInt(2001, 12)
bitWriter2.writeInt(10, 4)
bitWriter2.writeInt(4, 6)

assertEquals(bitWriter.data(), bitWriter2.data() )

Tenga en cuenta que esto evita cualquier conocimiento de cómo se empaquetan los bits reales. Eso fue probado por la prueba anterior, y para esta prueba, simplemente asumiremos que funciona.

Luego, en el siguiente nivel de abstracción tendríamos

bitWriter = new BitWriter();
encodeObject(bitWriter, myObject);


bitWriter2 = new BitWriter();
bitWriter2.writeInt(42, 32)
writeDate(bitWriter2, new Datetime(2001, 10, 4));
writeVarString(bitWriter2, "alphanumeric");

assertEquals(bitWriter.data(), bitWriter2.data() )

así que, nuevamente, no tratamos de incluir el conocimiento de cómo se codifican realmente las cadenas de caracteres, las fechas o los números. En esta prueba, solo estamos interesados ​​en la codificación producida por encodeObject.

El resultado final es que si se cambia el formato de las fechas, tendrá que corregir las pruebas que realmente involucran fechas, pero todos los demás códigos y pruebas no se preocupan por cómo se codifican realmente las fechas y una vez que actualiza el código para hacer ese trabajo, todas esas pruebas pasarán bien.

Winston Ewert
fuente
Me gusta esto. Supongo que esto es lo que algunos de los otros comentaristas decían sobre dividirlo en pedazos más pequeños. No evita por completo el problema cuando cambia la especificación, pero lo mejora.
ConditionRacer
6

Depende Si la codificación es algo completamente fijo, donde se supone que cada implementación crea exactamente la misma salida, no tiene sentido verificar nada más que verificar que las entradas de ejemplo se asignen exactamente a las salidas esperadas. Esa es la prueba más obvia, y probablemente también la más fácil de escribir.

Si hay margen de maniobra con salidas alternativas, como en el estándar MPEG (por ejemplo, hay ciertos operadores que puede aplicar a la entrada, pero puede cambiar el esfuerzo de codificación por la calidad de salida o el espacio de almacenamiento), entonces es mejor aplicar el definió una estrategia de decodificación para la salida y verifique que sea la misma que la entrada o, si la codificación tiene pérdidas, que esté razonablemente cerca de la entrada original. Es más difícil de programar, pero lo protege contra cualquier mejora futura que pueda hacerse a su codificador.

Kilian Foth
fuente
2
Suponga que usa el decodificador y compara valores. ¿Qué pasa si el codificador y el decodificador están rotos en el mismo lugar? El codificador codifica incorrectamente y el decodificador decodifica incorrectamente, pero los objetos de entrada / salida son correctos porque el proceso se realizó incorrectamente dos veces.
ConditionRacer
@ Justin984 a continuación, utilizar los llamados "vectores de prueba", de entrada sabe / pares de salida que se puede utilizar precisamente para probar un codificador y decodificador
trinquete monstruo
@ratchet freak Eso me vuelve a poner a prueba con los valores esperados. Lo cual está bien, eso es lo que estoy haciendo actualmente, pero es un poco frágil, así que estaba buscando ver si hay mejores formas.
ConditionRacer
1
Además de leer detenidamente el estándar y crear un caso de prueba para cada regla, no hay forma de evitar que tanto un codificador como un decodificador contengan el mismo error. Por ejemplo, supongamos que "ABC" debe traducirse a "xyz" pero el codificador no lo sabe y su decodificador tampoco entendería "xyz" si alguna vez lo encontrara. Los casos de prueba hechos a mano no contienen la secuencia "ABC", porque el programador no estaba al tanto de esa regla, y también una prueba con codificación / decodificación de cadenas aleatorias pasaría incorrectamente porque tanto el codificador como el decodificador ignoran el problema.
user281377
1
Para ayudar a detectar errores que afectan tanto a los decodificadores como a los codificadores escritos por usted mismo debido a la falta de conocimiento, haga un esfuerzo para obtener salidas de codificador de otros proveedores; e intente también probar la salida de su codificador en los decodificadores de terceros. No hay una alternativa a su alrededor.
rwong
3

Prueba eso encode(decode(coded_value)) == coded_valuey decode(encode(value)) == value. Puede dar una entrada aleatoria a las pruebas si lo desea.

Todavía es posible que tanto el codificador como el decodificador se rompan de manera complementaria, pero eso parece bastante improbable a menos que tenga un malentendido conceptual del estándar de codificación. Hacer pruebas codificadas del codificador y decodificador (como ya lo estás haciendo) debería protegerte contra eso.

Si tiene acceso a otra implementación de esto que se sabe que funciona, al menos puede usarla para tener la seguridad de que su implementación es buena incluso si fuera imposible usarla en las pruebas unitarias.

Michael Shaw
fuente
Estoy de acuerdo en que un error complementario de codificador / decodificador es poco probable en general. En mi caso específico, el código para las clases de codificador / decodificador es generado por otra herramienta basada en reglas de una base de datos. Entonces, los errores complementarios ocurren ocasionalmente.
ConditionRacer
¿Cómo puede haber 'errores complementarios'? Eso implica que hay una especificación externa para la forma codificada y, por lo tanto, un decodificador externo.
Kevin Cline
No entiendo tu uso de la palabra externa. Pero hay una especificación de cómo se codifican los datos y también un decodificador. Un error complementario es cuando el codificador y el decodificador funcionan de manera complementaria pero que se desvía de la especificación. Tengo un ejemplo en los comentarios debajo de la pregunta original.
ConditionRacer
Si se suponía que el codificador implementaba ROT13 pero accidentalmente hizo ROT14 y el decodificador también lo hizo, entonces decodifica (codifica ('a')) == 'a' pero el codificador todavía está roto. Para cosas mucho más complicadas que eso, es probable que sea mucho menos probable que ese tipo de cosas sucedan, pero en teoría podría suceder.
Michael Shaw
@MichaelShaw es solo un truco, el codificador y el decodificador para ROT13 son los mismos; ROT13 es su propio inverso. Si implementó ROT14 por error, entonces decode(encode(char))no sería igual char(sería igual char+2).
Tom Marthenal
2

Prueba a los requisitos .

Si los requisitos son '' codificar a un flujo de bytes que cuando se decodifica produce un objeto equivalente '', simplemente pruebe el codificador decodificando. Si está escribiendo tanto el codificador como el decodificador, simplemente pruébelos juntos. No pueden tener "errores coincidentes". Si trabajan juntos, entonces la prueba pasa.

Si hay otros requisitos para el flujo de datos, tendrá que probarlos examinando los datos codificados.

Si el formato codificado está predefinido, entonces deberá verificar los datos codificados con el resultado esperado, como lo hizo, u (mejor) obtener un decodificador de referencia en el que se pueda confiar para realizar la verificación. El uso de un decodificador de referencia elimina la posibilidad de que haya malinterpretado la especificación del formato.

Kevin Cline
fuente
1

Dependiendo del marco de prueba y el paradigma que esté utilizando, aún puede usar el patrón Arrange Act Assert para esto como ha dicho.

[TestMethod]
public void EncodeMyObject_ForValidInputs_Encodes()
{
    //Arrange object under test
    MyEncoder encoderUnderTest = new MyEncoder();
    MyObject validObject = new MyOjbect();
    //arrange object for condition under test

    //Act
    byte[] actual = encoderUnderTest.EncodeMyObject(myObject);

    //Assert
    byte[] expected = new byte[3]{ 0x01, 0x02, 0xFF };
    Assert.IsEqual(expected, actual);
}

Debe conocer los requisitos EncodeMyObject()y puede usar este patrón para probar cada uno de ellos para criterios válidos e inválidos, organizando cada uno de ellos y codificando el resultado esperado para expected, de manera similar para el decodificador.

Dado que los esperados están codificados, estos serán frágiles si tiene un cambio masivo.

Es posible que pueda automatizar con algo controlado por parámetros (eche un vistazo a Pex ) o si está haciendo DDD o BDD eche un vistazo a gerkin / pepino .

StuperUser
fuente
1

Puedes decidir qué es importante para ti.

¿Es importante para usted que un Objeto sobreviva al viaje de ida y vuelta, y el formato exacto del cable no es realmente importante? ¿O es el formato de cable exacto una parte importante de la funcionalidad de su codificador y decodificador?

Si es lo primero, asegúrese de que los objetos sobrevivan al viaje de ida y vuelta. Si el codificador y el decodificador están rotos de manera exactamente complementaria, realmente no te importa.

Si es lo último, entonces debe probar que el formato del cable es el esperado para las entradas dadas. Esto significa probar el formato directamente o utilizar una implementación de referencia. Pero después de probar lo básico, puede obtener valor de las pruebas adicionales de ida y vuelta, que deberían ser más fáciles de escribir en volumen.

Bill Michell
fuente