google / gwtmockito

Better GWT unit testing
https://google.github.io/gwtmockito
Apache License 2.0
157 stars 50 forks source link

Mocked @UiField types are erased, causing ClassCastExceptions #50

Open cushon opened 9 years ago

cushon commented 9 years ago

The implementation of FakeUiBinderProvider erases the type of all of the fields it injects (e.g. Box<String> gets mocked as Box<Object>). It isn't possible to pass the necessary type information through GWT.create() (since it only takes a class literal), but there may be alternatives I'm missing.

Here's an example:

@RunWith(GwtMockitoTestRunner.class)
public class MyWidgetTest {

  interface Box<T> {
    T get();
  }

  static class MyWidget extends Composite {
    interface MyUiBinder extends UiBinder<Widget, MyWidget> {}
    private final MyUiBinder uiBinder = GWT.create(MyUiBinder.class);

    @UiField
    Box<String> message;

    public MyWidget() {
      initWidget(uiBinder.createAndBindUi(this));
    }
  }

  MyWidget myWidget;

  @Before
  public void setUp() {
    myWidget = new MyWidget();
  }

  @Test
  public void simpleTest() {
    String message = myWidget.message.get();
  }
}

The mock for myWidget.message will be a raw Box, so get() returns a String, causing a ClassCastException:

java.lang.ClassCastException: org.mockito.internal.creation.jmock.ClassImposterizer$ClassWithSuperclassToWorkAroundCglibBug$$EnhancerByMockitoWithCGLIB$$ef767c84 cannot be cast to java.lang.String
    at mockitobug.MyWidgetTest.simpleTest(MyWidgetTest.java:44)
        ...

I encountered this because if you write:

when(myWidget.message.get()).thenReturn("Hello");

Then the eclipse compiler will generate a string cast on the result of myWidget.message.get(), which causes the test to crash. (javac is unaffected, except for specific versions of javac9.)

Here's the complete repro:

mkdir -p src/test/java/mockitobug
curl https://gist.githubusercontent.com/cushon/50a9cbe451e7d6607a65/raw/8ccb8750c462b06d55109cfa2a6fcf7ff54f310c/MyWidgetTest.java > src/test/java/mockitobug/MyWidgetTest.java
curl https://gist.githubusercontent.com/cushon/50a9cbe451e7d6607a65/raw/94d77af02627d2b7b1756ff14e7a396514d7aeaa/pom.xml > pom.xml
mvn test
Running mockitobug.MyWidgetTest
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.565 sec <<< FAILURE!
simpleTest(mockitobug.MyWidgetTest)  Time elapsed: 0.237 sec  <<< ERROR!
java.lang.ClassCastException: org.mockito.internal.creation.jmock.ClassImposterizer$ClassWithSuperclassToWorkAroundCglibBug$$EnhancerByMockitoWithCGLIB$$ef7df407 cannot be cast to java.lang.String
    at mockitobug.MyWidgetTest.simpleTest(MyWidgetTest.java:41)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
...
ekuefler commented 9 years ago

Yeah, the fact that we call GWT.create to populate UiFields will be problematic. We probably don't need to do this though - we could instead provide some sort of back door to invoke the underlying create code directly with additional type information. Would take some experimenting to figure out if we have the necessary information available and how to get it into Mockito.