google / gwtmockito

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

"java.lang.ClassCastException: com.google.gwt.core.client.JavaScriptObject$$EnhancerByMockitoWithCGLIB$$29edae7f cannot be cast to com.google.gwt.user.client.Element" while trying to test a widget extending SimpleLayoutPanel #4

Open konradstrack opened 11 years ago

konradstrack commented 11 years ago

Let's take the following example:

public class MyPanel extends SimpleLayoutPanel {

    private Label label;

    public MyPanel() {
        label = GWT.create(Label.class);
        label.setText("It's my panel");

        add(label);
    }

}

And a test case:

@RunWith(GwtMockitoTestRunner.class)
public class MyPanelTests {

    private MyPanel myPanel;

    @Before
    public void setUp() {
        myPanel = new MyPanel();
    }

    @Test
    public void testTruth() {
        assertTrue(true);
    }
}

Running this test case will result in the following exception:

java.lang.ClassCastException: com.google.gwt.core.client.JavaScriptObject$$EnhancerByMockitoWithCGLIB$$bd5b9f31 cannot be cast to com.google.gwt.user.client.Element
    at com.google.gwt.user.client.DOM.createDiv(DOM.java:149)
    at com.google.gwt.user.client.ui.SimplePanel.<init>(SimplePanel.java:35)
    at com.google.gwt.user.client.ui.SimpleLayoutPanel.<init>(SimpleLayoutPanel.java:31)
    at cern.ais.plugin.gwt.client.layout.view.MyPanel.<init>(MyPanel.java:11)
    at cern.ais.plugin.gwt.client.layout.view.MyPanelTests.setUp(MyPanelTests.java:17)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:27)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
    at com.google.gwtmockito.GwtMockitoTestRunner.run(GwtMockitoTestRunner.java:102)
    at org.junit.runners.Suite.runChild(Suite.java:128)
    at org.junit.runners.Suite.runChild(Suite.java:24)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:77)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:195)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:63)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

If MyPanel extends Composite, everything works fine, but with SimpleLayoutPanel (or just SimplePanel) the result is the same as the one above.

Is this a known limitation?

In many cases widgets can be refactored to extend Composite, but is there a way of testing with gwtmockito those that have to extend something else than Composite, or is using GWTTestCase required in such cases?

ekuefler commented 11 years ago

Hm, it definitely shouldn't matter what your widget is extending. I'll dive into this tomorrow to try and figure out why it's breaking. Thanks for the detailed report!

ekuefler commented 11 years ago

So the underlying problem here is pretty interesting (skip to the bottom if you just want to see the fix). The issue is with the methods from DOM.java in GWT that create elements. LayoutPanel uses these directly instead of going through the nicer Document API. They look like this:

public static Element createDiv() {
  return Document.get().createDivElement().cast();
}

The issue is the cast() method, which is designed to coerce any JavaScriptObject into any other. This is fine when GWT is compiled to Javascript, but it causes some havoc in Java when you try to cast one class to something it doesn't extend. That's what's happening in GWT here - Element in this case is actually com.google.gwt.user.client.Element, which is a sibling of com.google.gwt.dom.client.DivElement, not a parent. So the cast fails.

It would be nice if we could just stub calls to cast() to return new mocks of the appropriate type (so it's not really a cast at all), but unfortunately it's impossible to figure out what type we actually want to cast to at runtime since that information is only contained in the generic definition of cast, which is lost at runtime. Too bad cast doesn't take the target class as an argument. So implementing cast() in a completely satisfying way probably isn't going to happen.

What I can do is stub out the create methods in DOM entirely to return mock Elements. This should avoid breakages when using code that calls into DOM as LayoutPanel does. The only case that this won't cover is when code does a weird cast like IFrameElement e = Document.get().createDivElement().cast(). But it's pretty unclear how GwtMockito should handle this case anyways and it seems fine to ask people to stick with GWTTestCase if they want to be that weird.

So, long story short, I've pushed a workaround for this in commit ce7323e5aab66f63342ba9065c91713a8bb06d7f. Want to try building GwtMockito from HEAD and seeing if that solves your problem?

konradstrack commented 11 years ago

Thank you for the very detailed answer. Unfortunately, the problem has been solved only partially.

First of all, I can successfully build commit ce7323e5aab66f63342ba9065c91713a8bb06d7f but the latest one (8f7f4afed768df2593319f7aa9f2e62b4735e555) results in three errors during Maven test phase. You can find the Surefire output here: https://gist.github.com/konradstrack/3a457db40d38a154b976#file-gistfile1-txt

But when I use ce7323e5aab66f63342ba9065c91713a8bb06d7f, the test passes if I don't attach any child widgets to MyPanel. So, this is alright:

public class MyPanel extends SimpleLayoutPanel {

    public MyPanel() {
        Label label = GWT.create(Label.class);
        label.setText("It's my panel");

//        add(label);
    }
}

But the original test results in an AssertionError. So, if I try to add a Label:

public class MyPanel extends SimpleLayoutPanel {

    public MyPanel() {
        Label label = GWT.create(Label.class);
        label.setText("It's my panel");

        add(label);
    }
}

I get:

java.lang.AssertionError
    at com.google.gwt.user.client.ui.Panel.adopt(Panel.java:126)
    at com.google.gwt.user.client.ui.SimpleLayoutPanel.setWidget(SimpleLayoutPanel.java:89)
    at com.google.gwt.user.client.ui.SimplePanel.add(SimplePanel.java:69)
    at cern.ais.plugin.gwt.client.layout.view.MyPanel.<init>(MyPanel.java:13)
    at cern.ais.plugin.gwt.client.layout.view.MyPanelTests.setUp(MyPanelTests.java:17)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:45)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:42)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:27)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:68)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:47)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:300)
    at com.google.gwtmockito.GwtMockitoTestRunner.run(GwtMockitoTestRunner.java:104)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:77)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:195)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:63)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
ekuefler commented 11 years ago

Oops, last-minute visibility change broke the tests. Pushed a fix for that.

The next problem is interesting. The reason GwtMockito works well with Composites is that all the code for widgets lives behind fields that can be mocked out. This gets harder when you're extending a real widget. For your case, it would work fine if you extended Composite and wrapped a LayoutPanel since we could mock out the add() call, but since you're extending LayoutPanel instead it gets harder to mock since your class inherits a real add method.

But extending non-Composite widgets is common enough that GwtMockito should find a way to make it work. I think what it can do is stub out the implementations for all of the methods in a bunch of common base widget classes - UIObject, Widget, Composite, and all the Panels seem like good candidates. This effectively allows us to "mock" the superclass of the widget. It's still probably not as testable as using a Composite since you can't stub and verify the methods individually, but it should at least allow subclasses to be instantiated and their methods be called without breaking.

I've implemented this in 11cb40a251da0c159c4f088bd4c9b106597a1c18. Let me know if that helps with your use case.

konradstrack commented 11 years ago

I have just tested GwtMockito from HEAD on the original test case and it seems to work perfectly fine. Thank you for fixing the problem, and in such short time.

There is however one other possible use case that I don't really know how to handle, and if it is in any way supported by GwtMockito right now. Everything seems to be fine if you extend something that comes from GWT. But if, for instance, my widget would be extending something like a SimpleContainer from GXT it would fall into similar problems.

Would it be enough to create an additional provider for SimpleContainer, or is there another mechanism that would allow to provide mocks for parents which are not standard GWT widgets?

PS. Sorry for prematurely closing the issue.

ekuefler commented 11 years ago

It probably doesn't make sense to add SimpleContainer to the list of things GwtMockito knows about, since that would introduce a dependency from GwtMockito to GXT.

However, it probably would be a good idea to make the list user-configurable. I'm not aware of an easy way to pass arguments to test runners, but we could probably accomplish this by making GwtMockitoTestRunner overridable, such that the set of classes to stub could be configured via a protected method. So if you're using GXT, you could define an override of GwtMockitoTestRunner that adds all the GXT base classes to the stub list, and use that in your tests, How does that sound?

konradstrack commented 11 years ago

I totally agree that it doesn't make sense to add any weird dependencies to GwtMockito - GXT is not the only set of widgets out there, and SimpleContainer is probably not the only thing that may need mocking.

The proposed solution seems very good, and pretty convenient too - for sure much more convenient than stubbing classes in every test case.

ekuefler commented 11 years ago

Okay, I've provided a protected method that you can override in 2e9f9f44d13b5d9c4599d4df0ab9acf58dc61dab. Check it out and let me know if it works like you want it to.

ghost commented 11 years ago

Hi! I've took over that issue from @konradstrack. Finally it works! The only thing is that the default class list is not full at all. For example the ListBox expose this method.

  private SelectElement getSelectElement() {
    return getElement().cast();
  }

And it's failing for the mentioned reason. My proposal is to find all classes programatically in GWT ( and expose some method to find all classes to mock in 3rd party libs like GXT) which calling the cast() function and automatically include them into list. Should not be that hard thing.

ekuefler commented 11 years ago

Yeah, the default class list is intentionally pretty narrow and I'm not including every GWT widget in it. I want to start by being fairly conservative about what goes on the list such that there's as little magic happening as possible. I think I'll wait a while to see what people think of the current setup and then re-examine if there's demand for expanding the default list.

justinmk commented 11 years ago

This worked well for me. For Sencha GXT, I am using the following:

@Override
protected Collection<Class<?>> getClassesToStub() {
    Collection<Class<?>> classes = super.getClassesToStub();
    classes.add(XElement.class);
    classes.add(SimpleContainer.class);
    classes.add(ResizeContainer.class);
    classes.add(Container.class);
    classes.add(StyleInjectorHelper.class);
    classes.add(NorthSouthContainer.class);
    classes.add(Draggable.class);
    classes.add(GridDragSource.class);
    return classes;
}

Also, Sencha GXT actually calls getElement().cast() in the constructor of some GXT widgets, such as Draggable. To deal with that, I stub the getElement() method of the injected widget(s):

    XElement el = XElement.createElement("div");
    Mockito.when(mockWidget.getElement()).thenReturn(el);
nikp commented 10 years ago

I'm having this identical issue, and upgrading to GWTMockito 1.1.1 did not help

In my case I'm using a custom DataTable that extends com.google.gwt.user.cellview.client.CellTable

What fails is line 613 in CellTable

table = getElement().cast();

java.lang.ClassCastException: com.google.gwt.user.client.Element$$EnhancerByMockitoWithCGLIB$$55b005a0 cannot be cast to com.google.gwt.dom.client.TableElement at com.google.gwt.user.cellview.client.CellTable.(CellTable.java:613)

The problem is getElement return type is com.google.gwt.user.client.Element, which is a sibling of com.google.gwt.dom.client.TableElement. Their mutual base class is com.google.gwt.dom.client.Element

How would I go about fixing this with explicit mocking in my test? I tried the following, but it doesn't compile due to the sibling issue:

when(myTable.getElement()).thenReturn(mock(TableElement.class));

But honestly I'm not even sure how this could work, because since this happens in the constructor of CellTable, there is a catch 22. I can't mock the object until I construct it and I can't construct it without getElement be mocked. How would one mock out a base class?

nikp commented 10 years ago

There is a further issue with the suggested workaround in https://github.com/google/gwtmockito/issues/4#issuecomment-18729283

As suggested, the following snippet fixes the problem for Labels created as part of code executed within the test:

        DivElement divElement = mock(DivElement.class);
        when(divElement.getTagName()).thenReturn("div");
        final Document document = Document.get();
        when(document.createDivElement()).thenReturn(divElement);

However, if the Label is part of a class or base class for a type that the test is attempting to @GwtMock, the failing assertion code will execute before the mocks can get setup and executed

ekuefler commented 10 years ago

@nikp - if your problem is a misbehaving base class in CellTable, you might be able to fix it by adding CellTable to the list of base classes to stub. Try defining a subclass of GwtMockitoTestRunner that overrides a method like this:

  @Override
  protected Collection<Class<?>> getClassesToStub() {
    Collection<Class<?>> classes = super.getClassesToStub();
    classes.add(CellTable.class);
    return classes;
  }

Can you let me know if that helps? If so I could add CellTable to the list of classes to stub by default.

nikp commented 10 years ago

Thanks @ekuefler, I will try that!

avetokhin commented 10 years ago

Hi. There is a problem with com.google.gwt.user.client.ui.Image class too. Image uses private inner class com.google.gwt.user.client.ui.Image.UnclippedState and I don't have access to it. When I create new Image("anyUrl") (in my view class which I want to test) I have an exception: com.google.gwt.user.client.Element$$EnhancerByMockitoWithCGLIB$$5830e7d1 cannot be cast to com.google.gwt.dom.client.ImageElement from UnclippedState's method:

@Override
public ImageElement getImageElement(Image image) { return image.getElement().cast(); }

Because Image is mocked it returns Element which cannot be cast to ImageElement.

Do you have any ideas how to fix it?

justinmk commented 10 years ago

@avetokhin I'm pretty sure that should just be a matter of stubbing out Image.getElement(). Something like this (from memory):

@GwtMock Image mockImage;
Mockito.when(mockImage.getElement()).thenReturn(ImageElement.class);
ajelcocks commented 10 years ago

I am was getting the ClassCastException for CellTable as mentioned 7 months ago. I tried the solution of subclassing GwtMockitoTestRunner and now I get NoClassDefFound (com/google/gwt/user/cellview/client/CellTable)

Using GwtMockito 1.1.3 from maven repo

Is there a solution to this?

ekuefler commented 10 years ago

Spent a long time trying to find a good workaround for Image (which does a lot of weird things casting Elements back and forth in a way that makes Java unhappy), without much luck. For now I've added it to the list of classes to stub by default, which should fix the issues you describe (but may cause others, let me know if so).

I've also fixed a few issues related to CellTable. You should be able to see those and the Image fixes if you run against the latest snapshot - is anyone still seeing ClassCastExceptions?

kishorepalakollu commented 9 years ago

Hi, i am getting a classcast exception when writing the Junit testcases for the following scenario. JsArray overlayArray=(JsArray) JavaScriptObject.createArray().cast(); java.lang.ClassCastException: com.google.gwt.core.client.JavaScriptObject$$EnhancerByMockitoWithCGLIB$$3f37cdf7 cannot be cast to com.google.gwt.core.client.JsArray at com.siemens.splm.clientfx.kernel.clientmodel.internal.JavaSciptObjectCastHepler.cast(JavaSciptObjectCastHepler.java:55) at com.siemens.splm.clientfx.kernel.clientmodel.internal.ListHelper.getOverlayArrayFromPojoList(ListHelper.java:61) at com.siemens.splm.clientfx.kernel.clientmodel.internal.ListHelperTest.testGetOverlayArrayFromPojoListWithNotNullInput(ListHelperTest.java:111) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47) at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44) at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26) at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70) at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50) at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238) at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63) at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236) at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53) at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229) at org.junit.runners.ParentRunner.run(ParentRunner.java:309) at com.google.gwtmockito.GwtMockitoTestRunner.run(GwtMockitoTestRunner.java:102) at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50) at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382) at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)

timeu commented 8 years ago

Is there a known workaround for the ClassCastException that @kishorepalakollu reported when dealing with JavascriptObject and JsArray ? Adding them to the @WithClassesToStub didn't really solve it

kishore-palakollu commented 8 years ago

@timeu Actually i have created a mehtod to mock the JavaScriptObject.createArray() and return the javascriptobject. and i mocked it .and after that i mocked the javascriptobjectobejct.cast and returned the jsarray. and this resolved my issue.

vikram-github commented 8 years ago

@ekuefler - I still get class cast exceptions in my code when I run GWT mockito test cases. Below is the snippet code which causing the cast issue. Element testElement = (Element) Document.get().createElement(tag);

Caused by: java.lang.ClassCastException: com.google.gwt.dom.client.Element$$EnhancerByMockitoWithCGLIB$$4ba5b663 cannot be cast to com.google.gwt.user.client.Element.

Any workaround for this issue?

kishore-palakollu commented 8 years ago

@vikram-github Yes even i faced the same issue,then i created two public methods in my class and did like this Element testElement = getElement(); public documentReturntype getDoucment(){ return Document.get(); } ou public Element getElement(){ return getDocument().createElement(tag); }

Now after this ,in your test file use spy Mocikto.spy() and mock the above two methods. and return the respective return types when these methods are called. This should work and it wont cause any impact in your application also.

lynx-r commented 8 years ago
@GwtMock Image mockImage;
Mockito.when(mockImage.getElement()).thenReturn(ImageElement.class);

@justinmk do you have a working example?

khouari1 commented 7 years ago

Hey, I am experiencing the following class cast exception issue:

Caused by: java.lang.ClassCastException: com.google.gwt.dom.client.Text$$EnhancerByMockitoWithCGLIB$$4050ddf1 cannot be cast to com.google.gwt.dom.client.Element at org.gwtbootstrap3.client.ui.html.Text.(Text.java:63) at org.gwtbootstrap3.client.ui.html.Text.(Text.java:53) at org.gwtbootstrap3.client.ui.Heading.(Heading.java:84) at org.gwtbootstrap3.client.ui.ModalHeader.(ModalHeader.java:38) at org.gwtbootstrap3.client.ui.Modal.(Modal.java:98)

manstis commented 7 years ago

@khouari1 Using @WithClassesToStub({ Text.class } ) works in our cases.