How can I mock a method that has a parameter with a private constructor?

Viewed 140

I have an abstract cache client with an implementation that I'm trying to add unit tests to, and it has a protected class implementation of the key. Like this:

public abstract class SimpleCacheClient<V extends Serializable> {
    // Autowired RedissonClient and RedisKeyGenerator

    public V get(SimpleCacheKey key) {
        // return the cache entry from the autowired RedissonClient
    }

    public void set(SimpleCacheKey key, V value) {
        // set the cache entry
    }

    public SimpleCacheKey getCacheKey(Object...keys) {
        return new SimpleCacheKey(keyGenerator.generateKey(keys));
    }

    /**
    * Simple wrapper for cache key to guarantee that implementations
    * are using the key generator provided in this class
    */
    protected class SimpleCacheKey {
        private String key;

        SimpleCacheKey(String key) {
            this.key = key;
        }

        public String getKey() {
            return key;
        }

        @Override
        public String toString() {
            return getKey();
        }
    }
}

And here's the implementation I'm trying to test:

public class CacheClientImplementation extends SimpleCacheClient<ArrayList<DateTime>> {
    public void addEventDateTimes(String key, ArrayList<DateTime> eventDateTimes) {
        // Do stuff with eventDateTimes and then
        set(getCacheKey(key), eventDateTimes);
    }
    
    public ArrayList<DateTime> getEventDateTimes(String key) {
        ArrayList<DateTime> eventDateTimes = get(getCacheKey(key));
        // Do stuff with eventDateTimes.
        return eventDateTimes;
    }
}

I'm trying to test to make sure that CacheClientImplementation performs certain operations on the values provided to it before setting and getting. I'm trying to mock the redis cache itself by hijacking the get() and set() methods to read and write from/to a HashMap so that I can check the contents of the "cache" in my tests.

@RunWith(MockitoJUnitRunner.class)
public class CacheClientImplementationTest{
    @Mock
    private RedissonClient redissonClient;

    @Mock
    private RedisKeyGenerator redisKeyGenerator;

    @Spy
    @InjectMocks
    private CacheClientImplementation cacheClient = new CacheClientImplementation();

    private final HashMap<String, ArrayList<DateTime>> cacheMap = new HashMap<>();

    @Before
    public void setup() {
        Mockito.doAnswer((ver) -> {
            cacheMap.put(ver.getArgumentAt(0, Object.class).toString(), ver.getArgumentAt(1, ArrayList.class));
            return null;
        }).when(cacheClient).set(Mockito.any(), Mockito.any(ArrayList.class));
        Mockito.doAnswer((ver) -> cacheMap.getOrDefault(ver.getArgumentAt(0, Object.class).toString(), null))
            .when(cacheClient).get(Mockito.any());
    }

    @After
    public void teardown() {
        cacheMap.clear();
    }
}

However, I end up with this problem when I run a test in the file.

C:\...\CacheClientImplementationTest.java:20: error: SimpleCacheClient.SimpleCacheKey has protected access in SimpleCacheClient
        }).when(cacheClient).set(Mockito.any(), Mockito.any(ArrayList.class));

Is there any way I can doAnswer for these methods without changing SimpleCacheKey?

Thanks!

1 Answers

This boils down to the visibility of the SimpleCacheKey class, you simply can't use it from a different package. So Mockito.any() can't use that class as a return type unless the unit test is in the same package as SimpleCacheClient.

One solution would be to move your unit test to the same package as SimpleCacheClient. If this is loaded from a different library that you can't change, you can re-create the same package structure to trick the compiler into thinking the package is the same, giving you access to protected classes. But i believe this trick doesn't work with Java 9 modules.

A better solution would be to make a small modification to your CacheClientImplementation and unit test; encapsulate the part you can't influence and mock that part.

Since you don't really care about the SimpleCacheKey but just the String key, the following should work for your intentions:

public class CacheClientImplementation extends SimpleCacheClient<ArrayList<DateTime>> {
    public void addEventDateTimes(String key, ArrayList<DateTime> eventDateTimes) {
        // Do stuff with eventDateTimes and then
        setForKey(key, eventDateTimes);
    }

    public ArrayList<DateTime> getEventDateTimes(String key) {
        ArrayList<DateTime> eventDateTimes = getForKey(key);
        // Do stuff with eventDateTimes.
        return eventDateTimes;
    }

    protected ArrayList<DateTime> getForKey(String key) {
        return super.get(getCacheKey(key));
    }

    protected void setForKey(String key, ArrayList<DateTime> value) {
        super.set(getCacheKey(key), value);
    }
}

And in the unit test you rewrite to the forKey variants we just created:

Mockito.doAnswer(myAnswer1()).when(cacheClient).setForKey(Mockito.any(), Mockito.any(ArrayList.class));
Mockito.doAnswer(myAnswer2()).when(cacheClient).getForKey(Mockito.any());

I've made the new methods protected as to not confuse callers which method to use, so in this case the unit test must be in same (test) package as the CacheClientImplementation.

Related