Wiederverwendung von Regeln für ArchUnit

vorhergehende Artikel in: Java Software-Test OpenSource
31.01.2025

Ich habe neulich wieder einmal über neue Regeln für ArchUnit berichtet. Ich organisiere meine Regeln so, dass ich Dateien mit Regeldefinitionen habe und in konkreten Paketen dann Test-Klassen erstelle, in denen einige der global definierten Regeln angewendet werden

Die aktuelle Regelsammlung sieht derzeit wie folgt aus:

import com.tngtech.archunit.lang.ArchRule;
import org.slf4j.Logger;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

public class Rules { public static final ArchRule NO_INLINE_JAVA_UTIL_LOGGING=noClasses(). should().callMethod("java.util.logging.Logger","getLogger","java.lang.String"). orShould().callMethod("java.util.logging.Logger","getLogger","java.lang.String","java.lang.String");

public static final ArchRule LOGGERS_SHOULD_BE_PRIVATE_STATIC_FINAL = // fields().that().haveRawType(Logger.class) fields().that().haveName("CLASS_LOGGER") .and().haveRawType(Logger.class) .or().haveName("EXCEPTION_LOGGER") .and().haveRawType(Logger.class) .should().bePrivate() .andShould().beStatic() .andShould().beFinal() .because("we agreed on this convention");

public static final ArchRule NO_SYSTEM_CURRENTTIMEMILLIS= noClasses(). should().callMethod("java.lang.System","currentTimeMillis");

public static final ArchRule NO_NEW_JAVAUTILDATE= noClasses(). should().callConstructor("java.util.Date");

public static final ArchRule NO_LOCAL_DATE_TIME_EMPTY_CONSTRUCTOR= noClasses().should() .callMethod(java.time.LocalDateTime.class, "now");

public static final ArchRule NO_JAVA_BEANS_INTROSPECTOR_GETBEANINFO= noClasses(). should().callMethod("java.beans.Introspector","getBeanInfo","java.lang.Class"). orShould().callMethod("java.beans.Introspector","getBeanInfo","java.lang.Class","java.lang.Class");

public static final ArchRule NO_JAVA_BEANS_PROPERTYDESCRIPTOR_GETDISPLAYNAME= noClasses(). should().callMethod("java.beans.PropertyDescriptor","getDisplayName");

public static final ArchRule NO_JAVA_BEANS_XMLENCODER_SETPERSISTENCEDELEGATE= noClasses(). should().callMethod("java.beans.XMLEncoder","setPersistenceDelegate","java.lang.Class","java.beans.PersistenceDelegate");

public static final ArchRule NO_NEW_JAVAX_SWING_JFILECHOOSER= noClasses(). should().callConstructor("javax.swing.JFileChooser"). orShould().callConstructor("javax.swing.JFileChooser","java.lang.String"). orShould().callConstructor("javax.swing.JFileChooser","java.io.File"). orShould().callConstructor("javax.swing.JFileChooser","javax.swing.filechooser.FileSystemView"). orShould().callConstructor("javax.swing.JFileChooser","java.io.File","javax.swing.filechooser.FileSystemView"). orShould().callConstructor("javax.swing.JFileChooser","java.lang.String","javax.swing.filechooser.FileSystemView");

public static final ArchRule NO_JAVA_IO_FILE_TOURI= noClasses(). should().callMethod("java.io.File","toURI");

public static final ArchRule NO_USE_OF_CLASSES_IN_SCRATCH_PACKAGES= noClasses().should().dependOnClassesThat().resideInAPackage("..scratch..");

public static final ArchRule NO_JAVAX_XML_PARSERS_DOCUMENTBUILDERFACTORY_NEWINSTANCE= noClasses(). should().callMethod("javax.xml.parsers.DocumentBuilderFactory","newInstance"); public static final ArchRule NO_JAVAX_XML_PARSERS_SAXPARSERFACTORY_NEWINSTANCE= noClasses(). should().callMethod("javax.xml.parsers.SAXParserFactory","newInstance");

public static final ArchRule NO_JAVAX_XML_STREAM_XMLINPUTFACTORY_NEWINSTANCE= noClasses(). should().callMethod("javax.xml.stream.XMLInputFactory","newInstance");

public static final ArchRule NO_JAVAX_XML_TRANSFORM_TRANSFORMERFACTORY_NEWINSTANCE= noClasses(). should().callMethod("javax.xml.transform.TransformerFactory","newInstance");

public static final ArchRule NO_JAVA_SQL_STATEMENT_EXECUTE= noClasses(). should().callMethod("java.sql.Statement","execute", "java.lang.String");

public static final ArchRule NO_JAVA_SQL_STATEMENT_EXECUTEUPDATE= noClasses(). should().callMethod("java.sql.Statement","executeUpdate", "java.lang.String");

public static final ArchRule NO_ACCESS_TO_CLASSES_IN_PACKAGE_SCRATCH= noClasses() .should() .accessClassesThat() .resideInAPackage("..scratch");

public static final ArchRule NO_CLASSES_SHOULD_CONSTRUCT_BIGDECIMAL_FROM_DOUBLE_WITHOUT_MATHCONTEXT = noClasses() .should() .callConstructor(java.math.BigDecimal.class, double.class);

private Rules() { super(); } }

Die Anwendung der Regeln könnte etwa wie folgt aussehen:

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.junit.ArchUnitRunner;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.library.freeze.FreezingArchRule;

import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.event.Level;

import static com.tngtech.archunit.library.GeneralCodingRules.*;

@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!! @AnalyzeClasses(packages = {"de.elbosso.algorithms"}, importOptions = ExcludeApiImportOption.class) public class TestCodingRules {

private final static Logger CLASS_LOGGER = org.slf4j.LoggerFactory.getLogger(TestCodingRules.class);

@org.junit.Rule public org.junit.rules.TemporaryFolder folder = new org.junit.rules.TemporaryFolder();

@org.junit.Rule public org.junit.rules.TestName name = new org.junit.rules.TestName();

/** * The Before annotation indicates that this method must be executed before * each test in the class, so as to execute some preconditions necessary for * the test. */

@org.junit.Before public void methodBefore() { }

/** * The BeforeClass annotation indicates that the static method to which is * attached must be executed once and before all tests in the class. That * happens when the test methods share computationally expensive setup (e.g. * connect to database). */

@org.junit.BeforeClass public static void methodBeforeClass() { de.elbosso.util.Utilities.configureBasicStdoutLogging(Level.INFO); }

/** * The After annotation indicates that this method gets executed after * execution of each test (e.g. reset some variables after execution of * every test, delete temporary variables etc) */

@org.junit.After public void methodAfter() { }

/** * The AfterClass annotation can be used when a method needs to be executed * after executing all the tests in a JUnit Test Case class so as to * clean-up the expensive set-up (e.g disconnect from a database). * Attention: The method attached with this annotation (similar to * BeforeClass) must be defined as static. */

@org.junit.AfterClass public static void methodAfterClass() { de.elbosso.util.Utilities.configureBasicStdoutLogging(Level.ERROR); }

@ArchTest private final ArchRule no_access_to_standard_streams = FreezingArchRule.freeze(NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS);

// more rules here }

Die referenzierte ExcludeApiImportOption sieht wie folgt aus:

class ExcludeApiImportOption implements com.tngtech.archunit.core.importer.ImportOption
{
	@Override
	public boolean includes(com.tngtech.archunit.core.importer.Location location)
	{
		return !((location.contains("de/elbosso/algorithms/scratch")));
	}
}

Das gleiche Schema wende ich auch für Tests selbst an - eine Regel, die nur speziell für (Unit-)Tests Sinn macht ist dabei:

import com.tngtech.archunit.lang.ArchRule;
import org.junit.Test;

public class Rules4Tests { public static final ArchRule ALL_JUNIT_TESTS_SHOULD_EITER_ASSUME_ASSERT_OR_EXPECT = com.tngtech.archunit.lang.syntax.ArchRuleDefinition .methods() .that() .areAnnotatedWith(Test.class) .should(new JUnitAssumeAssertExpectedExceptionCondition());

private Rules4Tests() { super(); } }

mit der JUnitAssumeAssertExpectedExceptionCondition Condition:

import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.JavaMethodCall;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;

//https://reflectoring.io/enforce-architecture-with-arch-unit/ public class JUnitAssumeAssertExpectedExceptionCondition extends ArchCondition<JavaMethod> { public JUnitAssumeAssertExpectedExceptionCondition() { super("a unit test should assert something, assume something or at least expect an exception!"); }

@Override public void check(JavaMethod item, ConditionEvents events) { for (JavaMethodCall call : item.getMethodCallsFromSelf()) { if ((call.getTargetOwner().getPackageName().equals( org.junit.Assert.class.getPackageName()) && call.getTargetOwner().getName().equals( org.junit.Assert.class.getName())) || (call.getTargetOwner().getPackageName().equals( org.junit.Assume.class.getPackageName()) && call.getTargetOwner().getName().equals( org.junit.Assume.class.getName())) || (call.getTargetOwner().getName().equals( com.tngtech.archunit.lang.ArchRule.class.getName()) && call.getName().equals("check")) //Invariantentest: ||((call.getTargetOwner().getName().equals( de.elbosso.util.test.TestInterleavedArrayCopy.class.getName())) &&(call.getName().equals("validateDestArray"))) ) { return; } } if (item.getAnnotationOfType(org.junit.Test.class).expected() != org.junit.Test.None.class) // System.out.println(" return; events.add(SimpleConditionEvent.violated( item, item.getDescription() + "does not assert anything.") ); } }

Ich habe hier die ursprüngliche Implementierung ein wenig erweitert - nun muss ein Test nicht mehr zwingend ein Assert enthalten - das Kriterium ist nun, dass für jede Testmethode entweder ein Assert oder ein Assume vorhanden sein muss oder aber eine erwartete Exception deklariert sein muss. Falls all das nicht zutrifft, zeigt das Beispiel, dass alternativ auch eine Methode zum Check einer oder mehrerer Invarianten aufgerufen werden kann. Man könnte diesen Invariantencheck noch generischer gestalten, um die konkrete Klasse aus der Condition entfernen zu können - vielleicht werde ich den Code irgendwann dahingehend noch verbessern.

Ein Beispiel für eine Testklasse, die die Regeln für Tests verwendet, sieht man hier:

import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.junit.ArchUnitRunner;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.library.freeze.FreezingArchRule;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.event.Level;

@RunWith(ArchUnitRunner.class) // Remove this line for JUnit 5!! @AnalyzeClasses(packages = {"de.elbosso.util.test","de.netsysit.util.test"}, importOptions = ExcludeApiImportOption.class) public class TestCodingRules4Tests {

private final static Logger CLASS_LOGGER =org.slf4j.LoggerFactory.getLogger(TestCodingRules4Tests.class);

@org.junit.Rule public org.junit.rules.TemporaryFolder folder = new org.junit.rules.TemporaryFolder();

@org.junit.Rule public org.junit.rules.TestName name = new org.junit.rules.TestName();

/** * The Before annotation indicates that this method must be executed before * each test in the class, so as to execute some preconditions necessary for * the test. */

@org.junit.Before public void methodBefore() { }

/** * The BeforeClass annotation indicates that the static method to which is * attached must be executed once and before all tests in the class. That * happens when the test methods share computationally expensive setup (e.g. * connect to database). */

@org.junit.BeforeClass public static void methodBeforeClass() { de.elbosso.util.Utilities.configureBasicStdoutLogging(Level.INFO); }

/** * The After annotation indicates that this method gets executed after * execution of each test (e.g. reset some variables after execution of * every test, delete temporary variables etc) */

@org.junit.After public void methodAfter() { }

/** * The AfterClass annotation can be used when a method needs to be executed * after executing all the tests in a JUnit Test Case class so as to * clean-up the expensive set-up (e.g disconnect from a database). * Attention: The method attached with this annotation (similar to * BeforeClass) must be defined as static. */

@org.junit.AfterClass public static void methodAfterClass() { de.elbosso.util.Utilities.configureBasicStdoutLogging(Level.ERROR); }

@ArchTest public static final ArchRule all_junit_tests_should_either_assume_assert_or_expect = FreezingArchRule.freeze(Rules4Tests.ALL_JUNIT_TESTS_SHOULD_EITER_ASSUME_ASSERT_OR_EXPECT);

// more rules here }

Alle Artikel rss Wochenübersicht Monatsübersicht Codeberg Repositories Mastodon Über mich home xmpp


Vor 5 Jahren hier im Blog

  • Ticketsysteme sind lebende Wesen

    29.03.2020

    Hier zunächst wieder eine Triggerwarnung: Dieser Artikel wird meine Meinung abbilden. es kann sein, dass sie dem einen oder anderen nicht gefällt - das ist mir aber egal. Und wenn hier irgendwelche Schneeflocken mitlesen, dann sind die selber schuld.

    Weiterlesen...

Neueste Artikel

  • Weitere Experimente mit dem Clifford-Attractor

    Ich berichtete hier bereits über Experimente mit dem Clifford-Attractor, allerdings war ich noch Experimente unter geringfügig geänderten Parametern schuldig...

    Weiterlesen
  • Neues Feature in meinem Static Site Generator: externe URLs

    Es wurde wieder einmal Zeit für ein neues Feature in meinem Static Site Generator mittels dessen ich ja auch meine Heimatseite im Zwischennetz gestalte und verwalte...

    Weiterlesen
  • Eine Bestandsaufnahme

    Es kamen mehrere Faktoren zusammen: die Tatsache, dass ich nicht mehr ganz so kürzlich die 50 überschritten habe hatte ebenso darauf Einfluss wie das heutige trübe Wetter und auch der Fakt, dass ich bereits beinahe alle Wochenendpflichten erledigt habe. Der letzte Stein des Anstoßes war dann aber, dass sich heute zum 125. Mal der Geburtstag von Erich Fromm jährt.

    Weiterlesen

jürgen key

Manche nennen es Blog, manche Web-Seite - ich schreibe hier hin und wieder über meine Erlebnisse, Rückschläge und Erleuchtungen bei meinen Hobbies.

Wer daran teilhaben und eventuell sogar davon profitieren möchte, muss damit leben, daß ich hin und wieder kleine Ausflüge in Bereiche mache, die nichts mit IT, Administration oder Softwareentwicklung zu tun haben.

Ich wünsche allen Lesern viel Spaß und hin und wieder einen kleinen AHA!-Effekt...

PS: Meine öffentlichen Codeberg-Repositories findet man hier.