Share this article:
  

How to automate a Java unit test, including mocking and assertions

java-save-time-02
You can auto-generate a unit test with a single button-click, including all of the mocking and validations. 

Good 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 are 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 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, itself.

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 the LoanStrategy argument, which is used to process the LoanRequest. The strategy object may be complex – it may access a database, an external system, or throw a RuntimeException. To write a test for 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 and LoanRequest configurations.

A unit test for 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(loanRequeststrategy);

// Assertions and other validations


 

As you can see, there’s a whole section of my test which 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, then re-run again and repeat the process over and over. I had to spend 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, more strategies, and more parameters for 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 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 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(loanRequeststrategy);

// Assertions and other validations
}

Let’s look at what’s happening here. We create a mocked instance of LoanStrategy using Mockito.mock(). Since we know that getQualifier() and getThreshold() will be called on the strategy, we define the return values for those calls using Mockito.when(…).thenReturn(). For this the test, we don’t care what the LoanRequest instance’s values are, nor do we need a real AdminManager anymore because 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 LoanStrategy might do. We don’t need to set up test environments, dependencies, or complex objects. We are focused on testing 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 module of Parasoft Jtest, which is an enterprise solution for Java testing, that includes static analysis, unit testing, code coverage and traceability, smart reporting to help you manage the risks of code changes, and more.

On the unit testing side of things, Parasoft Jtest's Unit Test Assistant (abbreviated as UTA below) helps you automate some of the most difficult parts of creating and maintaining unit tests with mocks. For the above example, it 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.

UTA Mocking Blog Image 1The Parasoft Jtest Unit Test Assistant Toolbar with requestLoan() selected

Here, I used the “Regular” action in the Parasoft Jtest Unit Test Assistant Toolbar 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(availableFundsdownPaymentloanAmount);
LoanStrategy strategy = mockLoanStrategy();
LoanResponse result = underTest.requestLoan(loanRequeststrategy);

// 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 Parasoft Jtest Unit Test Assistant detected the method calls to getQualifier() and getThreshold() 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 requestLoan(). The test may need to be updated to specify what validate() should return.

Without changing the generated test, let’s run it within the Parasoft Jtest Unit Test Assistant:

UTA Mocking Blog Image 2The recommendation for validate() in the Parasoft Jtest Unit Test Assistant

The Parasoft Jtest Unit Test Assistant detected that validate() was called on the mocked LoanStrategy argument during my test run. Since the method has not been set up for the mock, the assistant recommends that I mock the validate() 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 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.

Analyzing the Test Flow

The assistant also provides some useful tools for analyzing the test flow. For instance, here is the flow tree for our test run:

UTA Mocking Blog Image 3

The Parasoft Jtest Unit Test Assistant’s Flow Tree, showing calls made during test execution

When the test ran, I can see that the test created a new mock for 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.

Summary

So you an automate many aspects of unit testing. Parasoft Jtest's Unit Test Assistant helps you create and maintain unit tests with less time and effort, helping you reduce the complexity associated with mocking. It 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). You can get a 7 day trial for free to check it out in your own environment if you click below:

Automate JUnit Test Creation and Start to Love Unit Testing

Share this article:
  

Related posts

Submit a Comment

Stay up to date