Issue
There are several enum classes in my project. To safeguard persistence we do not rely on the internal id as this changes with order of enum values (e.g. when new enum is added later somewhere in between). Instead a (usually numeric) id attribute is assigned to each value.
But this opens up another error possibility: the same id could be assigned twice. Therefore I create a JUnit test which checks the ID of all values for duplicates. And I want to make that test generic, to avoid copy-paste for each new enum class. Something that takes the enum class and the getter that shall be used for persistence, and runs the test, e.g.:
(new JUnitEnumTester(OneEnum.class, getId)).runTest()
(new JUnitEnumTester(TwoEnum.class, getName)).runTest()
What I got going so far is the test for a single enum class:
package com.xyz.enums;
import static org.junit.Assert.*;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
public class OneTest {
@Test
// Testing enum IDs
public void testUniqueId_WithMap() {
Map<Integer,String> idMap = new HashMap<Integer,String>();
String msg = "";
for (One elem : One.values()) {
Integer enumId = new Integer(elem.getId());
if (idMap.containsKey(enumId)) {
msg += "\nDuplicate ID found: '" + enumId.toString() +
"' for enum: " + idMap.get(enumId) + ", " + elem.name();
}
else {
idMap.put(enumId, elem.name());
}
}
if (msg.length() > 0) {
fail(msg);
}
}
}
Question is, how to generalize this approach?
First I was going for a static method but could not figure out how to reference the method to be used as getter.
public class JUnitTester {
static public void runTest(Class cls, ? method) {
:
}
}
I think that this would not work anyway, so I am approaching generics now. Though it seems to be more object-oriented, still not rolling.
public class JunitEnumTester <? extends Enum> {
public void runTest() {
Map<Integer,String> idMap = new HashMap<Integer,String>();
String msg = "";
for (Enum elem : Enum.values()) {
Integer enumId = new Integer(elem.getId());
if (idMap.containsKey(enumId)) {
msg += "\nDuplicate ID: '" + enumId.toString() +
"' for " + idMap.get(enumId) + ", " + elem.name();
}
else {
idMap.put(enumId, elem.name());
}
}
if (msg.length() > 0) {
fail(msg);
}
}
}
Any hints or directions where to go?
Solution
To reuse your method, we need to generalize it. We need the enum values and something we can call on an enum value to get the corresponding id. These two dependencies should be passed in as parameters.
The enum values of an enum type T
can be passed in as an array T[]
. To get the id of an enum, we can use a Function<T, ID> idExtractor
which takes an enum value of type T
and returns it id of type ID
.
The method look like this:
public static <T extends Enum<T>, ID> void testUniqueIds(T[] enumValues, Function<T, ID> idExtractor) {
String msg = "";
Map<ID, T> enumsById = new HashMap<>();
for (T enumValue : enumValues) {
ID id = idExtractor.apply(enumValue);
if (enumsById.containsKey(id)) {
msg += "\nDuplicate ID found: '" + id.toString() + "' for enum: " + enumsById.get(id) + ", " + enumValue.name();
} else {
enumsById.put(id, enumValue);
}
}
if (!msg.isEmpty()) {
fail(msg);
}
}
Let's look at a small example:
enum ABC {
A('a'), B('b'), C('c');
private char letter;
private ABC(char letter) {
this.letter = letter;
}
public char getLetter() {
return letter;
}
}
The corresponding test method:
@Test
void testABC() {
JunitEnumTester.testUniqueIds(ABC.values(), ABC::getLetter);
}
To pass in the enum values we use the method values()
which is defined on every enum. To pass in the id extractor function we use a method reference, which keeps the invocation short.
If we run the test, it will be green. But if we initialize the enum constant C
with the letter 'a'
the test will fail.
Answered By - LuCio