Te sugiero que refactorices un poco tu código. Cuando tiene que comenzar a pensar en usar la reflexión u otro tipo de cosas, solo para probar su código, algo va mal con su código.
Mencionaste diferentes tipos de problemas. Comencemos con los campos privados. En el caso de campos privados, habría agregado un nuevo constructor e inyectado campos en eso. En lugar de esto:
public class ClassToTest {
private final String first = "first";
private final List<String> second = new ArrayList<>();
...
}
Hubiera usado esto:
public class ClassToTest {
private final String first;
private final List<String> second;
public ClassToTest() {
this("first", new ArrayList<>());
}
public ClassToTest(final String first, final List<String> second) {
this.first = first;
this.second = second;
}
...
}
Esto no será un problema incluso con algún código heredado. El código anterior usará un constructor vacío, y si me preguntas, el código refactorizado se verá más limpio y podrás inyectar los valores necesarios en la prueba sin reflexión.
Ahora sobre métodos privados. En mi experiencia personal, cuando tienes que colocar un método privado para probar, ese método no tiene nada que ver en esa clase. Un patrón común, en ese caso, sería envolver dentro de una interfaz, como Callable
y luego pasar esa interfaz también en el constructor (con ese truco de constructor múltiple):
public ClassToTest() {
this(...);
}
public ClassToTest(final Callable<T> privateMethodLogic) {
this.privateMethodLogic = privateMethodLogic;
}
Casi todo lo que escribí parece que es un patrón de inyección de dependencia. En mi experiencia personal, es realmente útil durante las pruebas, y creo que este tipo de código es más limpio y será más fácil de mantener. Yo diría lo mismo sobre las clases anidadas. Si una clase anidada contiene una lógica pesada, sería mejor si la hubiera movido como una clase privada de paquete y la hubiera inyectado en una clase que la necesita.
También hay varios otros patrones de diseño que he usado al refactorizar y mantener el código heredado, pero todo depende de los casos de su código para probar. El uso de la reflexión en su mayoría no es un problema, pero cuando tienes una aplicación empresarial que está muy probada y las pruebas se ejecutan antes de cada implementación, todo se vuelve realmente lento (es molesto y no me gusta ese tipo de cosas).
También hay una inyección de setter, pero no recomendaría usarla. Mejor me quedo con un constructor e inicializo todo cuando sea realmente necesario, dejando la posibilidad de inyectar las dependencias necesarias.