How to Automate a Java Unit Test, Including Mocking and Assertions
Want to learn how to automate a Java Unit Test? Check out this post on how to use mocking, assertions, and the Parasoft Jtest Unit Test Assistant to automate unit tests.
Join the DZone community and get the full member experience.
Join For FreeGood unit tests are a great way to make sure that your code works today and continues to work in the future. A comprehensive suite of tests, with good code-based and behavior-based coverage, can save an organization a lot of time and headaches. And, yet, it is not uncommon to see projects where not enough tests were written. In fact, some developers have even been arguing against their use completely.
Where Is the test?
There are many reasons why developers don’t write enough unit tests. One of the biggest reasons is the amount of time that they take to build and maintain, especially in large, complex projects. In complex projects, often a unit test needs to instantiate and configure a lot of objects. This takes a lot of time to set up and can make the test as complex (or more complex) than the code it is testing.
Let’s look at an example in Java:
public LoanResponse requestLoan(LoanRequest loanRequest, LoanStrategy strategy)
{
LoanResponse response = new LoanResponse();
response.setApproved(true);
if (loanRequest.getDownPayment().compareTo(loanRequest.getAvailableFunds()) > 0) {
response.setApproved(false);
response.setMessage("error.insufficient.funds.for.down.payment");
return response;
}
if (strategy.getQualifier(loanRequest) < strategy.getThreshold(adminManager)) {
response.setApproved(false);
response.setMessage(getErrorMessage());
}
return response;
}
Here, we have a method that processes a LoanRequest
, generating a LoanResponse
. Note here the LoanStrategy
argument, which is used to process the LoanRequest
. The strategy object may be complex, because it may access a database, external system, or throw a RuntimeException
. To write a test for the requestLoan()
, I need to worry about which type of LoanStrategy
I am testing with. And, I probably need to test my method with a variety of LoanStrategy
implementations andLoanRequest
configurations.
A unit test for the requestLoan()
may look like this:
@Test
public void testRequestLoan() throws Throwable
{
// Set up objects
DownPaymentLoanProcessor processor = new DownPaymentLoanProcessor();
LoanRequest loanRequest = LoanRequestFactory.create(1000, 100, 10000);
LoanStrategy strategy = new AvailableFundsLoanStrategy();
AdminManager adminManager = new AdminManagerImpl();
underTest.setAdminManager(adminManager);
Map<String, String> parameters = new HashMap<>();
parameters.put("loanProcessorThreshold", "20");
AdminDao adminDao = new InMemoryAdminDao(parameters);
adminManager.setAdminDao(adminDao);
// Call the method under test
LoanResponse response = processor.requestLoan(loanRequest, strategy);
// Assertions and other validations
}
As you can see, there’s a whole section of my test that just creates objects and configures parameters. It wasn’t obvious looking at the requestLoan()
method what objects and parameters need to be set up. To create this example, I had to run the test, add some configuration, and then re-run it again and repeat the process over and over. I spent way too much time figuring out how to configure the AdminManager
and the LoanStrategy,
instead of focusing on my method and what needed to be tested there. And, I still need to expand my test to cover more LoanRequest
cases, strategies, and parameters for the AdminDao
.
Additionally, by using real objects to test with, my test is actually validating more than just the behavior of requestLoan()
— I am depending on the behavior of the AvailableFundsLoanStrategy
, AdminManagerImpl
, and AdminDao
in order for my test to run effectively. I am testing those classes, too. In some cases, this is desirable, but, in other cases, it is not. Plus, if one of those other classes change, the test may start failing even though the behavior of the requestLoan()
didn’t change. For this test, we would rather isolate the class under test from its dependencies.
Using Mock Objects
One solution for the complexity problem is to mock those complex objects. For this example, I will start by using a mock for the LoanStrategy
parameter:
@Test
public void testRequestLoan() throws Throwable
{
// Set up objects
DownPaymentLoanProcessor processor = new DownPaymentLoanProcessor();
LoanRequest loanRequest = LoanRequestFactory.create(1000, 100, 10000);
LoanStrategy strategy = Mockito.mock(LoanStrategy.class);
Mockito.when(strategy.getQualifier(any(LoanRequest.class))).thenReturn(20.0d);
Mockito.when(strategy.getThreshold(any(AdminManager.class))).thenReturn(20.0d);
// Call the method under test
LoanResponse response = processor.requestLoan(loanRequest, strategy);
// Assertions and other validations
}
Let’s look at what’s happening here. We create a mocked instance of the LoanStrategy
using Mockito.mock()
. Since we know that the getQualifier()
and the getThreshold()
will be called on the strategy, we define the return values for those calls using Mockito.when(…).thenReturn()
. For this test, we don’t care what the LoanRequest
instance’s values are, nor do we need a real AdminManager
anymore, because the AdminManager
was only used by the real LoanStrategy
.
Additionally, since we aren't using a real LoanStrategy
, we don’t care what the concrete implementations of the LoanStrategy
might do. We don’t need to set up test environments, dependencies, or complex objects. We are focused on testing the requestLoan()
– not LoanStrategy
or AdminManager
. The code-flow of the method under test is directly controlled by the mock.
This test is a lot easier to write with Mockito than it would have been if I had to create a complex LoanStrategy
instance. But, there are still some challenges:
- For complex applications, tests may require lots of mocks
- If you are new to Mockito, you need to learn its syntax and patterns
- You may not know which methods need to be mocked
- When the application changes, the tests (and mocks) need to be updated too
Solving Mocking Challenges With Parasoft Jtest
We created the Parasoft Jtest Unit Test Assistant to help address the challenges above. The Unit Test Assistant is a component of Parasoft Jtest, which helps automate some of the most difficult parts of creating and maintaining unit tests with mocks. For the above example, the Unit Test Assistant can auto-generate a test for requestLoan()
with a single button-click, including all of the mocking and validations you see in the example test.
Unit Test Assistant’s Toolbar with requestLoan()
was selected. I used the “Regular” action in the Unit Test Assistant (UTA) to generate the following test:
@Test
public void testRequestLoan() throws Throwable
{
// Given
DownPaymentLoanProcessor underTest = new DownPaymentLoanProcessor();
// When
double availableFunds = 0.0d; // UTA: default value
double downPayment = 0.0d; // UTA: default value
double loanAmount = 0.0d; // UTA: default value
LoanRequest loanRequest = LoanRequestFactory.create(availableFunds, downPayment, loanAmount);
LoanStrategy strategy = mockLoanStrategy();
LoanResponse result = underTest.requestLoan(loanRequest, strategy);
// Then
// assertNotNull(result);
}
All the mocking for this test happens in a helper method:
private static LoanStrategy mockLoanStrategy() throws Throwable
{
LoanStrategy strategy = mock(LoanStrategy.class);
double getQualifierResult = 0.0d; // UTA: default value
when(strategy.getQualifier(any(LoanRequest.class))).thenReturn(getQualifierResult);
double getThresholdResult = 0.0d; // UTA: default value
when(strategy.getThreshold(any(AdminManager.class))).thenReturn(getThresholdResult);
return strategy;
}
All the necessary mocking is set up for me — the Unit Test Assistant detected the method calls to getQualifier()
andgetThreshold()
and mocked the methods. Once I configure values in my test for availableFunds, downPayment, etc, the test is ready to run (I could also generate a parameterized test for better coverage!). Note also that the assistant provides some guidance as to which values to change by its comments, “UTA: default value,” making testing easier.
This saves a lot of time in generating tests, especially if I don’t know what needs to be mocked or how to use the Mockito API.
Handling Code Changes
When the application logic changes, the tests often need to change also. If the test is well-written, it should fail if you update the code without updating the test. Often, the biggest challenge in updating the test is understanding what needs to be updated and how exactly to perform that update. If there are lots of mocks and values, it can be difficult to track down what the necessary changes are.
To illustrate this, let’s make some changes to the code under test:
public LoanResponse requestLoan(LoanRequest loanRequest, LoanStrategy strategy)
{
...
String result = strategy.validate(loanRequest);
if (result != null && !result.isEmpty()) {
response.setApproved(false);
response.setMessage(result);
return response;
}
...
return response;
}
We have added a new method to LoanStrategy – validate()
and are now calling it from the requestLoan()
. The test may need to be updated to specify whatvalidate()
should return.
Without changing the generated test, let’s run it within the Unit Test Assistant:
The Unit Test Assistant detected thatvalidate()
was called on the mocked LoanStrategy
argument during my test run. Since the method has not been set up for the mock, the Unit Test Assistant recommends that I mock thevalidate()
method. The “Mock it” quick-fix action updates the test automatically. This is a simple example. But, for complex code where it isn’t easy to find the missing mock, the recommendation and quick-fix can save us a lot of debugging time.
After updating the test using the quick-fix, I can see the new mock and set the desired value for validateResult
:
private static LoanStrategy mockLoanStrategy() throws Throwable
{
LoanStrategy strategy = mock(LoanStrategy.class);
String validateResult = ""; // UTA: default value
when(strategy.validate(any(LoanRequest.class))).thenReturn(validateResult);
double getQualifierResult = 20.0d;
when(strategy.getQualifier(any(LoanRequest.class))).thenReturn(getQualifierResult);
double getThresholdResult = 20.0d;
when(strategy.getThreshold(any(AdminManager.class))).thenReturn(getThresholdResult);
return strategy;
}
I can configure the validateResult
with a non-empty value to test the use-case where the method enters the new block of code, or I can use an empty value (or null) to validate behavior when the new block is not entered.
The Unit Test Assistant also provides some useful tools for analyzing the test flow. For instance, here is the flow tree for our test run:
When the test ran, I can see that the test created a new mock for the LoanStrategy
and mocked the validate()
, getQualifier()
, and getThreshold()
methods. I can select method calls and see (in the variables view) what the arguments were sent to that call, and what value was returned (or exceptions thrown). When debugging tests, this can be much easier to use and understand than digging through logfiles.
The Parasoft Jtest Unit Test Assistant helps create and maintain unit tests with less time and effort, helping you reduce the complexity associated with mocking. The Unit Test Assistant also makes many other kinds of recommendations to improve existing tests based on runtime data, and has support for parameterized tests, Spring application tests, and PowerMock (for mocking static methods and constructors). In addition, Parasoft Jtest provides other functionality, such as coverage data, static analysis, and integrations with sophisticated reporting and analytics tools, such as Parasoft DTP.
Published at DZone with permission of Brian McGlauflin, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments