Architectural Unit Testing

1.jpg
Prasanth PerumalAndroid Application Developer
Technology6 mins read
Architecture Unit Testing.png

What is project architecture?

Every organization has its own set of rules for naming conventions, folder structures, inheritable base contracts, design patterns, and module dependency hierarchy which should be followed by developers in their projects. Some of the most commonly known examples include Clean architecture (See Figure 1), Onion architecture, Hexagonal architecture etc.,

Loading ...

Figure 1 - Clean Architecture

Good project architecture is essential for easy comprehension, effective reuse of code, and long-term viability. However, every team member has to be responsible for the regular upkeep and maintenance of the project architecture. When a new member comes on board, the reviewer has to provide a lot of documentation and conduct tedious training to bring them up to speed.

So, where does architecture unit testing come in?

All projects, no matter how well-maintained and peer-reviewed, tend to lose their structure as they get bigger. Teams expand, their composition changes, and there is a constant influx of new developers who may or may not have the proper context. In this transition, critical information invariably gets forgotten, ignored, and even lost. For example, a particularly egregious case (See Figure 2 and Figure 3) can involve the repository class not inheriting its base class, the use of circular dependencies, huge classes, duplicate code, and forced implementation of unnecessary contracts. 

Architecture unit testing can help identify structure deterioration, and allow developers to prepare for it in a pre-emptive manner. This method uses libraries to write the validations as unit tests within the project. Each rule can be coded in the same programming language as your project. It also reduces the possibility of human error while reviewing newer changes in the project. When added to a CI/CD pipeline, it automatically assures adherence to the code policy. 

Loading ...

Figure 2

Loading ...

Figure 3

How to architecture unit test:

Currently, libraries like ArchUnit (Java), Konsist (Kotlin), NetArchTests (.Net) etc. can be used in architecture unit testing. With growing need and popularity, I foresee more libraries coming up to support a wider roster of programming languages. In this post, I will focus on the Konsist library for Kotlin.

Loading ...

Loading ...

Why Konsist?

Konsist can be used in Kotlin MultiPlatform Projects supporting various devices. It also supports declarative API, thus making it a great candidate to use for this tutorial.

Declaration Tests:

To start Konsist tests, you need to add the dependency in your project module Gradle file. 

testImplementation("com.lemonappdev:konsist:0.13.0")

The first type you should run is for all the declarations in our project. Declarations include filenames, variable names, packages, method names etc. For example, if you want to ensure that all the ViewModel classes are within the ViewModels package,  you can write a test like the following:

class ViewModelKonsistTest {
    @Test
    fun `every view model class must reside in view model package`() {
        Konsist
            .scopeFromProject() // Define the scope containing all Kotlin files present in the project
            .classes() // Get all class declarations
            .withNameEndingWith("ViewModel") // Filter classes heaving name ending with 'ViewModel'
            .assertTrue { it.resideInPackage("..viewmodel..") 
} // Assert that each class resides in 'viewmodel' package
    }
}

You can also check if all the files annotated with ViewModel reside in the ViewModel package. Konsist provides an annotation filter as well.

@Test
fun `classes annotated with HiltViewModel annotation reside in viewmodel package`() {
    Konsist.scopeFromProject() // 1. Create a scope representing the whole project //all Kotlin files in project
        .classes()
        .withAnnotation { it.hasNameContaining("HiltViewModel") }
        .assertTrue { it.resideInPackage("..viewmodel..") } // 3. Define the assertion
}

You can check the dependency graph of the project. This is to check whether a package depends on another package or not. assertArchitecture lets us declare Layer objects, on which you can declare rules.

@Test
fun `viewmodels depend on repository layer and not vice versa`() {
    Konsist
        .scopeFromProject()
        .assertArchitecture {
            val viewmodel = Layer("ViewModel", "com.myapp.feed.viewmodel..")
            val repository = Layer("Repository", "com.myapp.feed.repository..")
            // Define architecture assertions
            viewmodel.dependsOn(repository)
            repository.dependsOn(viewmodel)
        }
}

Check if all the functions start with lowercase inside a file. This is not limited to functions alone;  you can check variables for casing as well.

@Test
fun `check the name of functions in a file start with lowercase`() {
    val functions = Konsist.scopeFromFile(
"/app/src/main/java/com/example/composesamples/utils/ErrorViewModel.kt"
) // List<KoFileDeclaration>
        .classes() // List<KoClassDeclaration>
	  .functions() // List<KoFunctionDeclaration>
    functions.forEach { function ->
        val name = function.name
        assertTrue(
		name.startsWith(name.first().lowercaseChar())
	   )
}
}

There are many more rules which can be placed declaratively. Examples of important rules – checking if a function returns a particular type of object or checking if a Util class does not have a constructor – are shown below.

@Test
    fun `methods annotated with @Composable should not have return type`() {
        Konsist
            .scopeFromPackage("com.archtests.cache")
            .functions()
            .withAnnotationOf(Composable::class)
            .assertTrue {
                !it.hasReturnValue
            }
    }
@Test
fun `check if AnnotationUtil does not have a constructor`() {
    val utilClasses = Konsist
		.scopeFromProject()
		.classes()
		.withNameEndingWith("Util")
    utilClasses.forEach { utilClass ->
        assertTrue(utilClass.constructors.isEmpty())
    }
}

Why is unit testing architecture good?

Since the tests run as static code checks, they do not adversely affect build time, app performance, or code complexity. Konsist is a stable library with a verbose API to write code guards for your projects. It is easy to learn and it takes marginal effort to add these tests to your existing projects.

Room for improvement

Despite its wide and robust functionalities, there is ample room for improvement. Dynamic code checks and the ability to test generated code would be great additions to the library. Scanning all classes can be tedious and time-consuming. Caching and indexing can fix this, and I believe libraries are already working in this direction. Support for Web and backend systems is also expected soon.

Conclusion:

Architecture unit testing is a great way to regularly check and maintain project code. As such, it should be an established practice in all organizations dealing with a large quantum of code over a long period of time. Konsist, with its multi-device support and declarative API, is a great candidate for anyone looking to start architecture unit testing. It also supports custom requirements, allowing you to tailor it to your specific needs.

Github repo: https://github.com/proximity-tech/KonsistExample

Category
Technology
Published On
29 Nov, 2023
Share

Subscribe to our tech, design & culture updates at Proximity

Weekly updates, insights & news across all teams

You may want to read

What Netflix, ESPN, and TikTok get right about AI (2).png
Technology5 mins read

What Netflix, ESPN, and TikTok get right about AI

The best in sports, media, and entertainment are doing something different Most brands lose their audience in the first five seconds. Not because their content is bad, but because it’s not built to adapt. To get real traction, you need a system that learns from behavior and fine-tunes what each person sees, when they see it. The key is to optimize for response patterns early, before scale makes mistakes harder to fix. This post breaks down how the top players in sports, media, and entertainment do just that, and how you can apply the same approach.

The double-edged whistle How AI in VAR has changed football forever.png
Technology3 mins read

How AI in VAR has changed football forever

If you’ve ever sat on the edge of your seat watching your team battle for glory, only to have the celebration halted by the dreaded VAR check, you know football isn’t just a game of skill, it’s a game of inches and interpretations. In those tense moments, the officials no longer stand alone. Artificial intelligence is now a silent partner in the most consequential decisions of the world’s most beloved sport. But has this partnership brought clarity, or simply another layer of controversy?

AI in gaming cover (2).png
Technology4 mins read

Not just smarter enemies: How AI is quietly reshaping the entire gaming industry 🎮

In 1997, a chess-playing AI named Deep Blue beat Garry Kasparov. The world gasped. Fast forward to today: AI doesn’t just play games, it helps make them. From the ethereal landscapes of No Man’s Sky to the unpredictable foes in Middle-earth: Shadow of Mordor, artificial intelligence has seeped into every corner of game development and gameplay. But here’s the twist, it’s not just about making things smarter. It’s about making things faster, deeper, and often, more human. Let’s unpack that.

Copyright © Proximity Works™