Como você teste de unidade um codificador?

9

Eu tenho algo parecido com isto:

public byte[] EncodeMyObject(MyObject obj)

Eu tenho testado em unidade assim:

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

EDIT: As duas maneiras que eu vi propostas são:

1) Usando valores esperados codificados, como no exemplo acima.

2) Usando um decodificador para decodificar a matriz de bytes codificados e comparando os objetos de entrada / saída.

O problema que vejo no método 1 é que ele é muito quebradiço e requer muitos valores codificados.

O problema com o método 2 é que o teste do codificador depende do funcionamento correto do decodificador. Se o codificador / decodificador for quebrado igualmente (no mesmo local), os testes poderão produzir falsos positivos.

Essas podem muito bem ser as únicas maneiras de testar esse tipo de método. Se for esse o caso, tudo bem. Estou fazendo a pergunta para ver se existem estratégias melhores para esse tipo de teste. Não consigo revelar as partes internas do codificador específico em que estou trabalhando. Em geral, estou perguntando como você resolveria esse tipo de problema e não acho que os internos sejam importantes. Suponha que um determinado objeto de entrada sempre produza a mesma matriz de bytes de saída.

ConditionRacer
fonte
4
Como myObjectvai de myObjectpara { 0x01, 0x02, 0xFF }? Esse algoritmo pode ser dividido e testado? A razão pela qual pergunto é atualmente, parece que você tem um teste que prova que uma coisa mágica produz outra coisa mágica. Sua única confiança é que a única entrada produz a única saída. Se você pode quebrar o algoritmo, pode ganhar mais confiança no algoritmo e ser menos dependente de entradas e saídas mágicas.
Anthony Pegram
3
@ Codism E se o codificador e o decodificador forem quebrados no mesmo local?
ConditionRacer
2
Os testes estão, por definição, fazendo alguma coisa e verificando se você obteve os resultados esperados, que é o que o seu teste faz. Obviamente, você precisaria garantir testes suficientes para exercer todo o seu código e cobrir casos extremos e outras estranhezas.
Blrfl
11
@ Justin984, bem, agora estamos indo mais fundo. Eu não exporia esses internos privados como membros da API do codificador, certamente não. Eu os removia completamente do codificador. Ou melhor, o codificador delegaria para outro lugar, uma dependência . Se é uma batalha entre um método de monstro não testável ou um monte de classes auxiliares, eu escolho as classes auxiliares todas as vezes. Mas, novamente, estou fazendo inferências desinformadas ao seu código neste momento, porque não consigo vê-lo. Mas se você deseja ganhar confiança em seus testes, ter métodos menores fazendo menos coisas é uma maneira de chegar lá.
Anthony Pegram 13/02/2013
11
@ Justin984 Se a especificação mudar, você alterará a saída esperada no seu teste e ela falhará agora. Então, você altera a lógica do codificador para passar. Parece exatamente como o TDD deve funcionar e só falhará quando deveria. Não vejo como isso o torna quebradiço.
Daniel Kaplan

Respostas:

1

Você está em uma situação um pouco desagradável lá. Se você tivesse um formato estático no qual estava codificando, seu primeiro método seria o caminho a seguir. Se fosse apenas o seu próprio formato, e ninguém mais tivesse que decodificar, o segundo método seria o caminho a seguir. Mas você realmente não se encaixa em nenhuma dessas categorias.

O que eu faria é tentar dividir as coisas pelo nível de abstração.

Então, eu começaria com algo no nível de bits, que testaria algo como

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

Portanto, a idéia é que o bitwriter saiba como escrever os tipos mais primitivos de campos, como ints.

Tipos mais complexos seriam implementados usando e testados 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() )

Observe que isso evita qualquer conhecimento de como os bits reais são compactados. Isso foi testado pelo teste anterior e, para esse teste, vamos assumir que ele funciona.

Então, no próximo nível de abstração, terí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() )

então, novamente, não tentamos incluir o conhecimento de como as sequências de caracteres, datas ou números são realmente codificados. Neste teste, estamos interessados ​​apenas na codificação produzida por encodeObject.

O resultado final é que, se o formato das datas for alterado, você precisará corrigir os testes que realmente envolvem datas, mas todos os outros códigos e testes não se preocupam com a forma como as datas são realmente codificadas e depois de atualizar o código para criar Nesse trabalho, todos esses testes serão aprovados.

Winston Ewert
fonte
Eu gosto disso. Eu acho que é isso que alguns dos outros comentaristas estavam dizendo sobre dividi-lo em pedaços menores. Não evita completamente o problema quando as especificações mudam, mas melhora.
ConditionRacer
6

Depende. Se a codificação for algo completamente fixo, em que toda implementação deve criar exatamente a mesma saída, não faz sentido verificar nada além de verificar se as entradas de exemplo são mapeadas exatamente para as saídas esperadas. Esse é o teste mais óbvio e provavelmente também o mais fácil de escrever.

Se houver espaço de manobra com saídas alternativas, como no padrão MPEG (por exemplo, existem certos operadores que você pode aplicar à entrada, mas você pode negociar o esforço de codificação versus a qualidade da saída ou o espaço de armazenamento), é melhor aplicar o definiu a estratégia de decodificação para a saída e verifique se é a mesma da entrada - ou, se a codificação estiver com perdas, está razoavelmente próxima da entrada original. Isso é mais difícil de programar, mas protege você contra futuras melhorias que possam ser feitas no seu codificador.

Kilian Foth
fonte
2
Suponha que você use o decodificador e compare valores. E se o codificador e o decodificador forem quebrados no mesmo local? O codificador codifica incorretamente e o decodificador decodifica incorretamente, mas os objetos de entrada / saída estão corretos porque o processo foi realizado incorretamente duas vezes.
ConditionRacer
@ Justin984 em seguida, usar os chamados "vetores de teste", introduza sabe / pares de saída que pode ser usado precisamente para testar um codificador e decodificador
catraca aberração
@ratchet freak Isso me coloca de volta aos testes com os valores esperados. O que é bom, é o que estou fazendo atualmente, mas é um pouco frágil, então eu estava olhando para ver se há maneiras melhores.
ConditionRacer
11
Além de ler atentamente o padrão e criar um caso de teste para todas as regras, dificilmente há uma maneira de evitar que um codificador e um decodificador contenham o mesmo bug. Por exemplo, vamos supor que "ABC" deve ser traduzido para "xyz", mas o codificador não sabe disso e seu decodificador também não entenderia "xyz" se ele o encontrasse. As caixas de teste artesanais não contêm a sequência "ABC", porque o programador não estava ciente dessa regra e também um teste com seqüências aleatórias de codificação / decodificação passaria incorretamente porque o codificador e o decodificador ignoram o problema.
user281377
11
Para ajudar a detectar bugs que afetam decodificadores e codificadores criados por você devido a falta de conhecimento, faça um esforço para obter saídas de codificador de outros fornecedores; e também tente testar a saída do seu codificador nos decodificadores de terceiros. Não há uma alternativa em torno disso.
Rwong
3

Teste isso encode(decode(coded_value)) == coded_valuee decode(encode(value)) == value. Você pode fornecer uma entrada aleatória para os testes, se desejar.

Ainda é possível que o codificador e o decodificador sejam quebrados de maneiras complementares, mas isso parece bastante improvável, a menos que você tenha um entendimento conceitual do padrão de codificação. Fazer testes codificados e codificados (como você já está fazendo) deve se proteger contra isso.

Se você tiver acesso a outra implementação disso que funcione, pelo menos você poderá usá-la para ter certeza de que sua implementação é boa, mesmo que seja impossível usá-la nos testes de unidade.

Michael Shaw
fonte
Concordo que um erro complementar de codificador / decodificador é improvável em geral. No meu caso específico, o código para as classes de codificador / decodificador é gerado por outra ferramenta baseada nas regras de um banco de dados. Portanto, erros complementares acontecem ocasionalmente.
ConditionRacer
Como pode haver 'erros complementares'? Isso implica que existe uma especificação externa para a forma codificada e, portanto, um decodificador externo.
22613 kevin cline
Não entendo o uso da palavra externo. Mas há uma especificação de como os dados são codificados e também um decodificador. Um erro complementar é onde o codificador e o decodificador operam de maneira complementar, mas que se desvia da especificação. Eu tenho um exemplo nos comentários da pergunta original.
ConditionRacer
Se o codificador deveria implementar o ROT13, mas acidentalmente o ROT14 e o decodificador também, decodifique (codifique ('a')) == 'a', mas o codificador ainda está quebrado. Para coisas muito mais complicadas do que isso, provavelmente é muito menos provável que esse tipo de coisa aconteça, mas teoricamente poderia.
Michael Shaw
@ MichaelShaw, apenas um trivia, o codificador e o decodificador do ROT13 são os mesmos; ROT13 é o seu próprio inverso. Se você implementou o ROT14 por engano, decode(encode(char))não seria igual char(seria igual char+2).
21813 Tom Marthenal
2

Teste com os requisitos .

Se os requisitos forem apenas 'codificar para um fluxo de bytes que, quando decodificado, produz um objeto equivalente'., Basta testar o codificador decodificando. Se você estiver escrevendo o codificador e o decodificador, basta testá-los juntos. Eles não podem ter "erros de correspondência". Se eles trabalharem juntos, o teste será aprovado.

Se houver outros requisitos para o fluxo de dados, você precisará testá-los examinando os dados codificados.

Se o formato codificado for predefinido, você deverá verificar os dados codificados com relação ao resultado esperado, como fez, ou (melhor) obter um decodificador de referência confiável para fazer a verificação. O uso de um decodificador de referência elimina a possibilidade de você ter interpretado mal a especificação de formato.

Kevin Cline
fonte
1

Dependendo da estrutura e do paradigma de teste que você está usando, você ainda pode usar o padrão Dispor Lei de Disposição para isso, como você disse.

[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);
}

Você deve conhecer os requisitos EncodeMyObject()e pode usar esse padrão para testar contra cada um deles critérios válidos e inválidos, organizando cada um deles e codificando o resultado esperado para expected, da mesma forma para o decodificador.

Como o esperado é codificado, eles serão frágeis se você tiver uma mudança maciça.

Você pode automatizar com algo orientado a parâmetros (dê uma olhada no Pex ) ou, se estiver fazendo DDD ou BDD, dê uma olhada no gerkin / pepino .

StuperUser
fonte
1

Você decide o que é importante para você.

É importante para você que um Objeto sobreviva à ida e volta e o formato exato da ligação não seja realmente importante? Ou o formato exato do fio é uma parte importante da funcionalidade do seu codificador e decodificador?

Se for o primeiro, certifique-se de que os objetos sobrevivam à ida e volta. Se o codificador e o decodificador estiverem quebrados de maneiras exatamente complementares, você realmente não se importa.

Neste último caso, você precisa testar se o formato do fio é o esperado para as entradas fornecidas. Isso significa testar o formato diretamente ou usar uma implementação de referência. Mas, depois de testar o básico, você pode obter valor de testes adicionais de ida e volta, que devem ser mais fáceis de escrever em volume.

Bill Michell
fonte