Eu li sobre esse problema: O bug de programação custa ao Citigroup US $ 7 milhões após transações legítimas confundidas com dados de teste por 15 anos .
Quando o sistema foi introduzido em meados da década de 1990, o código do programa filtrou todas as transações que receberam códigos de ramificação de três dígitos de 089 a 100 e usaram esses prefixos para fins de teste.
Mas em 1998, a empresa começou a usar códigos alfanuméricos de filiais à medida que expandia seus negócios. Entre eles estavam os códigos 10B, 10C e assim por diante, que o sistema tratava como estando dentro do intervalo excluído e, portanto, suas transações foram removidas de quaisquer relatórios enviados à SEC.
(Acho que isso ilustra que o uso de um indicador de dados não explícito é ... sub-ideal. Teria sido muito melhor preencher e usar uma Branch.IsLive
propriedade semanticamente explícita .)
Além disso, minha primeira reação foi "testes de unidade teriam ajudado aqui" ... mas eles ajudariam?
Li recentemente Por que a maioria dos testes de unidade é desperdiçada com interesse e, portanto, minha pergunta é: como seriam os testes de unidade que teriam falhado na introdução de códigos alfanuméricos de ramificação?
fonte
Respostas:
Você está realmente perguntando "os testes de unidade teriam ajudado aqui?", Ou você está perguntando "algum tipo de teste poderia ter ajudado aqui?".
A forma mais óbvia de teste que teria ajudado é uma asserção de pré-condição no próprio código, de que um identificador de ramificação consiste apenas de dígitos (supondo que essa seja a suposição invocada pelo codificador ao escrever o código).
Isso pode ter falhado em algum tipo de teste de integração e, assim que os novos IDs de ramificação alfanuméricos são introduzidos, a asserção explode. Mas isso não é um teste de unidade.
Como alternativa, pode haver um teste de integração do procedimento que gera o relatório da SEC. Esse teste garante que todo identificador de filial real relate suas transações (e, portanto, requer entrada do mundo real, uma lista de todos os identificadores de filial em uso). Portanto, isso também não é um teste de unidade.
Não consigo ver nenhuma definição ou documentação das interfaces envolvidas, mas pode ser que os testes de unidade não tenham detectado o erro porque a unidade não estava com defeito . Se a unidade puder assumir que os identificadores de ramificação consistem apenas de dígitos, e os desenvolvedores nunca tomarem uma decisão sobre o que o código deve fazer caso não o faça, então eles não devemescreva um teste de unidade para impor um comportamento específico no caso de identificadores que não sejam dígitos, porque o teste rejeitaria uma implementação hipotética válida da unidade que manipulava corretamente os identificadores alfanuméricos de ramificação e você geralmente não deseja escrever um teste de unidade que impeça a validade futuras implementações e extensões. Ou talvez um documento escrito há 40 anos definido implicitamente (por meio de um intervalo lexicográfico no EBCDIC bruto, em vez de uma regra de classificação mais amigável ao ser humano) que 10B é um identificador de teste porque, na verdade, cai entre 089 e 100. Mas então Há 15 anos, alguém decidiu usá-lo como um identificador real; portanto, a "falha" não está na unidade que implementa corretamente a definição original: está no processo que não percebeu que 10B é definido como um identificador de teste e, portanto, não deve ser atribuído a uma ramificação. O mesmo aconteceria no ASCII se você definisse 089 - 100 como um intervalo de teste e, em seguida, introduzisse um identificador 10 $ ou 1,0. Acontece que no EBCDIC os dígitos vêm depois das letras.
Um teste de unidade (ou possivelmente um teste funcional) que é concebívelpode ter salvado o dia, é um teste da unidade que gera ou valida novos identificadores de filial. Esse teste afirmaria que os identificadores devem conter apenas dígitos e seria gravado para permitir que os usuários dos identificadores de ramificações assumissem o mesmo. Ou talvez exista uma unidade em algum lugar que importe identificadores de ramificação reais, mas nunca os veja, e que possa ser testada por unidade para garantir que rejeite todos os identificadores de teste (se os identificadores tiverem apenas três caracteres, poderemos enumerá-los todos e comparar o comportamento de o validador ao do filtro de teste para garantir que eles correspondam, o que lida com a objeção usual aos testes pontuais). Então, quando alguém mudasse as regras, o teste de unidade teria falhado, pois contradiz o novo comportamento exigido.
Como o teste foi realizado por um bom motivo, o ponto em que você precisa removê-lo devido a requisitos de negócios alterados torna-se uma oportunidade para alguém receber o trabalho ", encontre todos os lugares no código que se baseiam no comportamento que queremos mudança". É claro que isso é difícil e, portanto, pouco confiável, de modo algum garantiria salvar o dia. Mas se você capturar suas suposições em testes das unidades das quais você está assumindo propriedades, então você se deu uma chance e, portanto, o esforço não é totalmente desperdiçado.
Concordo, é claro, que se a unidade não tivesse sido definida em primeiro lugar com uma entrada "de formato engraçado", não haveria nada a ser testado. As divisões de espaço de nomes complicados podem ser difíceis de testar adequadamente, porque a dificuldade não está em implementar sua definição engraçada, mas em garantir que todos entendam e respeitem sua definição engraçada. Essa não é uma propriedade local de uma unidade de código. Além disso, alterar algum tipo de dados de "uma sequência de dígitos" para "uma sequência alfanumérica" é semelhante a fazer com que um programa baseado em ASCII manipule o Unicode: não será simples se o seu código estiver fortemente associado à definição original e quando o tipo de dados é fundamental para o que o programa faz, geralmente é fortemente associado.
Se às vezes seus testes de unidade falharem (enquanto você está refatorando, por exemplo) e, ao fazer isso, fornecer informações úteis (sua alteração está errada, por exemplo), o esforço não foi desperdiçado. O que eles não fazem é testar se o seu sistema funciona. Portanto, se você estiver escrevendo testes de unidade, em vez de ter testes funcionais e de integração, poderá usar seu tempo de maneira otimizada.
fonte
Os testes de unidade poderiam ter detectado que os códigos de ramificação 10B e 10C foram classificados incorretamente como "ramificações de teste", mas acho improvável que os testes para essa classificação de ramificação tenham sido extensos o suficiente para detectar esse erro.
Por outro lado, as verificações pontuais dos relatórios gerados poderiam ter revelado que 10B e 10C ramificados estavam constantemente ausentes dos relatórios muito antes dos 15 anos em que o bug agora podia permanecer presente.
Finalmente, esta é uma boa ilustração do motivo pelo qual é uma má idéia misturar dados de teste com os dados reais de produção em um banco de dados. Se eles tivessem usado um banco de dados separado que contém os dados de teste, não haveria necessidade de filtrar isso dos relatórios oficiais e seria impossível filtrar demais.
fonte
O software tinha que lidar com certas regras de negócios. Se houvesse testes de unidade, os testes de unidade teriam verificado se o software tratava as regras de negócios corretamente.
As regras de negócios foram alteradas.
Aparentemente, ninguém percebeu que as regras de negócios haviam mudado e ninguém mudou o software para aplicar as novas regras de negócios. Se houvesse testes de unidade, esses testes de unidade teriam que ser alterados, mas ninguém o faria porque ninguém percebeu que as regras de negócios haviam mudado.
Portanto, não, testes de unidade não teriam percebido isso.
A exceção seria se os testes de unidade e o software tivessem sido criados por equipes independentes, e a equipe que fazia os testes de unidade alterasse os testes para aplicar as novas regras de negócios. Então os testes de unidade teriam falhado, o que, esperançosamente, teria resultado em uma alteração do software.
Obviamente, no mesmo caso, se apenas o software fosse alterado e não os testes de unidade, os testes de unidade também falhariam. Sempre que um teste de unidade falha, isso não significa que o software está errado, significa que o software ou o teste de unidade (às vezes os dois) estão errados.
fonte
Não. Esse é um dos grandes problemas dos testes de unidade: eles levam você a uma falsa sensação de segurança.
Se todos os seus testes forem aprovados, isso não significa que seu sistema esteja funcionando corretamente; isso significa que todos os seus testes estão passando . Isso significa que as partes do seu design nas quais você pensou conscientemente e escreveu testes estão funcionando como você pensava que funcionaria conscientemente, o que realmente não é tão importante assim: essas eram as coisas nas quais você estava prestando muita atenção então é muito provável que você acertou! Mas não serve para capturar casos em que você nunca pensou, como este, porque você nunca pensou em escrever um teste para eles. (E, se você tivesse, estaria ciente de que isso significava que alterações de código eram necessárias e você as teria alterado.)
fonte
Não, não necessariamente.
O requisito original era usar códigos de ramificação numéricos; portanto, um teste de unidade teria sido produzido para um componente que aceitasse vários códigos e rejeitasse qualquer 10B. O sistema teria sido passado como funcionando (como estava).
Em seguida, o requisito teria mudado e os códigos atualizados, mas isso significaria que o código de teste de unidade que forneceu os dados incorretos (que agora são dados válidos) precisaria ser modificado.
Agora assumimos que, as pessoas que gerenciam o sistema saberiam que esse era o caso e alterariam o teste de unidade para lidar com os novos códigos ... mas, se soubessem que isso estava ocorrendo, também teriam sabido alterar o código que tratava desses códigos. códigos de qualquer maneira .. e eles não fizeram isso. Um teste de unidade que originalmente rejeitasse o código 10B teria dito felizmente "está tudo bem aqui" quando executado, se você não soubesse atualizar esse teste.
O teste de unidade é bom para o desenvolvimento original, mas não para o sistema, especialmente 15 anos após os requisitos terem sido esquecidos.
O que eles precisam nesse tipo de situação é um teste de integração de ponta a ponta. Um onde você pode passar os dados que espera trabalhar e ver se funcionam. Alguém teria notado que seus novos dados de entrada não produziram um relatório e depois investigaram mais.
fonte
O teste de tipo (o processo de testar invariantes usando dados válidos gerados aleatoriamente, como exemplificado pela biblioteca de testes Haskell QuickCheck e várias portas / alternativas inspiradas por ele em outros idiomas) pode ter percebido esse problema, o teste de unidade quase certamente não teria acontecido. .
Isso ocorre porque quando as regras para a validade dos códigos de ramificação foram atualizadas, é improvável que alguém tenha pensado em testar esses intervalos específicos para garantir que funcionassem corretamente.
No entanto, se o teste de tipo estivesse em uso, alguém deveria, no momento em que o sistema original foi implementado, escrever um par de propriedades, uma para verificar se os códigos específicos para ramificações de teste foram tratadas como dados de teste e outra para verificar se nenhum outro código foram ... quando a definição do tipo de dados para o código da ramificação foi atualizada (o que seria necessário para permitir testar se alguma das alterações no código da ramificação de dígito para numérico funcionava), esse teste começaria a testar valores em o novo intervalo e provavelmente teria identificado a falha.
Obviamente, o QuickCheck foi desenvolvido pela primeira vez em 1999, por isso já era tarde demais para entender esse problema.
fonte
Eu realmente duvido que o teste de unidade faria diferença para esse problema. Parece uma daquelas situações de visão de túnel porque a funcionalidade foi alterada para oferecer suporte a novos códigos de ramificação, mas isso não foi realizado em todas as áreas do sistema.
Usamos testes de unidade para projetar uma classe. É necessário executar novamente um teste de unidade apenas se o design tiver sido alterado. Se uma unidade específica não for alterada, os testes de unidade inalterados retornarão os mesmos resultados de antes. Os testes de unidade não mostrarão os impactos das alterações em outras unidades (caso contrário, você não está escrevendo testes de unidade).
Você só pode detectar razoavelmente esse problema via:
Não ter testes de ponta a ponta suficientes é mais preocupante. Você não pode confiar no teste de unidade como seu teste SOMENTE ou PRINCIPAL para alterações do sistema. Parece que só foi necessário que alguém gerasse um relatório nos formatos de código de filial recém-suportados.
fonte
Uma afirmação incorporada ao tempo de execução pode ter ajudado; por exemplo:
bool isTestOnly(string branchCode) { ... }
Veja também:
fonte
A conclusão disso é falhar rapidamente .
Não temos o código, nem temos muitos exemplos de prefixos que são ou não são prefixos de ramificação de teste de acordo com o código. Tudo o que temos é o seguinte:
O fato de o código permitir números e seqüências de caracteres é mais do que um pouco estranho. Obviamente, 10B e 10C podem ser considerados números hexadecimais, mas se todos os prefixos forem tratados como números hexadecimais, 10B e 10C ficarão fora do intervalo de teste e serão tratados como ramificações reais.
Isso provavelmente significa que o prefixo é armazenado como uma sequência, mas tratado como um número em alguns casos. Aqui está o código mais simples que consigo pensar que replica esse comportamento (usando C # para fins ilustrativos):
Em inglês, se a sequência for um número e estiver entre 89 e 100, é um teste. Se não é um número, é um teste. Caso contrário, não é um teste.
Se o código seguir esse padrão, nenhum teste de unidade detectaria isso no momento em que o código foi implantado. Aqui estão alguns exemplos de testes de unidade:
O teste de unidade mostra que "10B" deve ser tratado como um ramo de teste. O usuário @ gnasher729 acima diz que as regras de negócios mudaram e é isso que a última afirmação acima mostra. Em algum momento, essa afirmação deveria ter mudado para uma
isFalse
, mas isso não aconteceu. Os testes de unidade são executados no tempo de desenvolvimento e construção, mas depois não há nenhum momento.Qual é a lição aqui? O código precisa de alguma maneira de sinalizar que recebeu entrada inesperada. Aqui está uma maneira alternativa de escrever esse código que enfatiza que ele espera que o prefixo seja um número:
Para quem não conhece C #, o valor retornado indica se o código foi ou não capaz de analisar um prefixo da string especificada. Se o valor de retorno for verdadeiro, o código de chamada pode usar a variável isTest out para verificar se o prefixo da ramificação é um prefixo de teste. Se o valor de retorno for falso, o código de chamada deve relatar que o prefixo fornecido não é esperado e a variável isTest out não tem sentido e deve ser ignorada.
Se você concorda com as exceções, pode fazer o seguinte:
Essa alternativa é mais direta. Nesse caso, o código de chamada precisa capturar a exceção. Em ambos os casos, o código deve ter alguma maneira de relatar ao chamador que ele não esperava um strPrefix que não pudesse ser convertido em um número inteiro. Dessa forma, o código falha rapidamente e o banco pode encontrar rapidamente o problema sem o embaraço da SEC.
fonte
Tantas respostas e nem mesmo uma citação de Dijkstra:
Portanto, depende. Se o código foi testado corretamente, provavelmente esse bug não existiria.
fonte
Eu acho que um teste de unidade aqui teria garantido que o problema nunca existisse.
Considere, você escreveu a
bool IsTestData(string branchCode)
função.O primeiro teste de unidade que você escreve deve ser para cadeia nula e vazia. Em seguida, para cadeias de comprimento incorretas e depois para cadeias não inteiras.
Para fazer todos esses testes passarem, você precisará adicionar a verificação de parâmetros à função.
Mesmo se você testar apenas os dados 'bons' 001 -> 999 sem pensar na possibilidade de 10A, a verificação de parâmetros forçará a reescrever a função quando você começar a usar alfanuméricos para evitar as exceções que serão lançadas
fonte
IsValidBranchCode
função-para executar esta verificação? E essa função provavelmente teria sido alterada sem a necessidade de modificar oIsTestData
? Portanto, se você estiver testando apenas 'bons dados', o teste não teria ajudado. O teste de caso de borda teria que incluir algum código de ramificação agora válido (e não simplesmente alguns ainda inválidos) para começar a falhar.