Skip to main content
Version: 10.2.9

Write unit tests in ODF

To develop Java unit tests, ODF provides JUnit 5 (Jupiter) and Mockito libraries.

Unit testing benefits

  • Unit testing increases confidence in changing/maintaining code. If good unit tests are written and if they are run every time any code is changed, we will be able to promptly catch any defects introduced due to the change. Also, if codes are already made less interdependent to make unit testing possible, the unintended impact of changes to any code is less.
  • Development is faster. How? If you do not have unit testing in place, you write your code and perform that fuzzy "developer test", when you set some breakpoints, fire up the GUI, provide a few inputs that hopefully hit your code, and hope that you are all set. But, if you have unit testing in place, you write the test, write the code, and run the test. Writing tests takes time but the time is compensated by the less amount of time it takes to run the tests. Also, you need not fire up the GUI and provide all those inputs. And, of course, unit tests are more reliable than "developer tests".
  • Development is faster in the long run too. How? The effort required to find and fix defects found during unit testing is much less in comparison to the effort required to fix defects found during system testing or acceptance testing.
  • The cost of fixing a defect detected during unit testing is lesser in comparison to that of defects detected at higher levels. Compare the cost (time, effort, destruction, humiliation) of a defect detected during acceptance testing or when the software is live.
  • Debugging is easy. When a test fails, only the latest changes need to be debugged. With testing at higher levels, changes made over the span of several days, weeks, or months need to be scanned.

Common use cases

Override private field

Sometimes, we need to override the private field of the tested object, for example, when this field is initialized by the code which requires a real environment, and by default this field would be initialized improperly.

private DBTransactionServiceImpl service;


@BeforeEach
public void init() throws Exception {
// "dsProperties" field is initialized improperly by default (because it gets created from mocked object)
Field dsPropertiesField = DBTransactionServiceImpl.class.getDeclaredField("dsProperties");
dsPropertiesField.setAccessible(true);
dsPropertiesField.set(service, dsProperties);

Override method

Sometimes, we need to override the method (public or protected) of the tested object, for example, when this method fails in a testing environment.

private DBTransactionServiceImpl service;


@BeforeEach
public void init() throws Exception {
...
// NOTE we need to wrap the object creation by Mockito.spy(...) to be able to override methods by Mockito API
service = Mockito.spy(new DBTransactionServiceImpl(logger, binding));
}

@Test
public void testStartTransaction() throws Exception {
// Override DBTransactionServiceImpl::getConnection() to return Connection mock
Connection connection = Mockito.mock(Connection.class);
Mockito.doReturn(connection).when(service).getConnection();
...
}

There are some specifics of unit tests implementation for connectors and processors and for other Intake classes.

Java unit tests development: connectors and processors

The specific of Intake components (connectors and processors) unit testing is that the components constructors parameters are injected. Let's consider the ShareFolderTransactionSupplier component class and its constructor:

    @Inject
public ShareFolderTransactionSupplier(final S3Manager s3Manager, final SmbFileBuilder smbFileBuilder,
final Logger logger, final Binding binding,
@Named("botConfigParams") final Map<String, String> params) {
...

You won't find the code, which explicitly initializes this class instance and invokes this constructor.

When developing such components unit tests, the following approach is used in most of the cases (all the below code excerpts belong to the ShareFolderTransactionSupplierTest unit test class).

  1. In your unit test, inject the instance of the class which is the object to test and declare all the constructor parameters as instance properties.

    @Inject
    private ShareFolderTransactionSupplier connector;
private SmbFileBuilder smbFileBuilder;
private S3Manager s3Manager;
private Logger logger;
private Binding binding;
private Map<String, String> params;
```
note

Use the component class, not interface.

  1. All the properties injected into the instance are mocked in the unit test init() method:

        @BeforeEach
    public void init() {
    s3Manager = Mockito.mock(S3Manager.class);
    logger = LoggerFactory.getLogger(ShareFolderTransactionSupplierTest.class);
    smbFileBuilder = Mockito.mock(SmbFileBuilder.class);
    binding = Mockito.mock(Binding.class);
    params = Mockito.mock(Map.class);
    ...
    }
  2. To provide properties to the Feather dependency injection library, a test module is implemented in the unit test init() method:

        @BeforeEach
    public void init() {
    ...
    Module testModule = new Module() {
    @Provides
    public S3Manager s3Manager() {
    return s3Manager;
    }

    @Provides
    public Logger logger() {
    return logger;
    }

    @Provides
    public SmbFileBuilder smbFileBuilder() {
    return smbFileBuilder;
    }

    @Provides
    @Named("botConfigParams")
    public Map<String, String> params() {
    return params;
    }
    };
    }
  3. The final test case initialization step is to provide the test module as override:

        @BeforeEach
    public void init() {
    ...
    Intake.init(binding).params(params).override(testModule).injectFields(this).get();
    }

    The testModule above will override the default Intake Module and additional modules.

  4. The typical unit test may look as follows. Take a look at the Java comments in the code below.

    @Test
    public void testGetTransactions() throws Exception {
    //given
    ...

    // Mocked properties are 'fine-tuned' additionally
    Mockito.when(params.get(ShareFolderTransactionSupplier.PARAM_HOST)).thenReturn(TEST_SMB_HOST);
    Mockito.when(params.get(ShareFolderTransactionSupplier.PARAM_PATH)).thenReturn("/");

    // 's3Manager' field mock
    Mockito.when(s3Manager.putFile(ArgumentMatchers.any(), ArgumentMatchers.any(), ArgumentMatchers.any())).thenReturn("s3-file.ext");

    // Invoke tested class instance method
    //when
    List<Transaction> transactions = new ArrayList<>(connector.get());

    //then
    Assertions.assertNotNull(transactions);
    Assertions.assertTrue(transactions.size() == 3);
    ...
    }