Java: Promoting Testability by Having Enum Implementing an Interface

OVERVIEW

This post illustrates how we can easily write a better test case without polluting our production code with non-production code by performing a minor refactoring to the production code.

PROBLEM

Let’s assume we have a simple Data Reader that reads all the lines of a given algorithm data file and returns them:-

public class DataReader {
    public List<String> getDataLines(AlgorithmEnum algorithm) {
        // we have `StS-data.txt`, `CtE-data.txt` and `TtI-data.txt` under `src/main/resources` dir
        String fileName = String.format("%s-data.txt", algorithm.getShortName());
        Scanner scanner = new Scanner(getClass().getClassLoader().getResourceAsStream(fileName));

        List<String> list = new ArrayList<String>();

        while (scanner.hasNextLine()) {
            list.add(scanner.nextLine());
        }

        return list;
    }
}

This API accepts an AlgorithmEnum and it looks something like this:-

public enum AlgorithmEnum {
    SKIN_TO_SKIN("StS"),
    CLOSURE_TO_EXIT("CtE"),
    TIME_TO_INCISION("TtI");

    private String shortName;
    
    AlgorithmEnum(String shortName) {
        this.shortName = shortName;
    }

    public String getShortName() {
        return shortName;
    }
}

Let’s assume each algorithm data file has millions of data lines.

So, how do we test this code?

SOLUTION 1: Asserting Actual Line Count == Expected Line Count

One straightforward way is to:-

  • Pass in one of the Enum constants (AlgorithmEnum.SKIN_TO_SKIN, etc) into DataReader.getDataLines(..)
  • Get the actual line counts
  • Assert the actual line counts against the expected line counts

public class DataReaderTest {
    @Test
    public void testGetDataLines() {
        List<String> lines = new DataReader().getDataLines(AlgorithmEnum.SKIN_TO_SKIN);
        assertThat(lines, hasSize(7500));
    }
}    

This is a pretty weak test because we only check the line counts. Since we are dealing with a lot of data lines, it becomes impossible to verify the correctness of each data line.

SOLUTION 2: Adding a Test Constant to AlgorithmEnum

Another approach is to add a test constant to AlgorithmEnum:-

public enum AlgorithmEnum {
    SKIN_TO_SKIN("StS"),
    CLOSURE_TO_EXIT("CtE"),
    TIME_TO_INCISION("TtI"),
		
    // added a constant for testing purpose
    TEST_ABC("ABC");

    private String shortName;
    
    AlgorithmEnum(String shortName) {
        this.shortName = shortName;
    }

    public String getShortName() {
        return shortName;
    }
}

Now, we can easily test the code with our test data stored at src/test/resources/ABC-data.txt:-

public class DataReaderTest {
    @Test
    public void testGetDataLines() {
        List<String> lines = new DataReader().getDataLines(AlgorithmEnum.TEST_ABC);
        assertThat(lines, is(Arrays.asList("line 1", "line 2", "line 3")));
    }
}

While this approach works, we pretty much polluted our production code with non-production code, which may become a maintenance nightmare as the project grows larger in the future.

SOLUTION 3: AlgorithmEnum Implements an Interface

Instead of writing a mediocre test case or polluting the production code with non-production code, we can perform a minor refactoring to our existing production code.

First, we create a simple interface:-

public interface Algorithm {
    String getShortName();
}

Then, we have AlgorithmEnum to implement Algorithm:-

public enum AlgorithmEnum implements Algorithm {
    SKIN_TO_SKIN("StS"),
    CLOSURE_TO_EXIT("CtE"),
    TIME_TO_INCISION("TtI");

    private String shortName;

    AlgorithmEnum(String shortName) {
        this.shortName = shortName;
    }

    public String getShortName() {
        return shortName;
    }
}

Now, instead of passing AlgorithmEnum into getDataLines(...), we will pass in Algorithm interface.

public class DataReader {
    public List<String> getDataLines(Algorithm algorithm) {
        String fileName = String.format("%s-data.txt", algorithm.getShortName());
        Scanner scanner = new Scanner(getClass().getClassLoader().getResourceAsStream(fileName));
    
        List<String> list = new ArrayList<String>();
    
        while (scanner.hasNextLine()) {
            list.add(scanner.nextLine());
        }

        return list;
    }
}

With these minor changes, we can easily unit test the code with our mock data stored under src/test/resources directory.

public class DataReaderTest {
    @Test
    public void testGetDataLines() {
        List<String> lines = new DataReader().getDataLines(new Algorithm() {
            @Override
            public String getShortName() {
                // we have `ABC-data.txt` under `src/test/resources` dir
                return "ABC";
            }
        });

        assertThat(lines, is(Arrays.asList("line 1", "line 2", "line 3")));
    }
}

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s