Search code examples
javaunit-testingjmockit

How to refactor class in order to be testable without reflection


I have a class similar to this:

public class QueueingCommandRunner {
    private Status status;
    private Map<Class<CommandHandler>, CommandHandler> queuedCommands;
    private RunnerClass runnerClass;
    private ExternalCommandRunnerRegistry externalCommandRunnerRegistry;
    private ExternalCommandRunner externalCommandRunner;

    public QueueingCommandRunner(ExternalCommandRunnerRegistry externalCommandRunnerRegistry,
            RunnerClass runnerClass) {

        this.externalCommandRunnerRegistry = externalCommandRunnerRegistry;
        this.runnerClass = runnerClass;
        this.queuedCommands = new LinkedHashMap<>();
        this.status = Status.DOWN;
    }

    public void init() {
        doSomeStuff();
        externalCommandRunner = externalCommandRunnerRegistry.get(runnerClass);
        externalCommandRunner.runListeningCommand(ListenableStatusCommand.class,
                new ListenableStatusHandler(this::changeStatus));
    }

    public <T extends CommandHandler> void runCommand(Class<T> command, T commandHandler) {
        if (status == UP) {
            externalCommandRunner.run(command, commandHandler);
        } else {
            queuedCommands.put(command, commandHandler);
        }
    }

    private void changeStatus(Status status) {
        this.status = status;
        if (status == UP) {
            Iterator<Entry<Class<CommandHandler>, CommandHandler>> commandsIterator =
                queuedCommands.iterator();
            while (commandsIterator.hasNext()) {
                <Entry<Class<CommandHandler>, CommandHandler>> queuedCommand = commandsIterator.next();
                externalCommandRunner.run(queuedCommand.getKey(), queuedCommand.getValue());
                commandsIterator.remove();
            }
        }
    }
}

I omit stuff like synchronization. My question is, how to test queuing which is inside without using things like invocation of private methods via reflection? In particular I would like to know how to test changeStatus method since it is not directly run from any public method in this class. Is this class bad by design (from unit testing point of view)?

I am using JMockit for testing...


Solution

  • As it was mentioned in the comment - you test

    desired public observable behavior

    So, if you want to test private methods you need to make them public. I suggest to make it as interface:

    public interface SomeInterface {
    
        changeStatus(Status status);
    }
    

    Then inject implementation to your class:

    public final class A {
    
        private final SomeInterface someInterface;
    
        public A(SomeInterface someInterface) {
            this.someInterface = someInterface;
        }
    }
    

    Then you can easily test SomeInterface implementation and mock if you need it in your class A.

    So, I cannot give you the whole refactoring process for your particular case. But you can follow this guideline and you can encapsulate all private methods with interfaces that are tested easily. As I can see, you are using internal details of your class in private methods - these details should be encapsulated in interface implementation constructor (via another interfaces) and you will end up with small nice cohesive testable classes. Look through Command Pattern as it seems suitable for your case and try to follow SOLID which will also lead to testable code.

    I see several problems with your design:

    • init() method. Which leads to temporal coupling as your class is not ready to use after construction;
    • your runCommand method is doing two things based on status. It's either run command or put it to map (which is hidden side-effect);
    • your changeStatus is also running comands.

    You need to decouple those thins (running command, holding them and tracing status). Maybe encapsulate status of the command inside command itself. So the command will know how to work in its own way.