I write JUnit tests for some Spring MVC Controllers. The initialization of the JUnit test is common for all my Controllers tests, so I wanted to create an abstract class that does this initialization.
Thus, I created the following code:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = { "classpath*:spring/applicationContext-test.xml", "classpath*:spring/spring-mvc-test.xml" })
@Transactional
public abstract class AbstractSpringMVCControllerTest<T> {
@Autowired
protected ApplicationContext applicationContext;
protected MockHttpServletRequest request;
protected MockHttpServletResponse response;
protected HandlerAdapter handlerAdapter;
protected T controller;
@SuppressWarnings("unchecked")
@Before
public void initContext() throws SecurityException, NoSuchFieldException {
request = new MockHttpServletRequest();
response = new MockHttpServletResponse();
handlerAdapter = applicationContext.getBean(AnnotationMethodHandlerAdapter.class);
// Does not work, the problem is here...
controller = applicationContext.getBean(T);
}
}
The idea is to create, for each controller I want to test a JUnit test class that extends my AbstractSpringMVCControllerTest
. The type given in the extends
declaration is the class of the Controller.
For example, if I want to test my AccountController
, I will create the AccountControllerTest
class like that:
public class AccountControllerTest extends AbstractSpringMVCControllerTest<AccountController> {
@Test
public void list_accounts() throws Exception {
request.setRequestURI("/account/list.html");
ModelAndView mav = handlerAdapter.handle(request, response, controller);
...
}
}
My problem is located in the last line of the initContext()
method of the abstract class. This abstract class declares the controller
object as a T
object, but how can say to the Spring Application Context to return the bean of type T
?
I've tried something like that:
Class<?> controllerClass = this.getClass().getSuperclass().getDeclaredField("controller").getType();
controller = (T) applicationContext.getBean(controllerClass);
but controllerClass
returns the java.lang.Object.class
class, not AccountController.class
.
Of course, I can create a public abstract Class<?> getControllerClass();
method, which will be overriden by each JUnit Controller test class, but I prefer to avoid this solution.
So, any idea?
This is possible if your subclasses of AbstractSpringMVCControllerTest
bind T
at compile time. That is, you have something like
public class DerpControllerTest extends AbstractSpringMVCControllerTest<DerpController> { }
rather than
public class AnyControllerTest<T> extends AbstractSpringMVCControllerTest<T> { }
I'm guessing you probably have the former. In this case, the type of T
is erased from the Class
object for AbstractSpringMVCControllerTest
at runtime, but the Class
object for DerpControllerTest
does provide a way to know what T
is, since it bound T
at compile time.
The following classes demonstrate how to access the type of T
:
Super.java
import java.lang.reflect.ParameterizedType;
public abstract class Super<T> {
protected T object;
@SuppressWarnings("unchecked")
public Class<T> getObjectType() {
// This only works if the subclass directly subclasses this class
return (Class<T>) ((ParameterizedType) getClass().getGenericSuperclass()).getActualTypeArguments()[0];
}
}
Sub.java
public class Sub extends Super<Double> {
}
Test.java
public class Test {
public static void main(String...args) {
Sub s = new Sub();
System.out.println(s.getObjectType()); // prints "Class java.lang.Double"
}
}
This is different from the type erasure we normally see. With type erasure, we don't know the parameter of the current class (the one you get with getClass()
), but you can get those in super class / super interface (those you get with getGenericSuperxxxxx()
) because this is part of the type declaration.
This won't give your the type of controller
field, but I hope this is enough for your purpose.
Code:
public class A<P> {
}
import java.lang.reflect.ParameterizedType;
public class B extends A<String> {
public static void main(String[] arg) {
System.out.println(
((ParameterizedType)B.class.getGenericSuperclass()).getActualTypeArguments()[0]);
}
}
Output:
class java.lang.String
In your case, it would be
Class controllerClass = (Class)( ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0]);
Something to notes:
If the class B
is also parameterized like this:
public class B<X> extends A<X> {}
This won't work. Or if you have another class extends B, it will have problem too. I won't go into all those cases, but you should get the idea.
You can't because at runtime, due to ERASURE, the JVM cannot know the class of your "controller" attribute. It is considered as Object...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With