问题
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?
回答1:
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.
回答2:
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);
回答3:
In order to get around this you could wrap an interfaced repository type class around class B. Once you have an interface you can then stub it for testing purposes.
By doing this you insulate A from the inner workings of B and focus only on the resulting actions of B (I guess this is just another way of saying to program to an interface rather than a concrete class)
来源:https://stackoverflow.com/questions/39922637/unit-testing-a-class-that-calls-a-static-method