{{announcement.body}}
{{announcement.title}}

Unit Test Insanity

DZone 's Guide to

Unit Test Insanity

Are you repeating your unit tests in an effort to expect a different result?

· Java Zone ·
Free Resource

Are you repeating your unit tests in an effort to expect a different result?

While moving our daughter into her new apartment, which happens to be on the 9th floor of the rental complex, I noticed an interesting situation while waiting for the elevator to arrive. As others walked up to wait for the elevator, they would push the button with the universal "arrow pointing up" symbol — even though we had already pushed the button to request the elevator's service.  

I began to wonder about the thought process at play here. Did the person feel like the elevator would arrive quicker if the up button is pushed again? Was a different result expected?

You may also like: 7 Tips for Writing Better Unit Tests in Java

Believe it or not, this reminded me of something I call "unit test insanity."

Unit Test Insanity

Since I saw the value in writing my first unit test decades ago, I have always been an advocate of including unit test coverage in order to validate the functionality of the system under test (SUT). However, like nearly everything in Information Technology, there is an opportunity to over-build to the point where the value of such enhancements approaches zero.

In this article, I want to focus on the scenario where unit test coverage is added — which duplicates a suite of tests already in use. When this happens, the testing suite ends up performing the same test over and over again — as if a different result is expected.

Insanity.

An Example

In my "Avoid Reloading Application Context on Unit Tests" article in the Microservices Zone, I provided the following service class implementation example:

Java




x
15


 
1
@RequiredArgsConstructor
2
@Service
3
public class WidgetServiceImpl implements WidgetService {
4
  private final AccountService accountService;
5
  private final WidgetRepository widgetRepository;
6
  
7
  /**
8
  * {@inheritDoc}
9
  */
10
  @Override
11
  public List<Widget> getWidgetsByAccountId(Long accountId, String authId) throws AccountException {
12
    Account account = accountService.getAccountById(accountId, authId);
13
    return widgetRepository.getWidgetsByAccountId(accountId);
14
  }
15
}



In this very simple example, before the List<Widget> can be returned, the AccountService is called to provide some level of authorization/validation that the authId has proper clearance to see widgets related to the provided accountId.

If proper clearance is not achieved, the AccountService will throw an AccountException. This exception is simply passed on to whatever is calling thegetWidgetsByAccountId() method.

Unit Test Coverage

In the example above, when the AccountService was introduced, the developer would have included unit tests which cover all aspects of the getAccountById() method.  

Some unit test cases are listed below:

  • A valid authId, which has proper access to a valid accountId, returning an Account object
  • Invalid accountId, throwing an AccountException
  • Invalid authId, throwing an AccountException
  • AuthId does not have access to accountId, throwing an AccountException

With respect to the getWidgetsByAccountId() method, there is certainly a need to mock the  AccountService and create a  when() case for when the accountId and  authId are valid values and an Account object is returned.  

An example of the very simple unit test may appear as follows:

Java




xxxxxxxxxx
1
27


 
1
public class WidgetServiceTest extends BaseServiceTest {
2
  private final AccountService accountService = Mockito.mock(AccountService.class);
3
  private final WidgetRepository widgetRespository = Mockito.mock(WidgetRepository.class);
4
  WidgetServiceImpl widgetService = new WidgetService(accountService, widgetRepository);
5
  
6
7
  @Test
8
  void testGetWidgetsByAccountId() throws Exception {
9
    long accountId = 1L;
10
    long widgetId = 2L;
11
    String authId = "notARealAuthId";
12
13
    Account account = new Account();
14
    account.setId(accountId);
15
16
    List<Widget> widgets = new ArrayList<>();
17
    Widget widget = new Widget();
18
    widget.setId(widgetId);
19
    widgets.add(widget);
20
21
    when(accountService.getAccountById(accountId, authId)).thenReturn(account);
22
    when(widgetRepository.getWidgetsByAccountId(accountId)).thenReturn(widgets);
23
24
    List<Widget> testWidgets = widgetService.getWidgetsByAccountId(accountId, authId);
25
26
    assertEquals(widgets, testWidgets);
27
  }
28
}



In this example, there is really not a need to test the use cases which simply throw an  AccountException.  This is because that result is not really part of the system under test (SUT) and is already assumed to be covered in the AccountServiceTest class.

Conclusion

When writing unit tests, it is always important to maintain a focus on testing only the system under test (SUT).  In cases where the method being tested can be impacted by an exception being thrown by an injected service, that service exception does not need to be tested if the result of the exception merely forwards the exception to method calling the SUT. To me, doing so is an example of "unit test insanity."

The exception to this rule is when the dependent exception is caught and the flow of the SUT is altered.  In this case, throwing the exception is necessary — just as valid mocked data is necessary — but the focus on the test should be to validate that the logic in the SUT is working as expected.

Have a really great day!

Further Reading

7 Tips for Writing Better Unit Tests in Java

Best Java Unit Testing Frameworks

Java Unit Testing Best Practices: How to Get the Most Out of Your Test Automation

Topics:
java ,unit test ,duplication

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}