Chapter 8: Building the Framework Hexagon

When building a hexagonal application, the last step consists of exposing application features by connecting input adapters to input ports. Also, if there is any need to get data from, or persist it inside, external systems, then we need to connect output adapters to output ports. The Framework hexagon is the place where we assemble all the adapters required to make the hexagonal system.

We first created the domain model using things including entities, value objects, and specifications in the Domain hexagon. Then, in the Application hexagon, we expressed the user's intent using use cases and ports. Now, in the Framework hexagon, we have to employ adapters to expose system features and define which technologies will be used to enable such features.

What is so compelling about the hexagonal architecture is that we can add and remove adapters without worrying about changing the core system logic wrapped in the Domain hexagon. Of course, there is a price to be paid in the form of data translation between Domain entities and external entities. But in exchange, we gain a more decoupled system with clear boundaries between its realms of responsibilities.

In this chapter, we will cover the following topics:

  • Bootstrapping the Framework hexagon
  • Implementing output adapters
  • Implementing input adapters
  • Testing the Framework hexagon

By the end of this chapter, you'll learn to create input adapters to make the hexagonal application features accessible to other users and systems. Also, you'll learn to implement output adapters to enable the hexagonal system to communicate with external data sources.

Technical requirements

To compile and run the code examples presented in this chapter, you'll need the latest Java SE Development Kit and Maven 3.6 installed on your computer. They are all available for the Linux, Mac, and Windows operating systems.

You can find the code files for this chapter on GitHub at https://github.com/PacktPublishing/Designing-Hexagonal-Architecture-with-Java/tree/main/Chapter08.

Bootstrapping the Framework hexagon

When building a system using hexagonal architecture, you don't need to decide up front if the system API will be exposed using REST or gRPC, nor if the system's primary data source will be a MySQL database or MongoDB. Instead, what you need to do is start modeling your problem domain in the Domain hexagon, then designing and implementing use cases in the Application hexagon. Then, only after creating the previous two hexagons, you'll need to start thinking about which technologies will enable the hexagonal system's functionalities.

The hexagonal approach centered on Domain Driven Design allows us to postpone the decisions regarding the underlying technologies internal or external to the hexagonal system. Another prerogative of the hexagonal approach is the pluggable nature of the adapters. If you want to expose some system feature to be accessible through REST, you create and plug a REST input adapter into an input port. Later on, if you want to expose that same feature for clients using gRPC, you can create and plug a gRPC input adapter into the same input port.

When dealing with external data sources, we have the same pluggable prerogatives when using output adapters. You can plug different output adapters to the same output port, changing the underlying data source technology without incurring major refactoring on the whole hexagonal system.

To further explore input adapters, we'll have a more in-depth discussion in Chapter 12, Using RESTEasy Reactive to Implement Input Adapters. Also, we'll investigate more possibilities for output adapters in Chapter 13, Persisting Data with Output Adapters and Hibernate Reactive.

Let's stick to the basics and create a solid structure for input and output adapters. On top of such a structure, we'll be able to, later on, add the exciting features provided by the Quarkus framework.

Continuing the development of the topology and inventory system, we need to bootstrap the Framework hexagon as a Maven and Java module.

Inside the topology and inventory Maven root project, we have to run the following command:

mvn archetype:generate

    -DarchetypeGroupId=de.rieckpil.archetypes  

    -DarchetypeArtifactId=testing-toolkit

    -DarchetypeVersion=1.0.0

    -DgroupId=dev.davivieira

    -DartifactId=framework

    -Dversion=1.0-SNAPSHOT

    -Dpackage=dev.davivieira.topologyinventory.framework

    -DinteractiveMode=false

We recommend running the preceding command directly on CMD instead of PowerShell if you are using Windows. If you need to use PowerShell, you'll need to wrap each part of the command in double-quotes.

The mvn archetype:generate goal creates a Maven module called framework inside topology-inventory. This module comes with a skeleton directory structure based on the groupId and artificatId we passed in to the mvn command. Also, it includes a child pom.xml file inside the framework directory.

After executing the mvn command to create the framework module, the root project's pom.xml file will be updated to contain the new module:

<modules>

  <module>domain</module>

  <module>application</module>

  <module>framework</module>

</modules>

The framework module is inserted in the end as the latest module we have just added.

Because the framework module depends on both domain and application modules, we need to add them as dependencies in the framework module's pom.xml file:

<dependencies>

  <dependency>

    <groupId>dev.davivieira</groupId>

    <artifactId>domain</artifactId>

    <version>1.0-SNAPSHOT</version>

  </dependency>

  <dependency>

    <groupId>dev.davivieira</groupId>

    <artifactId>application</artifactId>

    <version>1.0-SNAPSHOT</version>

  </dependency>

<dependencies>

After running the Maven command to create the framework module, you should see a directory tree similar to the one shown here:

Figure 8.1 – The directory structure of the Framework hexagon

Figure 8.1 – The directory structure of the Framework hexagon

There should be a child pom.xml file in the framework directory, and a parent pom.xml file in the topology-inventory directory.

Once we have completed the Maven configuration, we can create the descriptor file that turns the framework Maven module into a Java module. We do that by creating the following file, topology-inventory/framework/src/java/module-info.java:

module framework {

    requires domain;

    requires application;

}

Because we have added domain and application as Maven dependencies on framework's pom.xml file, we can also add them as Java module dependencies in the module-info.java descriptor file.

With both the Maven and Java modules properly configured for the Framework hexagon, we can move on to create first the output adapters for the topology and inventory system.

Implementing output adapters

We start implementing the output adapters to set up the integration between our topology and inventory system and the underlying data source technology that is an H2 in-memory database. It's also important to implement output adapters first because we refer to them when implementing the input adapters.

The topology and inventory system allows external data retrieval for routers' and switches' entities. So, in this section, we will review the output ports' interfaces that get external data related to these entities. Also, we'll provide an output adapter implementation for each output port interface.

Router management output adapter

The router output adapter we need to create should implement this RouterManagementOutputPort interface:

package dev.davivieira.topologyinventory.application.ports.output;

import dev.davivieira.topologyinventory.domain.entity.Router;

import dev.davivieira.topologyinventory.domain.vo.Id;

public interface RouterManagementOutputPort {

    Router retrieveRouter(Id id);

    Router removeRouter(Id id);

    Router persistRouter(Router router);

}

Both retrieveRouter and removeRouter methods' signatures have Id as a parameter. We use that Id to identify the router on the underlying data source. Then, we have the persistRouter method signature receiving a Router parameter that can represent both core and edge routers. We use that Router parameter to persist the data in the data source.

For the topology and inventory system, for now, we have to implement only one output adapter to allow the system to use an H2 in-memory database.

We start the implementation with the RouterManagementH2Adapter class:

package dev.davivieira.topologyinventory.framework.adapters.output.h2;

import dev.davivieira.topologyinventory.application.ports.output.RouterManagementOutputPort;

import dev.davivieira.topologyinventory.domain.entity.Router;

import dev.davivieira.topologyinventory.domain.vo.Id;

import dev.davivieira.topologyinventory.framework.adapters.output.h2.data.RouterData;

import dev.davivieira.topologyinventory.framework.adapters.output.h2.mappers.RouterH2Mapper;

import jakarta.persistence.EntityManager;

import jakarta.persistence.EntityManagerFactory;

import jakarta.persistence.Persistence;

import jakarta.persistence.PersistenceContext;

public class RouterManagementH2Adapter implements RouterManagementOutputPort {

    private static RouterManagementH2Adapter instance;

    @PersistenceContext

    private EntityManager em;

    private RouterManagementH2Adapter(){

        setUpH2Database();

    }

    /** Code omitted **/

}

The H2 database connection is controlled by EntityManager. This connection is configured by the setUpH2Database method, which we execute when we call the class's empty constructor. We use the variable called instance to provide a singleton so other objects can trigger database operations.

Let's implement each method declared on the output port interface:

  1. We start with the retrieveRouter method that receives Id as a parameter:

    @Override

    public Router retrieveRouter(Id id) {

        var routerData = em.getReference(

                         RouterData.class, id.getUuid());

        return RouterH2Mapper.routerDataToDomain(routerData);

    }

    The getReference method from EntityManager is called with RouterData.class and the UUID value is extracted from the Id object. RouterData is a database entity class that we use to map data coming from the database into the Router domain entity class. This mapping is accomplished by the routerDataToDomain method from the RouterH2Mapper class.

  2. Then, we implement the removeRouter method that removes a router from the database:

    @Override

    public Router removeRouter(Id id) {

        var routerData = em.getReference(

                         RouterData.class, id.getUuid());

        em.remove(routerData);

        return null;

    }

    To remove a router, we first have to retrieve it by calling the getReference method. Once we have a RouterData object representing the database entity, we can call the remove method from EntityManager, which can delete the router from the database.

  3. Finally, we implement the persistRouter method:

    @Override

    public Router persistRouter(Router router) {

        var routerData = RouterH2Mapper.

                         routerDomainToData(router);

        em.persist(routerData);

        return router;

    }

    It receives a Router domain entity object that needs to be converted to a RouterData database entity object that can be persisted with the persist method from EntityManager.

By implementing the retrieveRouter, removeRouter, and persistRouter methods, we provide the basic database operations required by the topology and inventory system.

Let's move on to see how the switch output adapters' implementation.

Switch management output adapter

The output adapter we implement for the switch is simpler because we don't need to persist switches directly, nor remove them. The sole purpose of the switch's output adapter is to enable the retrieval of switches from the database. We allow persistence only through the router output adapter.

To get started, let's define the SwitchManagementOutputPort interface:

package dev.davivieira.topologyinventory.application.ports.output;

import dev.davivieira.topologyinventory.domain.entity.Switch;

import dev.davivieira.topologyinventory.domain.vo.Id;

public interface SwitchManagementOutputPort {

    Switch retrieveSwitch(Id id);

}

We have just one method called retrieveSwitch that receives Id and returns Switch.

The SwitchSwitchManagementH2Adapter output adapter implementation is very straightforward and similar to its router counterpart. So, we'll just assess the implementation of the retrieveSwitch method:

/** Code omitted **/

public class SwitchManagementH2Adapter implements SwitchManagementOutputPort {

    /** Code omitted **/

    @Override

    public Switch retrieveSwitch(Id id) {

        var switchData = em.getReference(

                         SwitchData.class, id.getUuid());

        return

        RouterH2Mapper.switchDataToDomain(switchData);

    }

    /** Code omitted **/

}

We call the getReference method from EntityManager with SwitchData.class and a UUID value as parameters in order to retrieve a SwitchData database entity object. Then, this object is converted to a Switch domain entity when we call the switchDataToDomain method from the RouterH2Mapper class.

Now that we have both RouterManagementH2Adapter and SwitchManagementH2Adapter properly implemented, we can proceed to implement the input adapters.

Implementing input adapters

When building the Application hexagon, we need to create use cases and input ports to express system capabilities. To make these capabilities available to users and other systems, we need to build input adapters and connect them to input ports.

For the topology and inventory system, we will implement a set of generic input adapters as Java POJO. These generic input adapters are the basis for the technologically specific implementation that takes place in Chapter 12, Using RESTEasy Reactive to Implement Input Adapters. In that chapter, we will reimplement the generic input adapters as RESTEasy-based input adapters using the Quarkus framework.

The input adapter's central role is to receive requests from outside the hexagonal system and fulfill these requests using an input port.

Continuing to develop the topology and inventory system, let's implement the input adapters that receive requests related to router management.

Router management input adapter

We start by creating the RouterManagementGenericAdapter class:

public class RouterManagementGenericAdapter {

    private RouterManagementUseCase

      routerManagementUseCase;

    public RouterManagementGenericAdapter(){

        setPorts();

    }

    /** Code omitted **/

}

We start the RouterManagementGenericAdapter implementation by declaring a class attribute for RouterManagementUseCase. Instead of using an input port class reference, we utilize the use case interface reference, RouterManagementUseCase, to connect to the input port.

On the constructor of RouterManagementGenericAdapter, we call the setPorts method that instantiates RouterManagementInputPort with a RouterManagementH2Adapter parameter as an output port for connection to an H2 in-memory database that the input port uses.

The following is how we should implement the setPorts method:

private void setPorts(){

    this.routerManagementUseCase =

            new RouterManagementInputPort(

            RouterManagementH2Adapter.getInstance()

    );

}

/** Code omitted **/

The setPorts method stores a RouterManagementInputPort object in the RouterManagementUseCase attribute we defined earlier.

After class initialization, we need to create the methods that expose the operations supported by the hexagonal system. The intent here is to receive the request in the input adapter and forward it to an input port by using its use case interface reference:

  1. Here are the operations to retrieve and remove routers from the system:

    /**

    * GET /router/retrieve/{id}

    * */

    public Router retrieveRouter(Id id){

        return routerManagementUseCase.retrieveRouter(id);

    }

    /**

    * GET /router/remove/{id}

    * */

    public Router removeRouter(Id id){

        return routerManagementUseCase.removeRouter(id);

    }

    The comments are to remind us that these operations will be transformed into REST endpoints when integrating Quarkus into the hexagonal system. Both retrieveRouter and removeRouter receive Id as a parameter. Then, the request is forwarded to an input port using a use case reference.

  2. Then, we have the operation to create a new router:

    /**

    * POST /router/create

    * */

    public Router createRouter(Vendor vendor,

                                   Model,

                                   IP,

                                   Location,

                                   RouterType routerType){

        var router = routerManagementUseCase.createRouter(

                null,

                vendor,

                model,

                ip,

                location,

                routerType

       );

       return routerManagementUseCase.persistRouter(router);

    }

    From the RouterManagementUseCase reference, we first call the createRouter method to create a new router, then we persist it using the persistRouter method.

  3. Remember that in the topology and inventory system, only core routers can receive connections from both core and edge routers. To allow the addition and removal of routers on a core router, we define the following two operations:

    /**

    * POST /router/add

    * */

    public Router addRouterToCoreRouter(

        Id routerId, Id coreRouterId){

        Router = routerManagementUseCase.

        retrieveRouter(routerId);

        CoreRouter =

            (CoreRouter) routerManagementUseCase.

            retrieveRouter(coreRouterId);

        return routerManagementUseCase.

                addRouterToCoreRouter(router, coreRouter);

    }

    For the addRouterToCoreRouter method, we pass the routers' Id instances as parameters we intend to add along with the target core router's Id. With these IDs, we call the retrieveRouter method to get the router objects from our data source. Once we have the Router and CoreRouter objects, we handle the request to the input port using a use case reference, by calling addRouterToCoreRouter to add one router into the other. We'll use the following code for this:

    /**

    * POST /router/remove

    * */

    public Router removeRouterFromCoreRouter(

        Id routerId, Id coreRouterId){

        Router =

        routerManagementUseCase.

        retrieveRouter(routerId);

        CoreRouter =

             (CoreRouter) routerManagementUseCase.

             retrieveRouter(coreRouterId);

        return routerManagementUseCase.

                removeRouterFromCoreRouter(router,

                  coreRouter);

    }

    For the removeRouterFromCoreRouter method, we follow the same steps as those of the addRouterToCoreRouter method. The only difference, though, is that at the end, we call removeRouterFromCoreRouter from the use case in order to remove one router from the other.

Let's create now the adapter that handles switch-related operations.

Switch management input adapter

Before we define the methods that expose the switch-related operations, we need to configure the proper initialization of the SwitchManagementGenericAdapter class:

package dev.davivieira.topologyinventory.framework.adapters.input.generic;

import dev.davivieira.topologyinventory.application.ports.input.*

import dev.davivieira.topologyinventory.application.usecases.*;

import dev.davivieira.topologyinventory.domain.entity.*;

import dev.davivieira.topologyinventory.domain.vo.*;

import dev.davivieira.topologyinventory.framework.adapters.output.h2.*;

public class SwitchManagementGenericAdapter {

    private SwitchManagementUseCase

      switchManagementUseCase;

    private RouterManagementUseCase

      routerManagementUseCase;

    public SwitchManagementGenericAdapter(){

        setPorts();

    }

SwitchManagementGenericAdapter is connected to two input ports – the first input port is SwitchManagementInputPort from SwitchManagementUseCase, and the second input port is RouterManagementInputPort from RouterManagementUseCase. That's why we start the class implementation by declaring the attributes for SwitchManagementUseCase and RouterManagementUseCase. We are connecting the switch adapter to the router input port because we want to enforce any persistence activity to happen only through a router. The Router entity, as an aggregate, controls the life cycle of the objects that are related to it.

Next, we implement the setPorts method:

private void setPorts(){

    this.switchManagementUseCase =

            new SwitchManagementInputPort(

            SwitchManagementH2Adapter.getInstance()

    );

    this.routerManagementUseCase =

            new RouterManagementInputPort(

            RouterManagementH2Adapter.getInstance()

    );

}

** Code omitted **

With the setPorts method, we initialize both input ports with the SwitchManagementH2Adapter and RouterManagementH2Adapter adapters to allow access to the H2 in-memory database.

Let's see how to implement the methods that expose the switch-related operations:

  1. We start with a simple operation that just retrieves a switch:

    /**

    * GET /switch/retrieve/{id}

    * */

    public Switch retrieveSwitch(Id switchId) {

        return switchManagementUseCase.retrieveSwitch(switchId);

    }

    The retrieveSwitch method receives Id as a parameter. Then, it utilizes a use case reference to forward the request to the input port.

  2. Next, we have a method that lets us create and add a switch to an edge router:

    /**

    * POST /switch/create

    * */

    public EdgeRouter createAndAddSwitchToEdgeRouter(

           Vendor,

           Model,

           IP,

           Location,

           SwitchType, Id routerId

    ) {

        Switch newSwitch = switchManagementUseCase.

        createSwitch(vendor, model, ip, location,

          switchType);

        Router edgeRouter = routerManagementUseCase.

        retrieveRouter(routerId);

        if(!edgeRouter.getRouterType().equals

          (RouterType.EDGE))

            throw new UnsupportedOperationException(

        "Please inform the id of an edge router to add a

         switch");

        Router = switchManagementUseCase.

        addSwitchToEdgeRouter(newSwitch, (EdgeRouter)

          edgeRouter);

        return (EdgeRouter)

        routerManagementUseCase.persistRouter(router);

    }

    We call the switch input port method, createSwitch, by passing the parameters received by the createAndAddSwitchToEdgeRouter method to create a switch. With routerId, we retrieve the edge router by calling the retrieveRouter method from the router input port. Once we have the Switch and EdgeRouter objects, we can call the addSwitchToEdgeRouter method to add the switch into the edge router. As the last step, we call the persistRouter method to persist the operation in the data source.

  3. Finally, we have the removeSwitchFromEdgeRouter method that allows us to remove a switch from an edge router:

    /**

    * POST /switch/remove

    * */

    public EdgeRouter removeSwitchFromEdgeRouter(

    Id switchId, Id edgeRouterId) {

        EdgeRouter =

                (EdgeRouter) routerManagementUseCase.

                             retrieveRouter(edgeRouterId);

        Switch networkSwitch = edgeRouter.

                               getSwitches().

                               get(switchId);

        Router = switchManagementUseCase.

                        removeSwitchFromEdgeRouter(

                        networkSwitch, edgeRouter);

        return (EdgeRouter) routerManagementUseCase.

        persistRouter(router);

    }

    removeSwitchFromEdgeRouter receives Id as a parameter for the switch and another Id for the edge router. Then, it retrieves the router by calling the retrieveRouter method. With the switch ID, it retrieves the switch object from the edge router object. Once it gets the Switch and EdgeRouter objects, it calls the removeSwitchFromEdgeRouter method to remove the switch from the edge router.

What's left now is to implement the adapter that deals with the topology and inventory networks.

Network management input adapter

As we did with the router and switch adapters, let's implement the NetworkManagementGenericAdapter class by defining first the ports it needs:

package dev.davivieira.topologyinventory.framework.adapters.input.generic;

import dev.davivieira.topologyinventory.application.ports.input.*;

import dev.davivieira.topologyinventory.application.usecases.*;

import dev.davivieira.topologyinventory.domain.entity.Switch;

import dev.davivieira.topologyinventory.domain.vo.*;

import dev.davivieira.topologyinventory.framework.adapters.output.h2.*;

public class NetworkManagementGenericAdapter {

    private SwitchManagementUseCase

      switchManagementUseCase;

    private NetworkManagementUseCase

    networkManagementUseCase;

    public NetworkManagementGenericAdapter(){

        setPorts();

    }

Besides NetworkManagementUseCase, we also use SwitchManagementUseCase. We need to call the setPorts method from the constructor of NetworkManagementGenericAdapter to properly initialize the input ports objects and assign them to their respective use case references. The following is how we implement the setPorts method:

private void setPorts(){

    this.switchManagementUseCase =

             new SwitchManagementInputPort(

             SwitchManagementH2Adapter.getInstance());

    this.networkManagementUseCase =

             new NetworkManagementInputPort(

             RouterManagementH2Adapter.getInstance());

}

/** Code omitted **/

As we have done in previous input adapter implementations, we configure the setPorts method to initialize input port objects and assign them to use case references.

Let's implement the network-related methods:

  1. First, we implement the addNetworkToSwitch method to add a network to a switch:

    /**

    * POST /network/add

    * */

    public Switch addNetworkToSwitch(Network network, Id switchId) {

        Switch networkSwitch = switchManagementUseCase.

                               retrieveSwitch(switchId);

        return networkManagementUseCase.

               addNetworkToSwitch(

               network, networkSwitch);

    }

    The addNetworkToSwitch method receives the Network and Id objects as parameters. To proceed, we need to retrieve the Switch object by calling the retrieveSwitch method. Then, we can call the addNetworkToSwitch method to add the network to the switch.

  2. Then, we implement the method to remove a network from a switch:

    /**

    * POST /network/remove

    * */

    public Switch removeNetworkFromSwitch(

    String networkName, Id switchId) {

        Switch networkSwitch = switchManagementUseCase.

                               retrieveSwitch(switchId);

        return networkManagementUseCase.

               removeNetworkFromSwitch(

               networkName, networkSwitch);

    }

    First, we get a Switch object by calling the retrieveSwitch method with the Id parameter. To remove a network from a switch, we use the network name to find it from a list of networks attached to the switch. We do that by calling the removeNetworkFromSwitch method.

The adapter to manage networks is the last input adapter we had to implement. With these three adapters we can now manage routers, switches, and networks from the Framework hexagon. To make sure these adapters are working well, let's create some tests for them.

Testing the Framework hexagon

By testing the Framework hexagon, we have not just the opportunity to check whether the input and output adapters are working well, but we can also test whether the other hexagons, Domain and Application, are doing their part in response to the requests coming from the Framework hexagon.

To test, we call the input adapters to trigger the execution of everything necessary in downstream hexagons to fulfill the request. We start by implementing tests for the router management adapters. The tests for switches and networks follow the same pattern and are available in the GitHub repository of this book.

For routers, we will put our tests inside the RouterTest class:

public class RouterTest extends FrameworkTestData {

    RouterManagementGenericAdapter

    routerManagementGenericAdapter;

    public RouterTest() {

        this.routerManagementGenericAdapter =

        new RouterManagementGenericAdapter();

        loadData();

    }

    /** Code omitted **/

}

In the RouterTest constructor, we instantiate the RouterManagementGenericAdapter input adapter class that we use to perform the tests. The loadData method loads some test data from the FrameworkTestData parent class.

Once we have correctly configured the requirements of the tests, we can proceed with the testing:

  1. First, we test router retrieval:

    @Test

    public void retrieveRouter() {

        var id = Id.withId(

        "b832ef4f-f894-4194-8feb-a99c2cd4be0c");

        var actualId = routerManagementGenericAdapter.

                       retrieveRouter(id).getId();

        assertEquals(id, actualId);

    }

    We call the input adapter, informing it of the router id we want to retrieve. With assertEquals, we compare the expected ID with the actual ID to see if they match.

  2. To test router creation, we have to implement the createRouter test method:

    @Test

    public void createRouter() {

        var ipAddress = "40.0.0.1";

        var routerId  = this.

                

        routerManagementGenericAdapter.createRouter(

                Vendor.DLINK,

                Model.XYZ0001,

                IP.fromAddress(ipAddress),

                locationA,

                RouterType.EDGE).getId();

        var router = this.routerManagementGenericAdapter.

        retrieveRouter(routerId);

        assertEquals(routerId, router.getId());

        assertEquals(Vendor.DLINK, router.getVendor());

        assertEquals(Model.XYZ0001, router.getModel());

        assertEquals(ipAddress,  

        router.getIp().getIpAddress());

        assertEquals(locationA, router.getLocation());

        assertEquals(RouterType.EDGE,

        router.getRouterType());

    }

    From the router input adapter, we call the createRouter method to create and persist a new router. Then, we call the retrieveRouter method with the ID previously generated by the router we have just created. Finally, we run assertEquals to confirm whether the router retrieved from the data source is indeed the router we created.

  3. To test the addition of a router to a core router, we have the addRouterToCoreRouter test method:

    @Test

    public void addRouterToCoreRouter() {

        var routerId = Id.withId(

        "b832ef4f-f894-4194-8feb-a99c2cd4be0b");

        var coreRouterId = Id.withId(

        "b832ef4f-f894-4194-8feb-a99c2cd4be0c");

        var actualRouter =

        (CoreRouter) this.routerManagementGenericAdapter.

        addRouterToCoreRouter(routerId,coreRouterId);

        assertEquals(routerId,   

        actualRouter.getRouters().get(routerId).getId());

    }

    We pass the variables, routerId and coreRouterId, as parameters to the input adapter's addRouterToCoreRouter method that returns a core router. assertEquals checks whether the core router has the router we added.

  4. To test the removal of a router from a core router, we'll use this code:

    @Test

    public void removeRouterFromCoreRouter(){

        var routerId = Id.withId(

        "b832ef4f-f894-4194-8feb-a99c2cd4be0a");

        var coreRouterId = Id.withId(

        "b832ef4f-f894-4194-8feb-a99c2cd4be0c");

        var removedRouter =

        this.routerManagementGenericAdapter.

        removeRouterFromCoreRouter(routerId,  

        coreRouterId);

        var coreRouter =

        (CoreRouter)this.routerManagementGenericAdapter.

        retrieveRouter(coreRouterId);

        assertEquals(routerId, removedRouter.getId());

        assertFalse(

        coreRouter.getRouters().containsKey(routerId));

    }

    This test is very similar to the previous one. We again use the routerId and coreRouterId variables, but now we also use the removeRouterFromCoreRouter method, which returns the removed router. assertEquals checks whether the removed router's ID matches the ID from the routerId variable.

To run these tests, execute the following command in the Maven project root directory:

mvn test

The output should be similar to the one here:

[INFO]  T E S T S

[INFO] -------------------------------------------------------

[INFO] Running dev.davivieira.topologyinventory.framework.NetworkTest

[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.654 s - in dev.davivieira.topologyinventory.framework.NetworkTest

[INFO] Running dev.davivieira.topologyinventory.framework.RouterTest

[INFO] Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.014 s - in dev.davivieira.topologyinventory.framework.RouterTest

[INFO] Running dev.davivieira.topologyinventory.framework.SwitchTest

[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.006 s - in dev.davivieira.topologyinventory.framework.SwitchTest

Along with RouterTest, we also have tests from SwitchTest and NetworkTest that you can find in the book's GitHub repository, as mentioned before.

By implementing the Framework hexagon tests, we conclude the development of the Framework hexagon and the whole topology and inventory system's backend. Taking what we've learned from this chapter and the previous chapters, we could apply all the techniques covered to create a system following the hexagonal architecture principles.

Summary

We started the Framework hexagon construction by implementing first the output adapters to enable the topology and inventory system to use as its primary data source an H2 in-memory database.

Then, we created three input adapters: one for router operations, another one for switch operations, and the last one for network-related operations. To conclude, we implemented tests to ensure that the adapters and the whole hexagonal system work as expected. By completing the development of the Framework hexagon, we finished the development of our overall hexagonal system.

We can improve the hexagonal system we have created by exploring the possibilities offered by the Java Platform Module System (JPMS). For example, we can leverage the hexagonal modular structure to apply the Dependency Inversion Principle (DIP). By doing so, we can make the hexagonal system more loosely coupled. We shall examine the DIP and other exciting features in the next chapter.

Questions

  1. Which other Java modules does the Framework hexagon Java module depend on?
  2. Why do we need to create the output adapters?
  3. In order to communicate with the input ports, the input adapters instantiate input port objects and assign them to an interface reference. What's that interface?
  4. When we test a Framework hexagon's input adapter, we are also testing other hexagons. Why does that happen?
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.247.159