É correto um método lançar a mesma exceção por dois motivos diferentes? – java exceção engenharia-de-software

Pergunta:


Estou praticando TDD simulando uma central de alarme. Centrais de alarme funcionam conectadas a sensores que detectam intrusão (abertura de uma porta ou janela, ou movimento dentro de uma sala, por exemplo). Elas possuem um número fixo de partições lógicas (representando diferentes locais a serem protegidos) e de slots para sensores. No ato da instalação os sensores são associados às respectivas partições. Então a interface da minha central de alarme inclui os seguintes métodos (obs.: “armar” uma partição significa protegê-la, isto é, a central irá disparar o alarme se um sensor da partição for acionado, e “desarmar” significa desproteger, isto é, ignorar o acionamento dos sensores da partição):

public interface InterfaceDeCentralDeAlarme {
    boolean armarParticao(int numero) throws IllegalArgumentException, IllegalStateException;
    boolean desarmarParticao(int numero) throws IllegalArgumentException, IllegalStateException;
    void associarSensorAParticao(int sensor, int particao) throws IllegalArgumentException, IllegalStateException;
    void desassociarSensorDeParticao(int sensor, int particao) throws IllegalArgumentException, IllegalStateException;
    ...
}

Aqui estão alguns dos possíveis testes unitários:

@Test(expected=IllegalStateException.class)
public void particaoSemSensoresAssociadosNaoPodeArmar() {
    InterfaceDeCentralDeAlarme central = criarCentralDeAlarme();
    central.armarParticao(1);
}

@Test(expected=IllegalStateException.class)
public void particaoJaArmadaNaoPodeArmar() {
    InterfaceDeCentralDeAlarme central = criarCentralDeAlarme();
    central.associarSensorAParticao(1, 1);
    central.fecharSensor(1);
    Assert.assertTrue(central.armarParticao(1));
    central.armarParticao(1);
}

O método armarParticao() delega o arme para uma classe chamada Particao. Até aí, nada de mais:

@Override
public boolean armarParticao(int numero) throws IllegalArgumentException, IllegalStateException {
    Particao particao = particoes.get(numero);
    if (particao == null) {
        throw new IllegalArgumentException();
    }

    return particao.armar();
}

Na classe Particao criei um método público armar() que pode lançar IllegalStateException por dois motivos distintos, isto é, duas situações distintas em que o objeto se encontra num estado indevido. Uma é quando a partição já está armada; outra é quando a partição não possui sensores associados a ela (ou seja, não tem como detectar intrusão):

public class Particao {

    private final Map<Integer, Sensor> sensores = new HashMap<>();
    private boolean armada = false;

    public boolean armar() throws IllegalStateException {
        if (armada) {
            throw new IllegalStateException("Partição já se encontra armada");
        }

        if (sensores.isEmpty()) {
            throw new IllegalStateException("Partição não possui sensor associado");
        }

        for (Sensor sensor : sensores.values()) {
            if (sensor.isAberto() && false == sensor.isInibido()) {
                return false;
            }
        }

        this.armada = true;
        return true;
    }

    ...
}

Do ponto de vista dos testes unitários e do TDD, isso é ruim? Um colega disse que sim, pois se o método lançar uma IllegalStateException eu nunca vou ter certeza se é por causa de um motivo ou por causa do outro. A exceção lançada pode mascarar uma situação diferente da que estou tentando testar.

Porém, não tenho certeza se isso chega a ser um problema. Talvez seja se eu não tiver testes para todas as situações possíveis (incluindo as duas situações em que IllegalStateException pode ser lançada). Mas se eu tiver, talvez esse overlap não chegue a ser problemático. Na prática, ao se escrever testes unitários ou fazendo TDD as pessoas têm o costume de evitar isso (lançar a mesma exceção num método em duas situações diferentes)?

Segunda pergunta: se eu tiver que usar exceções distintas, convém criar exceções customizadas (novas classes) para cada uma das situações, ou essa é uma decisão mais ou menos arbitrária? A(s) exceção(ões) criada(s) devem ser subclasses de IllegalStateException?

Autor da pergunta Piovezan

Comunidade

A pergunta foi modificada depois da resposta

Tudo em engenharia precisa ser pensado para o que está fazendo. Pra variar a resposta é um enorme DEPENDE.

A primeira coisa é se perguntar se realmente fará diferença ter situações diferentes. Muitas vezes só precisa saber que a exceção ocorreu, não precisa saber a situação exata, então pode conviver com isto. Se for o caso, pode ser, mas não necessariamente, que poderia reescrever isto:

 if (condicao1 || condicao2) {
     throw new IllegalStateException("Falhou devido à condição");
 }

O teste precisa ser adequado

Se não pode facilitar sua vida, uma das soluções é fazer o teste identificar os detalhes da exceção, se isso é importante.

Exemplo:

try {
    executarAcao();
} catch (Exception e) {
    assertThat(e).isInstanceOf(IllegalStateException.class)
                 .hasMessage("Falhou devido à condição 1");
}

O Junit tem o expectMessage() para isso. Acredito que a maioria dos bons frameworks possuem facilidades semelhantes.

Também pode usar anotação expected=IllegalStateException.class, message = "Falhou devido à condição 1".

É normal que um método lance mais de uma exceção igual e precisa ser testado, por isso há facilidades para testar. Se o teste precisa fazer isso, faça e teste apropriadamente.

O método está errado

Outra solução é lançar exceções diferentes mais personalizadas.

Há quem diga que isso sempre deveria ser feito, que exceções genéricas não são boas. Outros acham exagero. Depende do caso. Se for mudar um pequeno detalhe, pode ser exagero mesmo, mas se for coisa pouco relacionada, então pode ser o caso. Pode estar usando uma exceção genérica por “preguiça”. Lembre que tem gente que só lança Exception, aí o problema é outro. Lançar IllegalStateException pode cair no mesmo problema, mesmo não sendo tão aparente. Também pode ser o certo para o caso.

Se elas devem ser subclasses ou não, depende do caso. Esse pode ser um que deve, pelo que aparenta, mas é claro que é um exemplo bem hipotético. Eu não diria que é uma decisão arbitrária, é uma decisão individual.

Note que o problema aí é de design do método*. Se é isso, resolva este problema. A solução anterior é resolver o design do teste, que deve ser o preferido se o seu método está correto.

Depois da edição dá para perceber que o método tem problemas diferentes. No mínimo um deles não é um estado ilegal, é só um estado que impede a operação. O que remete que a exceção está sendo usada como regra de negócio (entendo que seja comum em Java fazer isso, mas eu iria por outro caminho). A outra pode, mas não necessariamente, existir por uma falha da classe, talvez não deveria existir o objeto com estado ilegal. Se tudo estiver certo, as exceções são completamente diferentes. O erro não é dificultar o teste, é especificação errada. Mas posso ter interpretado errado.

Acho um absurdo as pessoas mudarem seu código principal para atender uma demanda de teste. Você deve permitir o teste, eventualmente até facilitar, mas nunca ao custo da responsabilidade principal do seu código. Tem quem discorde, mas eu acho um erro programar para o teste. Fazer algo testável, ok, mas o código deve ser feito para atender a especificação. Se o TDD é a especificação, ele está errado.

Conclusão

Até onde eu sei as pessoas não evitam lançar a mesma exceção não, se o método precisa, deve fazer. E vejo como algo bem comum. Não sei se tem outras soluções melhores.

O exemplo da pergunta mostra como TDD é um treco complicado. Ele funciona bem quando: (a) o problema é amplamente conhecido e possui especificação muito boa (raro); (b) um gênio da arquitetura o fez (nunca conheci um).

Teve a curiosidade de perguntar para o colega qual é a solução? Talvez ele não saiba do que está falando. Mesmo acertando. Lembre-se que muita gente lê sobre algo ser “boa ou má prática” em algum lugar, repete o que “viu”, mas não entende o problema. Obviamente não sei se é o caso.

Fonte

Related Posts:

Qual a diferença entre AppCompatActivity e Activity? – android android-activity
Pergunta: Qual a diferença da AppCompatActivity para Activity ? A partir de qual versão a AppCompatActivity foi adicionada ao Android? Autor da pergunta Luhhh A diferença reside ...
Como abreviar palavras em PHP? – php string
Pergunta: Possuo informações comuns como nome de pessoas e endereços, e preciso que elas contenham no máximo 30 caracteres sem cortar palavras. Exemplo: 'Avenida Natalino João Brescansin' ...
Qual é a finalidade de um parêntese vazio numa declaração Lambda? – c# expressões-lambda característica-linguagem
Pergunta: Criei um exemplo de uma declaração Lambda sem argumentos, entretanto, estou com duvidas referente a omissão do parêntese vazio () na declaração. Veja o exemplo: class ...
Boas práticas para URI em API RESTful – api rest restful
Pergunta: Estou com dúvida em relação às URIs de alguns recursos da api que estou desenvolvendo. Tenho os recursos projetos e atividades com relação 1-N, ...
Dúvidas sobre a integração do MySQL com Java – java mysql netbeans
Pergunta: Estou criando um sistema no NetBeans, utilizando a linguagem Java e o banco de dados MySQL. Escrevi o seguinte código para realizar a conexão ...
Qual é a finalidade da pasta Model do framework Inphinit? – php inphinit
Pergunta: No Inphinit micro-framework existe a pasta Model que fica dentro da pasta application, e nela é onde ficam as classes, mas eu estou muito ...
Uso do ‘@’ em variáveis – javascript typescript coffeescript
Pergunta: Vejo em algumas linguagens que compilam para javascript, como TypeScript e CoffeeScript, o uso do @ em variáveis, como também, casos em que o ...
Qual tamanho máximo um arquivo JSON pode ter? – json arquivo
Pergunta: Vou dar um exemplo para conseguir explicar minha duvida: Preciso recuperar informação de imagens vindas de uma API, esse banco de imagens me retorna JSON's ...
O que é Teste de Regressão? – terminologia engenharia-de-software testes
Pergunta: Na matéria de Teste de Software o professor abordou um termo chamado Teste de Regressão, isto dentro da disciplina de teste de software. Sendo ...
O que é um construtor da linguagem? – php característica-linguagem
Pergunta: Em PHP, já li e ouvi várias vezes a respeito dos Construtores da Linguagem. Os casos que sempre ouvi falar deles foi em casos ...
Função intrínseca para converter numérico para string – cobol
Pergunta: Estou a tentar saber se existe alguma função intrínseca do COBOL para converter um data numérico para string sem precisar usar a cláusula REDEFINES: ( ...
Porque usar implements? – java android
Pergunta: Qual a diferença entre usar btn.setOnClickListener(new OnClickListener() { e public class MainActivity extends Activity implements OnClickListener{ Estive fazendo um curso de Android e meu professor falou que ...
O que é XHTML e quando deve ser usado? – html xml xhtml
Pergunta: O que eu sei é que o XHTML precisa ser XML válido. Isso implica, por exemplo, que todas as tags precisam ser fechadas. Por ...
Uma placa aceleradora de vídeo pode melhorar o desempenho não-gráfico? [fechada] – desempenho
Pergunta: Para desenvolver em Ruby on Rails, eu utilizo aqui uma máquina virtual do VirtualBox com Ubuntu Server 14.04 sem interface gráfica instalada. Recentemente descobri uma ...
Concat() VS Union() – c# .net
Pergunta: Qual a diferença entre Concat() e Union() ? Quando usar Concat() e quando usar Union() ? Somente pode ser usado em list ? ...

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *