Simplify Compliance Workflows With New C/C++test 2024.2 & AI-Driven Automation | Register Now

Mocking in Java: How to Automate a Java Unit Test, Including Mocking and Assertions

Headshot of Brian McGlauflin,
July 19, 2018
6 min read

By reducing the complexity involved with mocking, Parasoft Jtest enables you to quickly and easily write unit tests. Learn more about how to automate a Java unit test, including mocking and assertions.

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.

What Makes a Good Unit 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 forrequestLoan()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.

What Is Mocking in Java?

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 a Java Unit Test Generator

We designed Parasoft Jtest to help address the challenges above. The unit testing module Parasoft Jtest, an enterprise solution for Java testing that helps developers manage the risks of Java software development.

On the unit testing side of things, Parasoft Jtest 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.

Screenshot of Jtest, auto-generate a test for with a single button-click, including all of the mocking and validation.
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 – Parasoft Jtest detected the method calls to getQualifier() and getThreshold() and mocked the methods. Once I configure values in my unit 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:

Sample JUnit test with mocking to be run within the Jtest Unit Test Assistant

Parasoft Jtest 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:

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

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 can automate many aspects of unit testing. Parasoft Jtest helps you generate 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). Start your full-featured, 14-day Jtest trial today.

Improve Unit Testing for Java With Automation