How to create Unit tests for code design?
Mark Andreev
Posted on April 15, 2024
As a seasoned software engineer, I understand the importance of well-designed code and the role of unit tests in maintaining its integrity. Code design is the foundation of any software system. It's the blueprint that guides the construction of the system, ensuring that the final product is robust, scalable, and maintainable. A well-designed codebase is easy to understand, modify, and extend, making it resilient to the ever-changing demands of the business environment.
Consistent code design is crucial for a team's productivity and the long-term maintainability of a project. However, achieving it is not a trivial task. It requires clear communication, shared understanding, and discipline from all team members. The difficulty lies in the fact that each developer has their unique coding style and problem-solving approach. Over time, without a concerted effort to maintain consistency, the codebase can become a patchwork of different styles and patterns, making it hard to understand and maintain.
I would like to share an approach based on ArchUnit - unit tests for your code.
ArchUnit is a powerful library developed by TNG that brings architectural control right into your codebase. The core idea of the project is to validate architectural rules in your Java codebase, ensuring that the design principles and standards you’ve set are being adhered to.
The library provides a domain-specific language (DSL) that allows you to express architectural rules in a clear, concise manner. This makes it easier for developers to understand and enforce these rules, leading to a more consistent and maintainable codebase.
@AnalyzeClasses(packages = "com.tngtech.archunit.example.layers")
public class MethodsTest {
@ArchTest
static ArchRule codeUnitsInDAOLayerShouldNotBeSecured =
noCodeUnits()
.that().areDeclaredInClassesThat().resideInAPackage("..persistence..")
.should().beAnnotatedWith(Secured.class);
}
One of the main benefits of using ArchUnit is its ability to catch architectural violations early. Instead of waiting for code reviews or architectural audits, ArchUnit can identify issues as soon as the code is written. This leads to quicker feedback and less technical debt.
Furthermore, ArchUnit is flexible and extensible, allowing you to define custom rules that fit your project’s unique needs. It integrates seamlessly with popular testing frameworks like JUnit, making it a natural fit for any Java project.
For example you can disable JUnit Assertions when you prefer AssertJ library:
@Test
void isTestClassesDontUseJunitAssertions() {
ArchRuleDefinition.noClasses()
.should()
.dependOnClassesThat()
.haveNameMatching("org.junit.jupiter.api.Assertions")
.check(importedTestClasses);
}
Or for Spring you can add restrictions for Controller names:
@Test
void controllerNameRule() {
classes()
.that()
.areAnnotatedWith(RestController.class)
.should()
.haveSimpleNameContaining("Controller")
.check(importedClasses);
}
Or even check Validation usage in RequestBody:
@Test
void restControllerValidationRule() {
classes()
.that()
.areAnnotatedWith(RestController.class)
.should()
.beAnnotatedWith(Validated.class)
.check(importedClasses);
}
@Test
void restControllerValidationRequestBodyRule() {
classes()
.that()
.areAnnotatedWith(RestController.class)
.should(
new ArchCondition<>("Any @RequestBody must be @Valid") {
@Override
public void check(JavaClass javaClass, ConditionEvents conditionEvents) {
for (JavaMethod method : javaClass.getMethods()) {
if (method.isConstructor()) {
continue;
}
for (Parameter parameter : method.reflect().getParameters()) {
if (parameter.isAnnotationPresent(RequestBody.class)
&& !parameter.isAnnotationPresent(Valid.class)) {
conditionEvents.add(
new SimpleConditionEvent(
javaClass,
false,
javaClass.getName()
+ " contains method "
+ method
+ " with @RequestBody and without Valid"));
}
}
}
}
})
.check(importedClasses);
}
Absolutely, one of the powerful features of ArchUnit is its predefined rules. These rules provide a solid foundation for enforcing architectural standards in your Java codebase. They cover a wide range of common architectural scenarios, making it easier for you to get started with architectural control.
These predefined rules can be used as-is, or they can serve as a starting point for creating your own custom rules. This flexibility allows you to tailor ArchUnit to the specific needs of your project, ensuring that your architectural standards are upheld in a way that makes sense for your team and your codebase.
In essence, ArchUnit’s predefined rules are a valuable tool for maintaining architectural integrity, reducing technical debt, and enhancing the overall quality of your code. They are a testament to the library’s power and versatility, making it an essential tool for any Java developer serious about architecture.
You can found general predefined rules in com.tngtech.archunit.library.GeneralCodingRules like:
- NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS
- NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS
- NO_CLASSES_SHOULD_USE_JODATIME
- NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING
- NO_CLASSES_SHOULD_USE_FIELD_INJECTION
And dependency rules available in com.tngtech.archunit.library.DependencyRules:
- NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES
More examples you can find at official user guide https://www.archunit.org/userguide/html/000_Index.html or at github repository https://github.com/TNG/ArchUnit
In summary, ArchUnit is a valuable tool for maintaining the integrity of your codebase’s architecture, promoting consistency, reducing technical debt, and enhancing overall code quality. It’s a must-have for any team serious about architecture.
Author is Mark Andreev, SWE @ Conundrum.ai
Posted on April 15, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.