In software development, writing test code is just as important as writing the application code itself. An application without tests will quickly turn into a nightmare for the maintainers because every change needed, no matter how harmless it may seem, something may stop working as it should and, in many cases, that will only be noticed when the application is already in production. In addition, there will be a significant increase in productivity when running thousands of tests in a few seconds. And contrary to what it may seem, some big companies still manually test their applications.
To be able to run these tests automatically, the code has to be prepared and one of the options that can be used is Dependency Injection. In this tutorial, developers will learn how to create an application using MicroProfile and the JSR 365: Contexts and Dependency Injection for Java™ 2.0 (CDI) specification to write and maintain code that is testable and loosely coupled.
The Application
The application consists of a microservice that returns the profit from a stock portfolio. The MicroProfile implementation that will be used in this example is Quarkus.
The source code is on GitHub. In the repository, there are two directories: “initial”, which is the application already working, but without the necessary refactoring for testing, and “complete”, which is the final result we will have after the refactoring.
To write the application from scratch, the following command can be run:
mvn io.quarkus:quarkus-maven-plugin:3.6.3:create -DprojectGroupId=com.hbelmiro -DprojectArtifactId=microprofile-cdi -DclassName="com.hbelmiro.microprofile.cdi.ProfitResource" -Dpath="/profit"
So let’s create a class called Portfolio that will calculate the profit for our stock portfolio. For this, two other services will be needed: one to provide us with the current value of our shares (StocksService) and another to provide us with our current position – the number of shares we have for each company – (PositionsLoader).
package com.hbelmiro.microprofile.cdi; import java.math.BigDecimal; public class Portfolio { private final StocksService stocksService = new StocksService(); private final PositionsLoader positionsLoader = new PositionsLoader(); public BigDecimal computePortfolioProfit() { return this.positionsLoader.load().stream() .map(this::computePositionProfit) .reduce(BigDecimal::add) .orElse(BigDecimal.ZERO); } private BigDecimal computePositionProfit(Position position) { return this.stocksService.getCurrentValue(position.getTicker()) .subtract(position.getAveragePrice()) .multiply(position.getQuantity()); } }
To simulate the oscillation of values and quantities in the portfolio, we will implement the services StocksService and PositionsLoader returning random values for each call.
package com.hbelmiro.microprofile.cdi; import java.math.BigDecimal; import java.util.Random; public class StocksService { public BigDecimal getCurrentValue(String ticker) { return BigDecimal.valueOf(new Random().nextInt(5000)); } }
package com.hbelmiro.microprofile.cdi; import java.math.BigDecimal; import java.util.List; import java.util.Random; public class PositionsLoader { private static final Random RANDOM = new Random(); public List<Position> load() { return List.of( new Position("AAPL", nextRandomNumber(), nextRandomNumber()), new Position("GOOG", nextRandomNumber(), nextRandomNumber()), new Position("AMZN", nextRandomNumber(), nextRandomNumber()) ); } private BigDecimal nextRandomNumber() { return BigDecimal.valueOf(RANDOM.nextInt(5000)); } }
package com.hbelmiro.microprofile.cdi; import java.math.BigDecimal; public final class Position { private final String ticker; private final BigDecimal quantity; private final BigDecimal averagePrice; public Position(String ticker, BigDecimal quantity, BigDecimal averagePrice) { this.ticker = ticker; this.quantity = quantity; this.averagePrice = averagePrice; } public String getTicker() { return this.ticker; } public BigDecimal getQuantity() { return this.quantity; } public BigDecimal getAveragePrice() { return this.averagePrice; } }
So let’s implement our endpoint:
package com.hbelmiro.microprofile.cdi; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("/profit") @Produces(MediaType.TEXT_PLAIN) public class ProfitResource { private final Portfolio portfolio = new Portfolio(); @GET public String get() { return "Current profit is: " + this.portfolio.computePortfolioProfit(); } }
That’s all we need to get our microservice working. So let’s run it using the following command:
./mvnw compile quarkus:dev
If you see an output similar to the following, our application is ready to receive requests.
__ ____ __ _____ ___ __ ____ ______ --/ __ \/ / / / _ | / _ \/ //_/ / / / __/ -/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \ --\___\_\____/_/ |_/_/|_/_/|_|\____/___/ 2020-08-19 21:07:14,601 INFO [io.quarkus] (Quarkus Main Thread) microprofile-cdi 1.0-SNAPSHOT on JVM (powered by Quarkus 3.6.3 started in 0.963s. Listening on: http://0.0.0.0:8080 2020-08-19 21:07:14,621 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated. 2020-08-19 21:07:14,621 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy]
We can see the profit from our stock portfolio by opening http://0.0.0.0:8080/profit in the browser or using the following command:
curl http://localhost:8080/profit
Note that with each run, the amount of profit returned is different.
But how do we know if our calculation is correct? In this case, we cannot even test it manually, as the stock values and the number of them in our portfolio are random.
So we need to create a test for the Portfolio class, but note that the class depends on two other classes to perform the profit calculation. These other two classes are StocksService and PositionsLoader. We then have to make sure that these two dependencies always return the same values so that we can say what would be the expected profit to be returned by the calculation. We will do this using a pattern called Dependency Injection.
Injecting Dependencies into the Portfolio Class
We will change the Portfolio class so that it is no longer responsible for creating its StocksService and PositionsLoader dependencies. For that, we will use JSR 365: CDI 2.0 which is one of the MicroProfile specifications. With that specification, we can annotate our dependencies with @Inject and the Dependency Injection Container will take care of providing an instance. So we can ask the Container to inject instances that always return the same values in our test. Also, our Portfolio class will be in charge of doing only what is its concern, not having to know how an instance of StocksService or PositionsLoader is created.
We can do that in two ways: annotating the attributes with @Inject and removing their initialization, or annotating the constructor and receiving the instances as an argument. In this case, we will use the second option. Our Portfolio class will look like this:
package com.hbelmiro.microprofile.cdi; import jakarta.inject.Inject; import java.math.BigDecimal; public class Portfolio { private final StocksService stocksService; private final PositionsLoader positionsLoader; @Inject public Portfolio(StocksService stocksService, PositionsLoader positionsLoader) { this.stocksService = stocksService; this.positionsLoader = positionsLoader; } public BigDecimal computePortfolioProfit() { return this.positionsLoader.load().stream() .map(this::computePositionProfit) .reduce(BigDecimal::add) .orElse(BigDecimal.ZERO); } private BigDecimal computePositionProfit(Position position) { return this.stocksService.getCurrentValue(position.getTicker()) .subtract(position.getAveragePrice()) .multiply(position.getQuantity()); } }
Note that now, our ProfitResource class has a compilation error, since it creates an instance of Portfolio using the default constructor, which no longer exists. This is one of the disadvantages of a class knowing how to instantiate its dependencies. Now that we have changed the Portfolio constructor, we will also have to change all classes that use that constructor, which wouldn’t happen if we were already using Dependency Injection.
We will change the ProfitResource class so that it also has its dependencies injected automatically. For this, we will remove the initialization from the portfolio attribute and we will annotate it with @Inject. Note that now we will not receive the instance from the constructor and the attribute can no longer be final. If this is a problem, we have the alternative to inject using the constructor, just like we did in the Portfolio class. These are two different ways to inject dependencies, each one with its advantages and disadvantages.
package com.hbelmiro.microprofile.cdi; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; @Path("/profit") @Produces(MediaType.TEXT_PLAIN) public class ProfitResource { @Inject Portfolio portfolio; @GET public String get() { return "Current profit is: " + this.portfolio.computePortfolioProfit(); } }
Also, note that the attribute is no longer private. This was done because we are using Quarkus as a MicroProfile implementation and the use of reflection is a limitation if we want to use native images. You can read more about this in the Quarkus documentation.
The next thing to be done is to define the scope of our dependencies. When we say that an object must be injected, we have to inform the context in which the object must exist. The scopes provided by the specification are as follows:
In our case, the instances will exist in the context of the application, that is, a single instance of the object will be shared throughout the application’s life cycle, similar to Singleton design pattern. We will then have ProfitResource, Portfolio, StocksService, and PositionsLoader classes annotated with @ApplicationScoped. When we access our endpoint again, we can see that it still works and now with our dependencies having been instantiated automatically.
Testing Portfolio Class
Now that we’ve changed our classes so that they have their dependencies injected automatically, let’s create the following test class:
package com.hbelmiro.microprofile.cdi; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; import jakarta.inject.Inject; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; @QuarkusTest class PortfolioTest { @Inject Portfolio portfolio; @Test void computePortfolioProfit() { assertEquals(BigDecimal.valueOf(520), portfolio.computePortfolioProfit()); } }
If we run the test, it will fail, because with each run the profit returned is different.
org.opentest4j.AssertionFailedError: Expected :520 Actual :1235796
What we will do then is ask the Dependency Injection Container to inject services that always return the same values. But our Portfolio class still has a problem, it depends directly on StocksService and PositionsLoader, which are implementations. So let’s change the Portfolio class so that it depends on interfaces instead of relying on implementations. Consequently, we will be able to inject any service, as long as this service implements the interfaces on which the Portfolio class depends.
So let’s change our StocksService and PositionsLoader classes to become interfaces and move implementations to new classes that implement these new interfaces. Our new classes and interfaces should look like the following:
package com.hbelmiro.microprofile.cdi; import java.util.List; public interface PositionsLoader { List<Position> load(); }
package com.hbelmiro.microprofile.cdi; import jakarta.enterprise.context.ApplicationScoped; import java.math.BigDecimal; import java.util.List; import java.util.Random; @ApplicationScoped public class PositionsLoaderImpl implements PositionsLoader { private static final Random RANDOM = new Random(); @Override public List<Position> load() { return List.of( new Position("AAPL", nextRandomNumber(), nextRandomNumber()), new Position("GOOG", nextRandomNumber(), nextRandomNumber()), new Position("AMZN", nextRandomNumber(), nextRandomNumber()) ); } private BigDecimal nextRandomNumber() { return BigDecimal.valueOf(RANDOM.nextInt(5000)); } }
package com.hbelmiro.microprofile.cdi; import java.math.BigDecimal; public interface StocksService { BigDecimal getCurrentValue(String ticker); }
package com.hbelmiro.microprofile.cdi; import jakarta.enterprise.context.ApplicationScoped; import java.math.BigDecimal; import java.util.Random; @ApplicationScoped public class StocksServiceImpl implements StocksService { @Override public BigDecimal getCurrentValue(String ticker) { return BigDecimal.valueOf(new Random().nextInt(5000)); } }
Now our Portfolio class is no longer coupled to specific implementations. We will then create implementations of StocksService and PositionsLoader in the test module that always return the same values. That way we can inject them into Portfolio and we will know what the profit should be returned.
package com.hbelmiro.microprofile.cdi; import jakarta.enterprise.context.ApplicationScoped; import java.math.BigDecimal; @ApplicationScoped public class FakeStocksService implements StocksService { @Override public BigDecimal getCurrentValue(String ticker) { return BigDecimal.valueOf(100); } }
package com.hbelmiro.microprofile.cdi; import jakarta.enterprise.context.ApplicationScoped; import java.math.BigDecimal; import java.util.List; @ApplicationScoped public class FakePositionsLoader implements PositionsLoader { @Override public List<Position> load() { return List.of( new Position("AAPL", BigDecimal.TEN, BigDecimal.valueOf(60)), new Position("GOOG", BigDecimal.valueOf(3), BigDecimal.valueOf(110)), new Position("AMZN", BigDecimal.valueOf(2), BigDecimal.valueOf(25)) ); } }
Now you may be asking yourself: How will the Dependency Injection Container know which implementation should be injected into Portfolio? That’s exactly the reason why our test will fail if we run it now.
java.lang.RuntimeException: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors [error]: Build step io.quarkus.arc.deployment.ArcProcessor#validate threw an exception: jakarta.enterprise.inject.spi.DeploymentException: Found 2 deployment problems: [1] Ambiguous dependencies for type com.hbelmiro.microprofile.cdi.StocksService and qualifiers [@Default] - java member: com.hbelmiro.microprofile.cdi.Portfolio#() - declared on CLASS bean [types=[com.hbelmiro.microprofile.cdi.Portfolio, java.lang.Object], qualifiers=[@Default, @Any], target=com.hbelmiro.microprofile.cdi.Portfolio] - available beans: - CLASS bean [types=[com.hbelmiro.microprofile.cdi.FakeStocksService, com.hbelmiro.microprofile.cdi.StocksService, java.lang.Object], qualifiers=[@Default, @Any], target=com.hbelmiro.microprofile.cdi.FakeStocksService] - CLASS bean [types=[com.hbelmiro.microprofile.cdi.StocksServiceImpl, com.hbelmiro.microprofile.cdi.StocksService, java.lang.Object], qualifiers=[@Default, @Any], target=com.hbelmiro.microprofile.cdi.StocksServiceImpl] [2] Ambiguous dependencies for type com.hbelmiro.microprofile.cdi.PositionsLoader and qualifiers [@Default] - java member: com.hbelmiro.microprofile.cdi.Portfolio# () - declared on CLASS bean [types=[com.hbelmiro.microprofile.cdi.Portfolio, java.lang.Object], qualifiers=[@Default, @Any], target=com.hbelmiro.microprofile.cdi.Portfolio] - available beans: - CLASS bean [types=[com.hbelmiro.microprofile.cdi.FakePositionsLoader, com.hbelmiro.microprofile.cdi.PositionsLoader, java.lang.Object], qualifiers=[@Default, @Any], target=com.hbelmiro.microprofile.cdi.FakePositionsLoader] - CLASS bean [types=[com.hbelmiro.microprofile.cdi.PositionsLoaderImpl, com.hbelmiro.microprofile.cdi.PositionsLoader, java.lang.Object], qualifiers=[@Default, @Any], target=com.hbelmiro.microprofile.cdi.PositionsLoaderImpl]
We have ambiguous dependencies for StocksService and PositionsLoader. For each of them, we have two classes that can be injected. So we need to specify which implementation has to be used in our test. We can do that by annotating our dependencies with qualifiers, which are annotations created by the developer. These annotations must be annotated with @Qualifier. We will then create our qualifier, which we will call TestMode.
package com.hbelmiro.microprofile.cdi; import jakarta.inject.Qualifier; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Qualifier @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) public @interface TestMode { }
Next, we annotate with @TestMode the implementations we want to use in our test.
package com.hbelmiro.microprofile.cdi; import jakarta.enterprise.context.ApplicationScoped; import java.math.BigDecimal; @ApplicationScoped @TestMode public class FakeStocksService implements StocksService { @Override public BigDecimal getCurrentValue(String ticker) { return BigDecimal.valueOf(100); } }
package com.hbelmiro.microprofile.cdi; import jakarta.enterprise.context.ApplicationScoped; import java.math.BigDecimal; import java.util.List; @ApplicationScoped @TestMode public class FakePositionsLoader implements PositionsLoader { @Override public List<Position> load() { return List.of( new Position("AAPL", BigDecimal.TEN, BigDecimal.valueOf(60)), new Position("GOOG", BigDecimal.valueOf(3), BigDecimal.valueOf(110)), new Position("AMZN", BigDecimal.valueOf(2), BigDecimal.valueOf(25)) ); } }
We also annotate the attribute in our test with @TestMode.
package com.hbelmiro.microprofile.cdi; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; import jakarta.inject.Inject; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; @QuarkusTest class PortfolioTest { @Inject @TestMode Portfolio portfolio; @Test void computePortfolioProfit() { assertEquals(BigDecimal.valueOf(520), portfolio.computePortfolioProfit()); } }
Finally, we create a Portfolio producer that will be used by the Dependency Injection Container to create instances for dependencies annotated with @TestMode. This producer will use dependencies also annotated with @TestMode to create an instance of Portfolio.
package com.hbelmiro.microprofile.cdi; import jakarta.enterprise.inject.Produces; import jakarta.inject.Inject; public class TestModePortfolioFactory { @Inject @TestMode StocksService fakeStocksService; @Inject @TestMode PositionsLoader fakePositionsLoader; @Produces @TestMode public Portfolio createPortfolio() { return new Portfolio(this.fakeStocksService, this.fakePositionsLoader); } }
If we run our test now, we will see that it will pass.
Conclusion
In the microservices era, the market is changing faster and faster, requiring agility from our systems to meet the changes promptly. The need for speed requires that our applications be tested quickly, efficiently, and those tests be reliable. Developers need to reduce the coupling between the classes, and Dependency Injection allows coders to reach low coupling. One of the features that MicroProfile provides us in an agnostic way is precisely that, with the JSR 365.
If you want to learn more about JSR 365, you can read the documentation. In addition to what was covered in this post, the specification provides other interesting features like Decorators, Interceptors, and Events.
Remember that the JSR 365 is also part of Jakarta® EE, so the concepts that were covered in this post also apply to applications using Jakarta® EE.