I'm practicing TDD simulating a central alarm. Alarm centrals work connected to sensors that detect intrusion (opening a door or window, or moving inside a room, for example). They have a fixed number of logical partitions (representing different locations to be protected) and slots for sensors. At the time of installation the sensors are associated with the respective partitions. So the interface of my alarm center includes the following methods (note: "arm" a partition means to protect it, that is, the control panel will trigger the alarm if a partition sensor is triggered, and "disarm" means unprotect, ie ignoring the drive of the partition sensors):
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;
...
}
Here are some of the possible unit tests:
@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);
}
The armarParticao()
method delegates the arming to a class named Particao
. Until then, nothing too much:
@Override
public boolean armarParticao(int numero) throws IllegalArgumentException, IllegalStateException {
Particao particao = particoes.get(numero);
if (particao == null) {
throw new IllegalArgumentException();
}
return particao.armar();
}
In class Particao
I created a public method armar()
that can cast IllegalStateException
for two distinct reasons, that is, two different situations where the object is in an undue state. One is when the partition is already armed; another is when the partition does not have sensors associated with it (that is, it can not detect intrusion):
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;
}
...
}
From the point of view of unit testing and TDD, is this a bad thing? A colleague said yes, because if the method throws a IllegalStateException
I'll never be sure if it's because of one reason or because of another. The thrown exception may mask a different situation than the one I'm trying to test.
But I'm not sure if that's ever a problem. Maybe it is if I do not have tests for all possible situations (including the two situations where% of% can be posted). But if I do, maybe this overlap will not be problematic. In practice, when writing unit tests or doing TDD, people have the habit of avoiding this (throwing the same exception in one method in two different situations)?
Second question: If I have to use distinct exceptions, should I create custom exceptions (new classes) for each of the situations, or is this a more or less arbitrary decision? The exception (s) created should be subclasses of IllegalStateException
?