Search code examples
javaunit-testingjunitmockitojunit4

Unit testing constructor initialization and public logic


Suppose you have the following class:

public class RepositoryImpl implements Repository {
  private static final Object DEFAULT_OBJECT = new Object();

  private final Persistence persistence;
  private volatile Object cachedObject; // maybe ignore that this is volatile and non-final

  public RepositoryImpl(Persistence persistence) {
    this.persistence = persistence;
    this.cachedObject = getInitialCachedObject();
  }

  private Object getInitialCachedObject() {
    try {
      return persistence.get();
    } catch (ObjectNotFoundException e) {
      persistence.persist(DEFAULT_OBJECT);
      return DEFAULT_OBJECT;
    }
  }

  public Object update() { /*some logic*/ }
  public Object get() { /*some logic*/ }
  public Object delete() { /*some logic*/ }
}

Then I want to unit test the Repository class and I mock out the persistence. I want to have probably 2 tests that test the initialization logic (happy path and exception) and 3 more tests for the public methods.

The question is how should I approach the testing of this?

The possible options that I managed to think of are:

  1. Consider calling the initialization from outside through a public method after ctor
    • breaks the immutability (in my particular case this is already broken as cachedObject needs to be volatile and not final, but in the general case.. yeah)
  2. Create the RepositoryImpl in each test case instead of using @InjectMocks or @Before
  3. Create two nested test classes - one for the init logic and one for the core logic
  4. Somehow use @InjectMocks but re-initialize only in one test, not sure if possible
  5. Some lazy approach in get(), but also breaks immutability in the general case

To me option 3 seems clean, but maybe there is a better approach or further refactoring is needed idk. Any suggestions are highly appreciated.

Note that I cannot just use @Cachable in the Persistence because this is not a Spring project.


Solution

    1. Create the RepositoryImpl in each test case instead of using @InjectMocks or @Before
    2. Create two nested test classes - one for the init logic and one for the core logic

    I think that the options above are viable solutions, but you made a good point here:

    1. Somehow use @InjectMocks but re-initialize only in one test, not sure if possible

    To achieve that you can simply ignore the instance stored in the test class field annotated with @InjectMocks and create a separate one, just in this test (as described in your second proposed approach).

    @ExtendWith(MockitoExtension.class)
    class RepositoryTest {
    
        @Mock
        Persistence persistence;
        @InjectMocks
        RepositoryImpl repository;
    
        @Test
        void oneOfMultipleTests() {
            // here you're defining the persistence field mock behavior
            // and using the repository field
        }
    
        @Test
        void objectNotFound() {
            var emptyPersistence = mock(Persistence.class);
            when(emptyPersistence.get()).thenThrow(...);
    
            var emptyBasedRepository = new RepositoryImpl(emptyPersistence);
            
            assertNotNull(emptyBasedRepository);
            verify(emptyPersistence).persist(argThat(...));
            // assert something else if you want
        }
    
    }