How To Java Unit Test a Complex Class

2019-08-06 10:07发布

问题:

I'm experiencing a difficult time on figuring out how to approach a problem with unit testing. I have little to no 'unit testing' experience. I am trying to change the classUnderTest only if absolutely necessary with minimal changes.

I am using JUnit 4, and I am willing to try to use Mockito, JMockit or any other libraries that would help with efficient and effectively unit testing. I am using JDBC for the database.

Problem:

Within the classUnderTest, I'm accessing a static database.

SpecializedDao specializedDao = SpecializedDao.getSpecializedDao();
ObjectX objectResult = specializedDao.currentObjectXByTimestamp(x, x, x, x, x, x);

and I am trying to use do unit test cases with a valid config, and an invalid config. What is the correct way to approach this problem.

Some possible solutions I have researched are:

  1. Somehow passing in, or 'injecting' a fake ObjectXResult of class ObjectX
  2. Mock a database (if even possible with static db reference, use separate unit-test servlet that references a fake SpecializedDao class?)
  3. Use Mockito with injectMocks, spy, when(methodcallwithparameters) thenReturn result, or some other implementation Mockito provides.
  4. Open to any other solution.

Problem:

Within the classUnderTest, I am using another complex processing class2 that does a lot of the work like so:

result = class2.execute(x, x, x, x, x, x);

class2 does processing stuff and returns a result ENUM or some exceptions. How do I handle this with keeping the scope of this specific Unit Test on classUnderTest. class2 accesses the database, and does a lot of the work which is why it is its own class but I think the final three test cases are dependent upon the processing to thoroughly test classUnderTest.

Thanks for bearing with me, I tried to make the question with as much clarity as possible.

回答1:

My recommendation, if you care about developing good, useful tests, is to not mock the database, or other complex classes your code may interact with.

Instead, think of the specific business scenarios that your code under test is meant to deliver, and write a realistic (ie, without replacing real classes/components with cheap - and often incorrect - imitations of them) and meaningful (from the point of view of real-world requirements) test for each scenario.

That said, if you still want to mock that DAO class, it can be done as follows using JMockit:

@Test
public void exampleTest(@Mocked SpecializedDao mockDao) {
    ObjectX objectResult = new ObjectX();

    new Expectations() {{
        mockDao.currentObjectXByTimestamp(anyX, anyY, anyZ); result = objectResult;
    }};


    // Call the SUT.
}

(anyX, etc. are not the actual JMockit argument matching fields, of course - the real ones are anyInt, anyString, any, etc.)



回答2:

A good rule is to never connect to external sources from your JUnits. Anything that does, such as database connections, you can mock them using Mockito.

Using Mockito, you can mock all classes you do not care to test. In your case you can just mock those heavy classes and return an expected result.



回答3:

You would need to use something like Mockito in any case, so I'll assume you have that.

First problem:

Testing code that involves static calls can be a pain. You could include some byte code manipulating library like PowerMock, but it's not worth it in my opinion. What you could do is place the static call in a package-local method, and then stub that out using spy. Something like:

//in class under test:
SpecializedDato getSpecializedDao() {
    return SpecializedDao.getSpecializedDao();
}

//in test:
import static org.mockito.Mockito.*;
//...
final SpecializedDao daoMock = mock(SpecializedDao.class);
final ClassUnderTest classUnderTest = spy(new ClassUnderTest());
doReturn(daoMock).when(classUnderTest).getSpecializedDao();

Second problem:

If you find that your tests become very complex, it is probably due to the class under test being too complex. See if you can extract functionality into other, smaller classes. Then, you only have to verify that these smaller classes are being invoked.



回答4:

At my office we typically use a few solutions to make a complex class more testable.

The least desirable is the ONLY one that will work without modifying the class under test--heavy mocking. You mock out everything outside of your class. If you have a static member in the class, you might set it to a mock with reflection before you start (Possibly the answer to your question).

This approach is difficult and fragile but is pretty much the only way that works if you can't modify the class under test.

A minor exception is a slightly different version of mocking--extending the class under test and override all the stuff your class interacts with. This isn't always effective for statics but works well if your class was designed using getters.

If you can modify your class we use a few techniques to help:

1) Avoid statics all the time. If you must have a singleton, make sure it's testable by either using an injection framework or building a mini-injection framework yourself (a static pre-populated map should be a good start)

2) We put a lot of our business logic in pure functions and we call the classes containing all this logic "Rules" classes. Our rules can typically be traced directly to requirements and they do not supply any of their own data, everything is passed in. They are generally called by an OO façade that contains related data and other code.

3) Reduce the complexity and interactions of your classes. Separate the database access from the calculations. Look at every class and ensure that it does exactly one thing well. This doesn't help directly but you'll notice that writing tests will be simpler if you follow this rule.



回答5:

Without hinging solution on a mocking framework, a standard pattern to deal with statics that involves only minimal changes to CuT is to extract the static method to sit behind an interface, and the class that originally accessed the static method now accesses a method on the interface. An instance of this interface is injected to the CuT which, when not testing, is a little wrapper class that just delegates straight through to the original static method.

Now you have an interface to work with, and you can inject the instance of that interface via setter or constructor injection, and Bob's your Uncle.

This standard trick does involve a little extra "stuff" - an interface, and a little wrapper class implementing the interface that delegates to the static, and some code or DI framework wiring that gets that implementing class in the production part of the code to inject onto the CuT via constructor or setter.

Then in your unit test, you have a hook to inject a mock, or a hand-created stub, or whatever style of Test Double you want.

This is a very common pattern when dealing with a third party library out of your control, that loves to use statics.



回答6:

i recently published a video on youtube that illustrates some of the steps Jim Weaver mentioned. It is in german but might still be helpful. In this video i introduce an interface to overcome the problem with static call of database access.

https://www.youtube.com/watch?v=KKYro-HGRyk

The second video demonstrates an approach helpful if you cannot change the sourcecode of the method called.

https://www.youtube.com/watch?v=fFLfx8qsBdI