DEV Community

Mark Andreev
Mark Andreev

Posted on

How to create Unit tests for code design?

Preview picture

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);
}



Enter fullscreen mode Exit fullscreen mode

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);
  }


Enter fullscreen mode Exit fullscreen mode

Or for Spring you can add restrictions for Controller names:



@Test
  void controllerNameRule() {
    classes()
        .that()
        .areAnnotatedWith(RestController.class)
        .should()
        .haveSimpleNameContaining("Controller")
        .check(importedClasses);
  }


Enter fullscreen mode Exit fullscreen mode

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);
  }


Enter fullscreen mode Exit fullscreen mode

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

Top comments (5)

Collapse
 
foookinaaa profile image
Yuliya Fokina

thanks a lot for describing this, it's helpful in my working project

Collapse
 
maxim-sokolov profile image
Maxim Sokolov

Great post! I appreciate the clear examples and step-by-step guide on crafting unit tests for better code design. It's really helpful for reinforcing best practices!

Collapse
 
kiquzipa profile image
kiquzipa

I found this article on unit testing for code design incredibly insightful. The author’s approach to testing not just functionality, but also design, is a game-changer. It’s a reminder that good design is just as crucial as functionality in software development. Highly recommended!

Collapse
 
naskarov profile image
Nurlan Askarov

We have to add this topic to our university program! Thank you for your insite!

Collapse
 
mrkandreev profile image
Mark Andreev

Please, contact me at linkedin.com/in/mark-andreev-75a75... . I will help you with details.