Unit-testing – How to simplify the complex stateful classes and their testing

complexityunit testing

I am in a distributed system project written in java where we have some classes which corresponds to very complex real-world-business objects. These objects have a lot of methods corresponding to actions that the user (or some other agent) may apply to that objects. As a result, these classes became very complex.

The system general architecture approach has lead to a lot of behaviors concentrated on a few classes and a lot of possible interaction scenarios.

As an example and to keep things easy and clear, let's say that Robot and Car were classes in my project.

So, in the Robot class I would have a lot of methods in the following pattern:

  • sleep(); isSleepAvaliable();
  • awake(); isAwakeAvaliable();
  • walk(Direction); isWalkAvaliable();
  • shoot(Direction); isShootAvaliable();
  • turnOnAlert(); isTurnOnAlertAvailable();
  • turnOffAlert(); isTurnOffAlertAvailable();
  • recharge(); isRechargeAvailable();
  • powerOff(); isPowerOffAvailable();
  • stepInCar(Car); isStepInCarAvailable();
  • stepOutCar(Car); isStepOutCarAvailable();
  • selfDestruct(); isSelfDestructAvailable();
  • die(); isDieAvailable();
  • isAlive(); isAwake(); isAlertOn(); getBatteryLevel(); getCurrentRidingCar(); getAmmo();

In the Car class, it would be similar:

  • turnOn(); isTurnOnAvaliable();
  • turnOff(); isTurnOffAvaliable();
  • walk(Direction); isWalkAvaliable();
  • refuel(); isRefuelAvailable();
  • selfDestruct(); isSelfDestructAvailable();
  • crash(); isCrashAvailable();
  • isOperational(); isOn(); getFuelLevel(); getCurrentPassenger();

Each of these (Robot and Car) is implemented as a state machine, where some actions are possible in some states and some aren't. The actions changes the object's state. The actions methods throws IllegalStateException when called in an invalid state and the isXXXAvailable() methods tell if the action is possible at the time. Although some are easily deducible from the state (e.g, in the sleeping state, awake is available), some aren't (to shoot, it must be awake, alive, having ammo and not riding a car).

Further, the interactions between the objects are complex too. E.g, the Car can only hold one Robot passenger, so if another one try to enter, an exception should be thrown; If the car crashes, the passenger should die; If the robot is dead inside a vehicle, he can't step out, even if the Car itself is ok; If the Robot is inside a Car, he can't enter another one before stepping out; etc.

The result of this, is as I already said, these classes became really complex. To make things worse, there is hundreds possible scenarios when the Robot and the Car interacts. Further, much of that logic does needs to access remote data in other systems. The result is that unit-testing it became very hard and we have a lot of testing problems, one causing the another in a vicious circle:

  • The testcases setups are very complex, because they need to create a significantly complex world to exercize.
  • The number of tests is huge.
  • The test suite takes some hours to run.
  • Our test coverage is very low.
  • The testing code tends to be written weeks or months later than the code that they test, or never at all.
  • A lot of tests are broken too, mainly because the requirements of the tested code changed.
  • Some scenarios are so complex, that they fail on timeout during the setup (we configured a timeout in each test, in the worst cases 2 minutes long and even this time long they timeouts, we ensured that it is not an infinite loop).
  • Bugs regularly slips into production environment.

That Robot and Car scenario is a gross over-simplification of what we have in reality. Clearly, this situation is not manageable. So, I am asking help and suggestions to: 1, Reduce the classes complexity; 2. Simplify the interactions scenarios between my objects; 3. Reduce the test time and the amout of code to be tested.

EDIT:
I think I was not clear about the state machines. the Robot is itself a state machine, with states "sleeping", "awake", "recharging", "dead", etc. The Car is another state machine.

EDIT 2:
In the case that you are curious about what my system actually is, the classes that interact are things like Server, IPAddress, Disk, Backup, User, SoftwareLicense, etc. The Robot and Car scenario is just a case that I found that would be simple enough to explain my problem.

Best Answer

The State design pattern might be of use, if you're not already using it.

The core idea is that you create an inner class for each distinct state - so to continue your example, SleepingRobot, AwakeRobot, RechargingRobot and DeadRobot would all be classes, implementing a common interface.

Methods on the Robot class (like sleep() and isSleepAvaliable()) have simple implementations that delegate to the current inner class.

State changes are implemented by swapping out the current inner class with a different one.

The advantage with this approach is that each of the state classes is dramatically simpler (as it represents just one possible state), and can be independently tested. Depending on your implementation language (not specified), you may still be constrained to having everything in the same file, or you may be able to split things out into smaller source files.

Related Topic