Fortalecendo código com manipulação de exceção possivelmente inútil

12

É uma boa prática implementar manipulação de exceção inútil, caso outra parte do código não seja codificada corretamente?

Exemplo básico

Simples, então não perco todo mundo :).

Digamos que estou escrevendo um aplicativo que exibirá as informações de uma pessoa (nome, endereço etc.), os dados sendo extraídos de um banco de dados. Digamos que sou eu quem está codificando a parte da interface do usuário e que alguém está escrevendo o código de consulta do banco de dados.

Agora imagine que as especificações do seu aplicativo digam que, se as informações da pessoa estiverem incompletas (digamos que o nome esteja ausente no banco de dados), a pessoa que codifica a consulta deve lidar com isso retornando "NA" para o campo ausente.

E se a consulta estiver mal codificada e não tratar desse caso? E se o cara que escreveu a consulta manipular um resultado incompleto e quando você tenta exibir as informações, tudo falha, porque seu código não está preparado para exibir itens vazios?

Este exemplo é muito básico. Acredito que a maioria de vocês dirá "não é problema seu, você não é responsável por esta falha". Mas ainda é sua parte do código que está travando.

Outro exemplo

Digamos que agora sou eu quem está escrevendo a consulta. As especificações não dizem o mesmo que acima, mas o sujeito que está escrevendo a consulta "inserir" deve garantir que todos os campos estejam completos ao adicionar uma pessoa ao banco de dados para evitar a inserção de informações incompletas. Devo proteger minha consulta "select" para garantir que eu forneça informações completas ao usuário da interface do usuário?

As questões

E se as especificações não disserem explicitamente "esse cara é o responsável por lidar com essa situação"? E se uma terceira pessoa implementa outra consulta (semelhante à primeira, mas em outro banco de dados) e usa seu código de interface do usuário para exibi-la, mas não lida com esse caso em seu código?

Devo fazer o que é necessário para evitar uma possível falha, mesmo que eu não deva lidar com o caso ruim?

Não estou procurando uma resposta como "(s) ele é o responsável pelo acidente", como não estou resolvendo um conflito aqui, gostaria de saber, devo proteger meu código contra situações que não são de minha responsabilidade lidar? Aqui, um simples "se vazio fizer alguma coisa" seria suficiente.

Em geral, essa pergunta aborda o tratamento de exceções redundantes. Estou perguntando, porque quando trabalho sozinho em um projeto, posso codificar 2 a 3 vezes uma exceção semelhante, em funções sucessivas, "apenas no caso" de fazer algo errado e permitir que um caso ruim ocorra.

rdurand
fonte
4
Você está falando de "testes", mas, tanto quanto eu entendo o seu problema, você quer dizer "testes que são aplicados na produção", isso é melhor chamado de "validação" ou "tratamento de exceção".
Doc Brown
1
Sim, a palavra apropriada é "manipulação de exceção".
Rdurand
mudou a tag errada então
Doc Brown
Eu o indico ao DailyWTF - você tem certeza de que deseja fazer esse tipo de teste?
Gbjbaanb
@gbjbaanb: Se eu entendi o seu link corretamente, não é disso que estou falando. Não estou falando de "testes estúpidos", mas de duplicar o tratamento de exceções.
Rdurand

Respostas:

14

O que você está falando aqui é sobre limites de confiança . Você confia na fronteira entre seu aplicativo e o banco de dados? O banco de dados confia que os dados do aplicativo são sempre pré-validados?

Essa é uma decisão que deve ser tomada em todas as aplicações e não há respostas certas e erradas. Costumo errar ao chamar muitos limites de limites de confiança; outros desenvolvedores confiarão em APIs de terceiros para fazer o que você espera que eles façam, o tempo todo, o tempo todo.

pdr
fonte
5

O princípio da robustez "Seja conservador no que envia, seja liberal no que aceita" é o que deseja. É um bom princípio - EDIT: desde que a sua aplicação não oculte erros graves -, mas eu concordo com o @pdr que ele sempre depende da situação se você deve aplicá-lo ou não.

Doc Brown
fonte
Algumas pessoas pensam que o "princípio da robustez" é uma porcaria. O artigo dá um exemplo.
@ MattFenwick: obrigado por apontar isso, é um ponto válido, eu mudei minha resposta um pouco.
Doc Brown
2
Este é um artigo ainda melhor apontando os problemas com o "princípio robustez": joelonsoftware.com/items/2008/03/17.html
hakoja
1
@hakoja: honestamente, eu conheço bem este artigo, trata-se de problemas que você obtém quando começa a não seguir o princípio de robustez (como alguns caras do MS tentaram com versões mais recentes do IE). No entanto, isso fica um pouco longe da questão original.
Doc Brown
1
@ DocBrown: é exatamente por isso que você nunca deveria ter sido liberal no que aceita. Robustez não significa que você precisa aceitar tudo que é jogado contra você sem reclamar, apenas que você precisa aceitar tudo que é jogado contra você sem bater.
Marjan Venema
1

Depende do que você está testando; mas vamos supor que o escopo do seu teste seja apenas seu próprio código. Nesse caso, você deve testar:

  • O "caso feliz": alimente a entrada válida do aplicativo e verifique se ele produz saída correta.
  • Os casos de falha: alimente entradas inválidas do aplicativo e verifique se ele as manipula corretamente.

Para fazer isso, você não pode usar o componente do seu colega: em vez disso, use zombaria , ou seja, substitua o restante do aplicativo por módulos "falsos" que você pode controlar a partir da estrutura de teste. Como exatamente você faz isso depende da maneira como os módulos interagem; basta bastar chamar os métodos do seu módulo com argumentos codificados e pode se tornar tão complexo quanto escrever uma estrutura inteira que conecte as interfaces públicas dos outros módulos ao ambiente de teste.

Esse é apenas o caso de teste de unidade, no entanto. Você também deseja testes de integração, onde você testa todos os módulos em conjunto. Novamente, você quer testar o caso feliz e as falhas.

No seu caso "Exemplo básico", para testar seu código por unidade, escreva uma classe simulada que simule a camada do banco de dados. Sua classe simulada realmente não vai para o banco de dados: você apenas o pré-carrega com as entradas e saídas fixas esperadas. No pseudocódigo:

function test_ValidUser() {
    // set up mocking and fixtures
    userid = 23;
    db = new MockDB();
    db.fixedResult = { firstName: "John", lastName: "Doe" };
    db.expectedCall = { method: 'getUser', params: { userid: userid } };
    userController = new UserController(db);
    expectedResult = "John Doe";

    // run the actual test
    actualResult = userController.displayUserAsString(userid);

    // check assertions
    assertEquals(expectedResult, actualResult);
    db.assertExpectedCall();
}

E aqui está como você testaria os campos ausentes relatados corretamente :

function test_IncompleteUser() {
    // set up mocking and fixtures
    userid = 57;
    db = new MockDB();
    db.fixedResult = { firstName: "John", lastName: "NA" };
    db.expectedCall = { method: 'getUser', params: { userid: userid } };
    userController = new UserController(db);

    // let's say the user controller is specified to leave "NA" fields 
    // blank
    expectedResult = "John";

    // run the actual test
    actualResult = userController.displayUserAsString(userid);

    // check assertions
    assertEquals(expectedResult, actualResult);
    db.assertExpectedCall();
}

Agora as coisas se tornam interessantes. E se a classe DB real se comportar mal? Por exemplo, poderia lançar uma exceção por razões pouco claras. Não sabemos se existe, mas queremos que nosso próprio código lide com isso normalmente. Não tem problema, só precisamos fazer o nosso MockDB lançar uma exceção, por exemplo, adicionando um método como este:

class MockDB {
    // ... snip
    function getUser(userid) {
        if (this.fixedException) {
            throw this.fixedException;
        }
        else {
            return this.fixedResult;
        }
    }
}

E então nosso caso de teste é assim:

function test_MisbehavingUser() {
    // set up mocking and fixtures
    userid = 57;
    db = new MockDB();
    db.fixedException = new SQLException("You have an error in your SQL syntax");
    db.expectedCall = { method: 'getUser', params: { userid: userid } };
    userController = new UserController(db);

    // run the actual test
    try {
        userController.displayUserAsString(userid);
    }
    catch (DatabaseException ex) {
        // This is good: our userController has caught the raw exception
        // from the database layer and wrapped it in a DatabaseException.
        return TEST_PASSED;
    }
    catch (Exception ex) {
        // This is not good: we have an exception, but it's the wrong kind.
        testLog.log("Found the wrong exception: " + ex);
        return TEST_FAILED;
    }
    // This is bad, too: either our mocking class didn't throw even when it
    // should have, or our userController swallowed the exception and
    // discarded it
    testLog.log("Expected an exception to be thrown, but nothing happened.");
    return TEST_FAILED;
}

Estes são os seus testes de unidade. Para o teste de integração, você não usa a classe MockDB; em vez disso, você agrupa as duas classes reais. Você ainda precisa de acessórios; por exemplo, você deve inicializar o banco de dados de teste para um estado conhecido antes de executar o teste.

Agora, no que diz respeito às responsabilidades: seu código deve esperar que o restante da base de código seja implementado de acordo com a especificação, mas também deve estar preparado para lidar com as coisas normalmente quando o resto estragar. Você não é responsável por testar outro código que não seja o seu, mas é responsável por tornar seu código resiliente a código de mau comportamento do outro lado e também é responsável por testar a resiliência do seu código. É o que o terceiro teste acima faz.

tdammers
fonte
você leu os comentários abaixo da pergunta? O OP escreveu "testes", mas ele quis dizer isso no sentido de "verificações de validação" e / ou "tratamento de exceções"
Doc Brown
1
@ Tdammers: desculpe-me pelo mal-entendido, eu quis dizer de fato tratamento de exceções. De qualquer maneira, obrigado pela resposta completa, o último parágrafo é o que eu estava procurando.
Rdurand 24/05
1

Existem três princípios principais que tento codificar por:

  • SECO

  • BEIJO

  • YAGNI

A questão de tudo isso é que você corre o risco de escrever um código de validação duplicado em outro lugar. Se as regras de validação mudarem, elas precisarão ser atualizadas em vários locais.

Obviamente, em algum momento no futuro, você poderá reformular seu banco de dados (isso acontece); nesse caso, você poderá achar vantajoso ter o código em mais de um local. Mas ... você está codificando para algo que pode não acontecer.

Qualquer código adicional (mesmo que nunca seja alterado) está sobrecarregado, pois precisará ser gravado, lido, armazenado e testado.

Todas as alternativas acima são verdadeiras, seria uma negligência sua não fazer nenhuma validação. Para exibir um nome completo no aplicativo, você precisará de alguns dados básicos - mesmo que não valide os dados em si.

Robbie Dee
fonte
1

Nas palavras dos leigos.

Não existe algo como "o banco de dados" ou "o aplicativo" .

  1. Um banco de dados pode ser usado por mais de um aplicativo.
  2. Um aplicativo pode usar mais de um banco de dados.
  3. O modelo de banco de dados deve impor a integridade dos dados, que inclui gerar um erro quando um campo obrigatório não for incluído em uma operação de inserção, a menos que um valor padrão seja definido na definição da tabela. Isso deve ser feito mesmo se você inserir a linha diretamente no banco de dados ignorando o aplicativo. Deixe o sistema de banco de dados fazer isso por você.
  4. Os bancos de dados devem proteger a integridade dos dados e gerar erros .
  5. A lógica de negócios deve capturar esses erros e lançar exceções na camada de apresentação.
  6. A camada de apresentação deve validar a entrada, manipular exceções ou mostrar um hamster triste ao usuário.

Novamente:

  • Banco de Dados-> lançar erros
  • Business Logic-> captura erros e lança exceções
  • Camada de apresentação-> validar, lançar exceções ou mostrar mensagens tristes.
Tulains Córdova
fonte