Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unit-testing a class that calls a static method

I am trying to unit-test a class 'A' which calls a static method of a class 'B'. Class 'B' essentially has a google guava cache which retrieves a value(Object) from the cache given a key, or loads the object into the cache (in case of a cache-miss) using a service adapter. The service-adapter class in turn has other autowired dependencies to retrieve the object.

These are the classes for illustration purposes:

Class A

public class A {
    public Object getCachedObject(String key) {
        return B.getObjectFromCache(key);
    }
}

Class B

public class B {

    private ServiceAdapter serviceAdapter;

    public void setServiceAdapter(ServiceAdapter serAdapt) {
        serviceAdapter = serAdapt;
    } 

    private static final LoadingCache<String, Object> CACHE = CacheBuilder.newBuilder()
                .maximumSize(100) 
                .expireAfterWrite(30, TimeUnit.MINUTES)
                .build(new MyCacheLoader());

    public static Object getObjectFromCache(final String key) throws ExecutionException {
        return CACHE.get(warehouseId);
    }

    private static class MyCacheLoader extends CacheLoader<String, Object>  {

        @Override
        public Object load(final String key) throws Exception {
            return serviceAdapter.getFromService(key)
        }
    }
}

Service-Adapter Class

public class ServiceAdapter {
        @Autowired
        private MainService mainService

        public Object getFromService(String key) {
            return mainService.getTheObject(key);
        }
    }

I am able to do the integration test successfully and fetch (or load) the value from (or into) the cache. However, I am unable to write the unit-test for class A. This is what I have tried:

Unit-Test for Class A

@RunWith(EasyMocker.class)
public class ATest {
    private final static String key = "abc";
    @TestSubject
    private A classUnderTest = new A();

    @Test
    public void getCachedObject_Success() throws Exception {
        B.setServiceAdapter(new ServiceAdapter());
        Object expectedResponse = createExpectedResponse(); //some private method 
        expect(B.getObjectFromCache(key)).andReturn(expectedResponse).once();
        Object actualResponse = classUnderTest.getCachedObject(key);
        assertEquals(expectedResponse, actualResponse);
    }
}

When I run the unit-test, it fails with a NullPointerException at ServiceAdapter class where the call: mainService.getTheObject(key) is made.

How do I mock the dependency of ServiceAdapter while unit-testing class A. Shouldn't I be just concerned about the immediate dependency that class A has, viz. B.

I am sure I am doing something fundamentally wrong. How should I write the unit-test for class A?

like image 471
user1639485 Avatar asked Oct 07 '16 17:10

user1639485


People also ask

Can you call a static method on a class?

A static method can be called directly from the class, without having to create an instance of the class. A static method can only access static variables; it cannot access instance variables. Since the static method refers to the class, the syntax to call or refer to a static method is: class name. method name.

Can I mock static class in unit test?

If you need to truly mock static methods, you need to use a commercial tool like Microsoft Fakes (part of Visual Studio Enterprise) or Typemock Isolator. Or, you can simply create a new class to wrap the static method calls, which itself can be mocked.

Should we mock static methods?

Ideally, a class should not be responsible for obtaining its dependencies, and if possible, they should be externally injected. So, it's always worth investigating if we can refactor our code to make it more testable. Of course, this is not always possible, and sometimes we need to mock static methods.


2 Answers

You now know why static method are deemed bad practice for unit testing, as they make mocking almost impossible, esp. if they are stateful.

It is hence more practical to refactor B static methods into a set of non-static public ones.

Class A should get an instance of class B injected, either via constructor or setter injection. In Your ATest you then instantiate class A with a mock of class B and have it return whatever you like depending on your test case and base your assertions on that.

By doing so you really test the unit, which in the end should be the public interface of class A. (This is also why I like for a class to have only one public method in an ideal world.)


Regarding to your specific example: The mock of B should also not care about its own dependencies. You currently write in your test:

 B.setServiceAdapter(new ServiceAdapter());       

You are in ATest. Not in BTest. ATest should only have a mock of B, so passing an instance of the ServiceAdapter should not be required.

You only should care how A's public methods behaves, and that may change given certain responses of B's public methods.

What I also find odd is that the method you want to test basically only a wrapper to B. Maybe this makes sense in your case yet this also hints to me that you maybe want to already inject an Object in A instead of an instance of B.

If you want to not get lost in mocking hell it really helps to have as less public methods per class which in turn have as less dependencies as possible. I strive for at three dependencies per class, and allow up to five on special occasions. (Each dependency may have huge impact on the mocking overhead.)

If you have too many dependencies, certainly some parts can be moved to other/new services.

like image 142
k0pernikus Avatar answered Oct 19 '22 03:10

k0pernikus


Re-writing code to make it more testable has already been explained in another answer. At times it is difficult to avoid these cases.

If you really wanted to mock a static call, you could use PowerMock. You will need to use @PrepareForTest({CACHE.class}) annotation for your class followed by code below in the unit test.

      Object testObject = null; // Change this
      PowerMock.mockStatic(CACHE.class);
      expect(CACHE.get(anyString())).andReturn(testObject);
      PowerMock.replay(CACHE.class);
like image 37
Andy Avatar answered Oct 19 '22 02:10

Andy