1

I am dealing with a design situation and I am not sure how to solve it or if I am doing anything wrong with my micro design.

public abstract class A implements SomeFrameworkInterface {

  private final Param1 a1;

  @Inject
  private Param2 a2;

  protected A(Param1 a1) {
    this.a1 = a1;
  }

  @Override
  public someFrameworkInterfaceMethod() {
    //code
    doMyStuff();
  }

  protected abstract void doMyStuff();
} 

@Named
@SomeCustomAnnotation(property="someProperty")
public class B extends A {

  @Inject
  public B(Param1 b1) { //b1 is different for every implementation of class A, through some annotations
    super(b1);
  }

  @Override
  protected void doMyStuff() {
    //code
  }

}
  • I need to change "@Inject Param2 a2" from class A to be injected through the constructor, so the code would be testable and I will clean a code smell associated with this.
  • I don't want to send it from B constructor, because I would have to add one more argument in B constructor, in A constructor and for each constructor from all implementations of A... but the child class never needs to know about Param2 instance.
  • Param2 may be static, but needs to be injected because it's component (@Named), so I didn't find a way to static inject.

Restrictions are the following:

  1. 3rd party framework with custom annotations to be used and interface to be implemented
  2. code must be testable, so @Inject/@Autowired on fields is not allowed
  3. we are restricted from using Spring which is "encapsulated" in the custom framework mentioned, so we can use only pure Java (JSR-330). (if any solution in Spring, would be nice to know about it too)
softwareRat
  • 57
  • 2
  • 7
  • Nice question. I'm pretty sure that there are only workarounds for your situation (i.e. either letting `B` pass-through the `Param2`, or writing your tests and manually set the `Param2` in some way). BTW: according to the documentation fields used with [`@Inject`](https://docs.oracle.com/javaee/6/api/javax/inject/Inject.html) *cannot* be `final`. So, given that you want to be compliant, start by removing `final` from the `Param2` field. – Giacomo Alzetta Apr 11 '18 at 13:36
  • This was written in post editor here. It was copy-paste from Param1 and forgot to remove :D – softwareRat Apr 11 '18 at 13:47

2 Answers2

0

It's impossible to pass any parameter to A's constructor without going through B's, because you are actually constructing an instance of B. A's constructor is only delegated to in order to construct the "A part" of the B instance.

The most you can do is pass some sort of "typeless" parameter to B's constructor, so that at least B won't know about Param2, but that cure already sounds worse than the disease even as I was writing it down...

I don't see injecting Param2 post-construction making the code any less testable. OTOH, using a static field would most certainly do.

jingx
  • 3,698
  • 3
  • 24
  • 40
  • I did think about passing some special class parameter that only `A` can read, but I agree with you that it doesn't sound good. – Giacomo Alzetta Apr 11 '18 at 15:31
0

I find the solution to test this, but it's look a little weird. I mean I do not understand why or how is this happening, but it does the job in a clean way. Test class looks like:

@RunWith(MockitoJUnitRunner.class)
public BTest {

  @Mock
  private Param1 mockParam1;

  @Mock
  private Param2 mockParam2;

  @InjectedMocks
  private A b = new B(null);

  @Test
  public someFrameworkInterfaceMethodTest() {
    when(...).thenReturn(...)
    b.someFrameworkInterfaceMethod();
    verify(...).someCall(any());
  }

}

I had a hard time to figure it out why Param1 a1 remained null and tried to find out different solutions. But it seems Mockito injects fields again post construct (after "new B(null)" is called - if fields are null or not sure). Param2 a2 was mocked by mockito while Param1 a1 remained null and this happened because of the "final" keyword on Param1 a1 in B class.

softwareRat
  • 57
  • 2
  • 7