[Java] JUnit that NG if a method with a large number of lines is detected using black magic

Assumptions: Java11, JUnit5, sbt

I'm tired of the monster method, so I wrote something like this ... Let's prepare it at the beginning of the project.

As a result of preparing something like this I divided the methods and reduced the number of lines per method for the time being, Instead, it could be made into a global variable by expanding the scope of the variable so that it can be referenced by various methods. There is a problem that ... Maybe it's still better to prepare this one this time? I think.

Paste ↓ in libraryDependencies of build.sbt

build.sbt


  "org.junit.jupiter" % "junit-jupiter-api" % "5.5.0",
  "org.junit.jupiter"%"junit-jupiter-engine" % "5.5.0",
  "org.javassist" % "javassist" % "3.25.0-GA",

code

package com.github.momosetkn;

import javassist.ClassPool;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;

class MonsterMethodAlert {

    @Test
    void test() throws Exception {
        var cp = ClassPool.getDefault();
        var fail = false;
        for (var className : getClassNameList()) {
            var cc = cp.get(className);
            for (var method : cc.getMethods()) {
                // java.lang.Object.Methods under java package such as equals are not applicable
                if (method.getDeclaringClass().getName().startsWith("java"))
                    continue;
                var methodInfo = method.getMethodInfo();
                var start = methodInfo.getLineNumber(Integer.MIN_VALUE);
                var end = methodInfo.getLineNumber(Integer.MAX_VALUE);
                var line = end - start + 1;
                if (line >= 25) {
                    System.err.println(String.format("%s%It is a monster method of s line", className + "#" + methodInfo.getName(), line));
                    fail = true;
                }
            }
        }
        if (fail)
            throw new Exception("Monster method detected");
    }

    private List<String> getClassNameList() throws IOException, URISyntaxException {
        var list = new ArrayList<String>();
        var classLoader = Thread.currentThread().getContextClassLoader();
        var targetUrls = classLoader.getResources("");
        var CLASS_EXT = ".class";
        while (targetUrls.hasMoreElements()) {
            var url = targetUrls.nextElement();
            if (!url.getProtocol().equals("file")) {
                continue;
            }
            var targetPath = Paths.get(url.toURI());
            Files.walkFileTree(targetPath, new SimpleFileVisitor<>() {
                @Override
                public FileVisitResult visitFile(Path foundPath, BasicFileAttributes attrs) throws IOException {
                    if (foundPath.toString().endsWith(CLASS_EXT)){
                        var relativizeStr = targetPath.relativize(foundPath).toString();
                        list.add(
                                relativizeStr
                                            .substring(0, relativizeStr.length() - CLASS_EXT.length())
                                            .replace(File.separatorChar, '.')
                        );
                    }
                    return super.visitFile(foundPath, attrs);
                }
            });
        }
        return list;
    }
}
Example#main is a 36-line monster method

java.lang.Exception:Monster method detected

	at com.github.momosetkn.MonsterMethodAlert.test(MonsterMethodAlert.java:37)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:436)
	at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:115)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:170)
	at org.junit.jupiter.engine.execution.ThrowableCollector.execute(ThrowableCollector.java:40)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:166)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:113)
	at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:58)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:112)
	at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:120)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:120)
	at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$2(HierarchicalTestExecutor.java:120)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:183)
	at java.base/java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:177)
	at java.base/java.util.Iterator.forEachRemaining(Iterator.java:133)
	at java.base/java.util.Spliterators$IteratorSpliterator.forEachRemaining(Spliterators.java:1801)
	at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:484)
	at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:474)
	at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:150)
	at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:173)
	at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:497)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.lambda$executeRecursively$3(HierarchicalTestExecutor.java:120)
	at org.junit.platform.engine.support.hierarchical.SingleTestExecutor.executeSafely(SingleTestExecutor.java:66)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.executeRecursively(HierarchicalTestExecutor.java:108)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor$NodeExecutor.execute(HierarchicalTestExecutor.java:79)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:55)
	at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:43)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:170)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:154)
	at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:90)
	at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:69)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

Commentary

Get method line count

Looking at the implementation of javassist.bytecode.MethodInfo # getLineNumber, It seems that the numerical value passed as an argument is passed to the following method. https://github.com/jboss-javassist/javassist/blob/rel_3_25_0_ga/src/main/javassist/bytecode/LineNumberAttribute.java#L77 It seems that it is judged only by turning it in a loop and exceeding it, so We are passing ʻInteger.MIN_VALUE and ʻInteger.MAX_VALUE.

start and end are

100: public void method(){
101: //start is the number of lines here
102: //
103: //end is the number of lines here
104: }

Since 103-102 = 2, it is treated as a method with 1 added and 3 lines.

About Javassist

Older versions of Javassist don't follow the newer Java version, so make it as new as possible.

Reference material

I want to recursively search the class list under the package -Qiita Javassist memo \ (Hishidama's Javassist Memo )

Recommended Posts

[Java] JUnit that NG if a method with a large number of lines is detected using black magic
[Java] You might be happy if the return value of a method that returns null is Optional <>
Even in Java, I want to output true with a == 1 && a == 2 && a == 3 (gray magic that is not so much as black magic)
How to deal with SQLite3 :: BusyException that occurs when uploading a large number of images using ActiveStorage in seeds.rb etc.
Create a large number of records with one command using the Ruby on Rails seeds.rb file
[Read Effective Java] Chapter 2 Item 2 "Consider a builder when faced with a large number of constructor parameters"
Pit of count method (Be careful of issuing a large number of queries!)
[Java] List method that determines whether a specific object is included
Is the version of Elasticsearch you are using compatible with Java 11?
Declare a method that has a Java return value with the return value data type
Find out if there is a font that can use Japanese (Hiragana, Katakana, Kanji) with AWS Lambda + Java