Last active
January 19, 2021 23:10
-
-
Save arosini/5bb709da5931d600b6b7 to your computer and use it in GitHub Desktop.
JUnit test for 100% code coverage of Lombok's @DaTa annotation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import com.google.common.reflect.ClassPath; | |
import javassist.CannotCompileException; | |
import javassist.ClassPool; | |
import javassist.CtClass; | |
import javassist.CtConstructor; | |
import javassist.CtMethod; | |
import javassist.CtNewConstructor; | |
import javassist.CtNewMethod; | |
import javassist.NotFoundException; | |
import nl.jqno.equalsverifier.EqualsVerifier; | |
import nl.jqno.equalsverifier.Warning; | |
import org.junit.Assert; | |
import org.junit.Before; | |
import org.junit.Test; | |
import org.meanbean.test.BeanTestException; | |
import org.meanbean.test.BeanTester; | |
import java.io.IOException; | |
import java.lang.reflect.Modifier; | |
// ClassLoader code adapted from http://stackoverflow.com/a/21430849/2464657 | |
public class ModelTests { | |
private static final String MODEL_PACKAGE = "my.package"; | |
private BeanTester beanTester; | |
@Before | |
public void before() { | |
beanTester = new BeanTester(); | |
} | |
@Test | |
public void testAbstractModels() throws IllegalArgumentException, BeanTestException, InstantiationException, | |
IllegalAccessException, IOException, AssertionError, NotFoundException, CannotCompileException { | |
// Loop through classes in the model package | |
final ClassLoader loader = Thread.currentThread().getContextClassLoader(); | |
for (final ClassPath.ClassInfo info : ClassPath.from(loader).getTopLevelClassesRecursive(MODEL_PACKAGE)) { | |
final Class<?> clazz = info.load(); | |
// Only test abstract classes | |
if (Modifier.isAbstract(clazz.getModifiers())) { | |
// Test #equals and #hashCode | |
EqualsVerifier.forClass(clazz).suppress(Warning.STRICT_INHERITANCE, Warning.NONFINAL_FIELDS).verify(); | |
} | |
} | |
} | |
@Test | |
public void testConcreteModels() | |
throws IOException, InstantiationException, IllegalAccessException, NotFoundException, CannotCompileException { | |
// Loop through classes in the model package | |
final ClassLoader loader = Thread.currentThread().getContextClassLoader(); | |
for (final ClassPath.ClassInfo info : ClassPath.from(loader).getTopLevelClassesRecursive(MODEL_PACKAGE)) { | |
final Class<?> clazz = info.load(); | |
// Skip abstract classes, interfaces and this class. | |
int modifiers = clazz.getModifiers(); | |
if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) || clazz.equals(this.getClass())) { | |
continue; | |
} | |
// Test getters, setters and #toString | |
beanTester.testBean(clazz); | |
// Test #equals and #hashCode | |
EqualsVerifier.forClass(clazz).withRedefinedSuperclass() | |
.suppress(Warning.STRICT_INHERITANCE, Warning.NONFINAL_FIELDS).verify(); | |
// Verify not equals with subclass (for code coverage with Lombok) | |
Assert.assertFalse(clazz.newInstance().equals(createSubClassInstance(clazz.getName()))); | |
} | |
} | |
// Adapted from http://stackoverflow.com/questions/17259421/java-creating-a-subclass-dynamically | |
static Object createSubClassInstance(String superClassName) | |
throws NotFoundException, CannotCompileException, InstantiationException, IllegalAccessException { | |
ClassPool pool = ClassPool.getDefault(); | |
// Create the class. | |
CtClass subClass = pool.makeClass(superClassName + "Extended"); | |
final CtClass superClass = pool.get(superClassName); | |
subClass.setSuperclass(superClass); | |
subClass.setModifiers(Modifier.PUBLIC); | |
// Add a constructor which will call super( ... ); | |
CtClass[] params = new CtClass[] {}; | |
final CtConstructor ctor = CtNewConstructor.make(params, null, CtNewConstructor.PASS_PARAMS, null, null, subClass); | |
subClass.addConstructor(ctor); | |
// Add a canEquals method | |
final CtMethod ctmethod = CtNewMethod | |
.make("public boolean canEqual(Object o) { return o instanceof " + superClassName + "Extended; }", subClass); | |
subClass.addMethod(ctmethod); | |
return subClass.toClass().newInstance(); | |
} | |
} |
This code works fine as long as you don't have the @lombok.RequiredArgsConstructor in the objects.
- This doesn't work if your concrete models contains fields declared with an abstract supertype. You'll have to configure the
BeanTester
of the meanbean library to account for this - This also doesn't work if your model class extends a superclass without adding new fields. For JPA for example this can be useful. You'll have to manually exclude those classes from this automated test
- This also doesn't work for a model structure which contains a circular dependency, which is often the case for JPA entities. You will have to register a custom instance factory with the
EqualsVerifier
for one of the types to break the cycle
I'm trying this out now. I wonder why the author preferred to share this as a snippet instead of sharing it as a project.
Seems to not work for ENUMS.
I changed mine to ignore them:
if (Modifier.isAbstract(modifiers) || Modifier.isInterface(modifiers) || clazz.equals(this.getClass()) || clazz.isEnum()) {
continue;
}
This code works fine as long as you don't have the @lombok.RequiredArgsConstructor in the objects.
Is there a way to cover this? or maybe @lombok.AllArgsConstructor ?
This works great except that i am also using lomboks's @builder, it just drops builder in coverage. Has anyone else solved that problem?
@sajadreshi I was able to just use a built in method of the @DaTa class and check if it wasn't null... that covered it for me. like:
var coveredData = myDataAnnotatedDumbUncoveredBuilderObject.toString()
// then assert whatever with coveredData and you'll fulfill the coverage.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Also needed to compile in my project
com.google.guava
guava
14.0