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

Copyright © 2019-2023 Proximity Works™