This essay explores the architecture and implementation of key quality attributes of AssertJ Core in relation to the previously defined key concepts and product vision. We first look at the architecture as a whole and then dive into the various views which provide insights into the design. This includes discussing the components, their connections, and the way they act at runtime and during development. We also reflect on the realisation of the key quality attributes and explore the API design principles used.
Like with many other large software systems, the architecture of AssertJ Core is layered. Since this is not an application that manages data in a certain way, we do not see the common presentation, application, and data layers1. Instead, the api
package forms the presentation layer, with the packages internal
and error
forming the layers below it. The users of AssertJ make calls to the api
in their test code, which are then handled by the internal layer. This layer performs the assertions and, if necessary, lets the error layer create and throw an error.
One of the risks of the layered architecture is the sinkhole anti-pattern. In this anti-pattern, “requests flow through multiple layers of the architecture as simple pass-through processing with little or no logic performed…”1. Luckily, AssertJ Core seems to have successfully avoided this anti-pattern, since the presentation layer performs no logic, acting as the library’s interface instead. When a test passes, the error
layer is not used. With this behaviour, there is no dedicated code necessary for passing requests to different parts of the system. Therefore, we do not see the sinkhole anti-pattern.
We do notice a downside of the chosen architecture: scalability1. The number of classes in each of the layers is already a couple dozen, which will become larger when more JDK types or assertions are added. Luckily, there is no high coupling between the various assertions, only coupling between the layers. This should keep changes in the separate layers manageable.
Containers & Components
AssertJ Core is used for testing within various testing frameworks such as JUnit or TestNG. These testing frameworks are essentially the containers for AssertJ Core, as it is the environment where the module’s functionality is used.
AssertJ Core consists of several components:
The internal
package contains the main assertion logic of AssertJ Core used for testing JDK types. The package has classes named after the type they assert about, e.g. the Maps
file contains the assertions for the Map<K, V>
interface. The naming and the related clustering convention aids in keeping the codebase organised and maintainable.
Another important package is error
which holds many distinct classes of assertion-specific errors with well-defined descriptions. This helps developers understand what exactly went wrong in the tests and directly contributes to the functional suitability of AssertJ Core.
The api
package represents the interface between the main logic and the user, providing the methods which users call when they want to test a given JDK type. It uses internal
for the main logic and error
to display potential assertion failures. The methods usually have a simple implementation, as the logic and relevant data structure formation are delegated to methods in the internal
classes.
Finally, package util
is also quite extensive. While it is not directly part of the logic infrastructure, it aids in maintainability by providing a vast array of utility methods and data structures. This makes the codebase more readable and thus also easier to change and contribute to.
The rest of the packages contain smaller amounts of code that target one specific function or mechanism, most often used by util
, api
or internal
. Usually, these packages further improve maintainability by encapsulating limited yet related functionality.
Important dependencies are displayed in the following dependency graph:
While this graph shows actual dependencies that carry the main workflow of AssertJ Core, the overall architecture is not as simple. There are some less intuitive connections, such as internal
having a dependency on api
. This should not be the case as api
makes use of the logic in internal
, but the api
should not have much to offer to the methods in the main logic. While these dependencies are relatively insignificant (e.g. using one data structure or method from the dependency), they point to some inconsistency between the intended and real architecture of AssertJ Core.
Connectors
Internal connections between components are all done by making direct Java calls to the methods needed. This is possible because all the different components are written within the same Java project. We identify a few advantages that this connector brings, mainly with respect to other connector types like REST APIs.
Firstly, any critical incompatibilities in the usage of the components will be caught at compilation time. Because Java is a strongly-typed language, any IDE will be able to catch type-related errors statically. While this does not guarantee proper usage of components, it warns about mistakes such as calling non-existent methods or violations of method signatures.
Secondly, the performance of this connector is relatively good. There is no need to convert data during communication, as Java can directly read the data that is in memory. Not having these extra steps will make it execute faster.
Finally, with components that are programmed in different environments, developers will often have to manually go through the documentation to find out how to use the other component. With all the components being written in the same Java project, the IDE can easily identify the different available methods in all the components. This enables auto-completion while developing AssertJ Core, making the work of the developers a lot easier and preventing some possible mistakes.
Besides the internal connections, AssertJ Core needs a connection from the API to the testing framework to be usable. Developers create this connection by calling the assertThat
method in their tests. Again, this connection is just a Java method call, so it has the same advantages.
Development View
The development view shows the “static organisation of the software in its development environment”2. In this section, we describe the development view of AssertJ Core.
AssertJ Core is a Maven repository and is one of the six modules of the AssertJ project. It is written in Java, though it also includes a few Shell scripts which convert JUnit, JUnit5, and TestNG assertions into AssertJ Core assertions. The project has the usual Java structure: there are some general configuration files in the root folder and a src
folder containing the main
and test
code.
In terms of the dependencies, the most commonly imported are bytebuddy
and junit
. Bytebuddy is a library for creating and modifying Java classes at runtime. In a way, AssertJ Core extends JUnit, thus JUnit being a common dependency is pretty self-explanatory. The list of all dependencies in the pom.xml
file is quite long, but not all of them are frequently used.
Another important aspect of the development view is the process of making contributions to the project. There are issues and PR templates available in the repository, and anyone is encouraged to open a new issue upon finding a bug or a missing assertion. The guidelines for contributions specify the conventions one must follow (e.g. which Java version to use or how to work with branching and naming conventions). A special focus in the guidelines is also given to binary compatibility3 which should be preserved whenever possible in any new additions to the code.
Runtime View
Since AssertJ Core is a support library for testing, it does not launch on its own. The runtime happens in other projects with AssertJ Core as a dependency when the assertions are run as part of the test suite. We demonstrate the runtime flow with the following test:
import static org.assertj.core.api.Assertions.*;
@Test
void test() {
assertThat(4 + 3).isGreaterThanOrEqualTo(7);
}
The following then happens within AssertJ Core at runtime:
- The
assertThat
method in theAssertions
class in theapi
package calls the same method inAssertionsForClassTypes
. - This returns a new
IntegerAssert
with3 + 4
as the actual value. - The
isGreaterThanOrEqualTo
method defined inAbstractIntegerAssert
inapi
is then called on the instance. - This propagates to the
assertGreaterThanOrEqualTo
method in theComparables
class of theinternal
package which checks that the actual is not null and can either:- use the
isLessThan
strategy which should evaluate to false, or - throw and
AssertionError
from theerror
package if it evaluates to true.
- use the
Realisation of Key Quality Attributes
In our previous essay, we have identified key quality attributes most relevant to different stakeholders. We believe there is still great value in differentiating between these stakeholders, and we also employ it to gain insight into the link between architectural decisions and quality of the software. This is depicted in the following table:
Quality attribute | Important for: | Relevant implementation decisions & architectural aspects |
---|---|---|
Functional Suitability | Everyone | The project has assertions for the most commonly used JDK types and therefore is functionally suitable. Furthermore, code is built in a way that adding assertions for new types or assertions with new functionality is easy, and any contributions are encouraged. |
Performance Efficiency | Developers | We could not find anything in the architecture or in the maintenance processes that would specifically address performance efficiency. |
Compatibility | Developers & Testers | In this project, compatibility refers to compatibility with different Java versions and different IDEs: both of these are taken into consideration in the source code. Different AssertJ Core versions support different ranges of Java versions, but all of them support Java 8 and higher4. AssertJ Core offers integration with Eclipse and IntelliJ. |
Operability | Developers & Testers | The documentation, Javadoc, and code-completion of AssertJ Core improve the operability of the code for both developers and testers. In terms of architecture, the api package contributes to this quality attribute because it divides logic from view. Furthermore, because this is a Maven repository, using AssertJ Core in a new project is very straightforward. |
Reliability | Everyone | The guidelines for contributions stress the importance of sufficient testing. Next to that, several CI pipelines are run for any Pull Request. |
Security | Users | Any PRs are reviewed by the maintainers of the project. Because this is open-source software, everyone has access to the source code and can be aware of the level of security the project has. |
Maintainability | Maintainers | Maintainability is supported by clearly defined contribution guidelines. This quality attribute is also reflected in how easy it is to add new assertions, even for newly created types in Java. |
One trade-off that we identified is between expressive assertions and performance of the system. Having very specific assertions improves functional suitability and operability, but it can hinder performance efficiency (e.g. in containsExactly
, a Map is cloned and checked several times). Clearly, functional suitability and operability is in AssertJ Core’s case prioritised more.
API Design Principles
AssertJ Core provides an API within a separate api
package. This package contains abstract assertion classes tied to specific JDK types, as well as their concrete equivalents. The following principles5 seem to have been applied within the API:
- Consistency and Standards. The assertions provided by AssertJ Core all follow a specific format, using keywords such as
is
to assert properties orcontains
to assert composition. - Match Between the System and the Real World. Aside from being consistent, the assertions are also very expressive and tied to what is actually being tested. A great example is the assertion
containsExactlyInAnyOrder
, which tests whether a collection has (you guessed it) exactly the elements you specified in any order. - Help and Documentation. This refers to the ease-of-use and support provided to the users of AssertJ Core. Because of the way it is packaged, the auto-completion always shows the assertion options for the specific JDK type you are testing6. Furthermore, in case that is not enough, AssertJ Core is meticulously documented, with examples and descriptions of all the available assertions.
References
-
Richards, M. (2015). Software Architecture Patterns. O’Reilly Media, Inc. Retrieved March 12, 2022, from https://learning-oreilly-com.tudelft.idm.oclc.org/library/view/software-architecture-patterns/9781491971437/. ↩︎
-
Kruchten, P. (1995). Architectural Blueprints-The “4+1” View Model of Software Architecture. Retrieved March 12, 2022, from https://www.cs.ubc.ca/~gregor/teaching/papers/4+1view-architecture.pdf ↩︎
-
Oracle. (n.d.). Chapter 13. Binary Compatibility. Chapter 13. Binary Compatibility. Retrieved March 12, 2022, from https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html ↩︎
-
AssertJ - fluent assertions java library. (2022, March 4). Retrieved March 13, 2022, from https://assertj.github.io/doc/#assertj-core-java-versions ↩︎
-
Mitra, R. (2021, August 4). Improve your API design with 7 guiding principles. The New Stack. Retrieved March 12, 2022, from https://thenewstack.io/improve-api-design-7-guiding-principles/ ↩︎
-
Bushnev, Y. (2017, September 19). Hamcrest vs AssertJ Assertion Frameworks - Which One Should You Choose? Hamcrest vs AssertJ Assertion Frameworks - Which One Should You Choose? | BlazeMeter. Retrieved March 13, 2022, from https://www.blazemeter.com/blog/hamcrest-vs-assertj-assertion-frameworks-which-one-should-you-choose ↩︎