Java – Is it feasible and useful to auto-generate some code of unit tests

designjavaunit testing

Earlier today I have come up with an idea, based upon a particular real use case, which I would want to have checked for feasability and usefulness. This question will feature a fair chunk of Java code, but can be applied to all languages running inside a VM, and maybe even outside. While there is real code, it uses nothing language-specific, so please read it mostly as pseudo code.

The idea
Make unit testing less cumbersome by adding in some ways to autogenerate code based on human interaction with the codebase. I understand this goes against the principle of TDD, but I don't think anyone ever proved that doing TDD is better over first creating code and then immediatly therafter the tests. This may even be adapted to be fit into TDD, but that is not my current goal.

To show how it is intended to be used, I'll copy one of my classes here, for which I need to make unit tests.

public class PutMonsterOnFieldAction implements PlayerAction {
    private final int handCardIndex;
    private final int fieldMonsterIndex;

    public PutMonsterOnFieldAction(final int handCardIndex, final int fieldMonsterIndex) {
        this.handCardIndex = Arguments.requirePositiveOrZero(handCardIndex, "handCardIndex");
        this.fieldMonsterIndex = Arguments.requirePositiveOrZero(fieldMonsterIndex, "fieldCardIndex");
    }

    @Override
    public boolean isActionAllowed(final Player player) {
        Objects.requireNonNull(player, "player");
        Hand hand = player.getHand();
        Field field = player.getField();
        if (handCardIndex >= hand.getCapacity()) {
            return false;
        }
        if (fieldMonsterIndex >= field.getMonsterCapacity()) {
            return false;
        }
        if (field.hasMonster(fieldMonsterIndex)) {
            return false;
        }
        if (!(hand.get(handCardIndex) instanceof MonsterCard)) {
            return false;
        }
        return true;
    }

    @Override
    public void performAction(final Player player) {
        Objects.requireNonNull(player);
        if (!isActionAllowed(player)) {
            throw new PlayerActionNotAllowedException();
        }
        Hand hand = player.getHand();
        Field field = player.getField();
        field.setMonster(fieldMonsterIndex, (MonsterCard)hand.play(handCardIndex));
    }
}

We can observe the need for the following tests:

  • Constructor test with valid input
  • Constructor test with invalid inputs
  • isActionAllowed test with valid input
  • isActionAllowed test with invalid inputs
  • performAction test with valid input
  • performAction test with invalid inputs

My idea mainly focuses on the isActionAllowed test with invalid inputs. Writing these tests is not fun, you need to ensure a number of conditions and you check whether it really returns false, this can be extended to performAction, where an exception needs to be thrown in that case.

The goal of my idea is to generate those tests, by indicating (through GUI of IDE hopefully) that you want to generate tests based on a specific branch.

The implementation by example

  1. User clicks on "Generate code for branch if (handCardIndex >= hand.getCapacity())".

  2. Now the tool needs to find a case where that holds.

    (I haven't added the relevant code as that may clutter the post ultimately)

  3. To invalidate the branch, the tool needs to find a handCardIndex and hand.getCapacity() such that the condition >= holds.

  4. It needs to construct a Player with a Hand that has a capacity of at least 1.

  5. It notices that the capacity private int of Hand needs to be at least 1.

  6. It searches for ways to set it to 1. Fortunately it finds a constructor that takes the capacity as an argument. It uses 1 for this.

  7. Some more work needs to be done to succesfully construct a Player instance, involving the creation of objects that have constraints that can be seen by inspecting the source code.

  8. It has found the hand with the least capacity possible and is able to construct it.

  9. Now to invalidate the test it will need to set handCardIndex = 1.

  10. It constructs the test and asserts it to be false (the returned value of the branch)

What does the tool need to work?
In order to function properly, it will need the ability to scan through all source code (including JDK code) to figure out all constraints. Optionally this could be done through the javadoc, but that is not always used to indicate all constraints. It could also do some trial and error, but it pretty much stops if you cannot attach source code to compiled classes.

Then it needs some basic knowledge of what the primitive types are, including arrays. And it needs to be able to construct some form of "modification trees". The tool knows that it needs to change a certain variable to a different value in order to get the correct testcase. Hence it will need to list all possible ways to change it, without using reflection obviously.

What this tool will not replace is the need to create tailored unit tests that tests all kinds of conditions when a certain method actually works. It is purely to be used to test methods when they invalidate constraints.

My questions:

  • Is creating such a tool feasible? Would it ever work, or are there some obvious problems?
  • Would such a tool be useful? Is it even useful to automatically generate these testcases at all? Could it be extended to do even more useful things?
  • Does, by chance, such a project already exist and would I be reinventing the wheel?

If not proven useful, but still possible to make such thing, I will still consider it for fun. If it's considered useful, then I might make an open source project for it depending on the time.

For people searching more background information about the used Player and Hand classes in my example, please refer to this repository. At the time of writing the PutMonsterOnFieldAction has not been uploaded to the repo yet, but this will be done once I'm done with the unit tests.

Best Answer

It's all just software, so given enough effort it's possible ;-). In a language which supports a decent way of doing code analysis it should be feasible too.

As for the usefulness, I think it some automation around unit testing is useful to a certain level, depending on how it's implemented. You need to be clear about where you want to go in advance, and be very aware of the limitations of this type of tooling.

However, the flow you describe has a huge limitation, because the code being tested leads the test. This means an error in reasoning when developing the code will likely end up in the test as well. The end result will probably be a test which basically confirms the code 'does what it does' instead of confirming it does what it should do. This actually isn't completely useless because it can be used later to verify the code still does what it did earlier, but it isn't a functional test and you shouldn't consider your code tested based on such a test. (You might still catch some shallow bugs, like null handling etc.) Not useless, but it can't replace a 'real' test. If that means you'll still have to create the 'real' test it might not be worth the effort.

You could go slightly different routes with this though. The first one is just throwing data at the API to see where it breaks, perhaps after defining some generic assertions about the code. That's basically Fuzz testing

The other one would be to generate the tests but without the assertions, basically skipping step 10. So end up with an 'API quiz' where your tools determines useful test cases and asks the tester for the expected answer given a specific call. That way you're actually testing again. It's still not complete though, if you forget a functional scenario in your code the tool won't magically find it and it doesn't remove all assumptions. Suppose the code should have been if (handCardIndex > hand.getCapacity()) instead if >=, a tool like this will never figure that out by itself. If you want to go back to TDD you could suggest test cases based on just the interface, but that would be even more functionally incomplete, because there isn't even code from which you can infer some functionality.

Your main issues are always going to be 1. Carrying errors in code over to the test and 2. functional completeness. Both issues can be suppressed somewhat, be never be eliminated. You'll always have to go back to the requirements and sit down to verify they are all actually tested correctly. The clear danger here is a false sense of security because your code coverage shows 100% coverage. Sooner or later someone will make that mistake, it's up to you to decide if the benefits outweigh that risk. IMHO it boils down to a classic trade-off between quality and development speed.

Related Topic