google / gwtmockito

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

Problem running tests with GwtMockitoTestRunner when classpath is assembled at runtime #5

Closed bwolff closed 11 years ago

bwolff commented 11 years ago

Hi,

first I want to say that the GwtMockito lib is really a very nice and promising approach that would benefit us much in testing our GWT client code!

Currently, I'm blocked by a problem to programmatically run the tests with the GwtMockitoTestRunner. When I create a test and use the @RunWith annotation and then use the IntelliJ IDE JUnit test runner to run the class (right-click on the test class) it works. The JUnit runner runs the "java" commands and passes all libs and folders via the -classpath parameter (which is a lot! ;)).

Now I want to run the test from within the Grails framework. The problem is (I believe) that the test classpath is assembled at runtime and before running the tests the current thread's contextClassLoader is modified with all the required libs and folders etc. When I now want to programmatically invoke the GwtMockitoTestRunner the problem is that the custom GwtMockitoClassLoader can't find my test class. The test class is found by the Thread classLoader though.

I'm not really familiar with the JUnit test runner internals, but the only thing I can imagine is that the GwtMockitoClassLoader respects classloader resources passed when the application is started, but not the current context classLoader of the thread?

I checked the sources for the GwtMockitoTestRunner.java and the GwtMockitoClassLoader and it seems that the initialization is done in the constructor and the classloader class is private, so there is no hook to interfere with the classloader resource locations...

I know this description is rather vague, but maybe you already have an idea what the problem may be? If you require additional information I would be happy to provide them.

Maybe we could also produce a small code snippet that show how the GwtTestRunner can be invoked on a test class / test classes in a programmatic way?

Thank you!

Cheers, Ben

bwolff commented 11 years ago

I spent some more time and tried the following things to "persuade" the GwtMockitoTestRunner/GwtMockitoClassLoader to load my test class, to no avail:

ekuefler commented 11 years ago

I'm not very familiar with Grails - do you know if there's an easy way for me to mimic your setup so I can try reproducing the problem to see what's going on?

If you're relying on a particular context classloader to be present I can see how that might cause interference. GwtMockito plugs in its own classloader while running tests here. This is necessary since Mockito creates mocks using the context classloader, and if that's different from the classloader used to load the test there will be conflicts. The GwtMockito classloader is probably using the system classloader as its parent, so if the framework is setting things up via the context classloader it might not be able to find the right classes.

As a quick experiment, you could try modifying the GwtMockitoClassLoader constructor to use the context classloader as the parent by adding this line:

super(Thread.currentThread().getContextClassLoader(), null);

If that fixes things then we know what the issue is and can probably fix it.

What do you mean about invoking the testrunner programmatically? The following seems to work for me:

Result result = new JUnitCore().run(GwtMockitoTest.class);

Assuming GwtMockitoTest is annotated with @RunWith(GwtMockitoTestRunner.class), this will respect the annotation and use GwtMockito. Are you trying to do something more complex than that?

bwolff commented 11 years ago

Hi Erik,

thanks for the response. I already tried your suggestions by modifying the GwtMockitoClassLoader class. However, the problem is that the ClassPool instance in the onLoad() method can't load the class. I spent some time with the Javassist API docs to find a way to pass a parent classloader to the ClassPool, but eventually I only found the option of adding a single classpath resource to the ClassPool using the #appendClassPool() method. Just a a test I added the path to my compile test class using this method and then the class was found, but it then failed with another ClassNotFound, so I would have to add every class output folder and library to it, which is not really feasible.

The short snippet you provided is a good start, but how do you actually run this code? I assume that your system classloader is aware of all your classes (GwtMockitoTest, GwtMockito, etc.)? How would the code behave if you put your test class somewhere else, and resolve it at runtime by using a context classloader, for example?

A mechanism to specify a "parent classloader" for the custom GwtMockitoClassLoader would be very nice in order to allow the integration into other frameworks that work with assembling the full classpath at runtime (due to convention over configuration source folders etc.).

I'll try to look more into this when I find the time. Thanks!

Cheers, Ben

ekuefler commented 11 years ago

It's a bit hard to provide configuration options to GwtMockitoTestRunner, since the @RunWith annotation doesn't allow you to pass additional arguments. One way to do it might be via extension, so you could override GwtMockitoTestRunner to create your own runner that set a parent classloader before starting. We'd probably have to expose some protected methods and change the initialization order a little to make things work. Do you think that would do what you want?

ekuefler commented 11 years ago

I was doing some similar things, so I went ahead and implemented this in 8e9c9e2a17499be8fed4c35c2f45d6761d14f010 and 015ec4fc58d78104d32388334d49c4170ebcee6e. You can now define your own runner extending GwtMockitoTestRunner, and override methods to specify the parent classloader and any additional classpath entries you need to add. Could you let me know if this addresses your problem? If so I will include these changes in the next release.

bwolff commented 11 years ago

Hi Erik,

I took a look at the commits and it might achieve what we need. I will give it a try as soon as I find the time an report back on this issue.

In any case, many thanks for your efforts and fast reponses!

Cheers, Ben

ghost commented 11 years ago

Hello! Finally we have managed to integrate the library in the our technology stack, so far i purpose to close it. Thanks, one more time for the good library.

bwolff commented 11 years ago

Hi Erik,

as parcque (who is actually my colleague ;)) mentioned, the new classpath hook you provided now allows us to integrate the GwtMockitoRunner into Grails. Feel free to close this issue.

Good job!

Cheers, Ben

uplight-dev commented 9 years ago

Hi

How did you fixed your setup? I still get an error because the ClassPool is not containing the paths from the overriden getParentClassloader(). In GwtMockitoTestRunner#onLoad(...) the pool.get(name) fails because the paths from the overriden getParentClassloader() are not there. In my case, apart from classPool.appendSystemPath() I've also added: classPool.appendClassPath(new LoaderClassPath(getParentClassloader())); to make it work.

Is there any alternative better fix? For my fix I have to patch the library as the ClassPool initializer is not possible to be overriden.

Thank you

ekuefler commented 9 years ago

Ah interesting - yes I think you're right that this is a bug when overriding the parent classloader. We build a class pool based on the system classpath and any provided additional classpaths. But rather than the system classpath, we should actually be using the classpath of the parent classloader (which is the system classpath by default).

I'll create a fix for this, thanks for pointing it out.

ekuefler commented 9 years ago

Okay, I think that's all that's needed. Let me know if this works for you.

tazle commented 8 years ago

I think the root cause is that you initialize the ClassPool based on "system class path", which is actually based on java.lang.Object. This doesn't seem to be very useful in IDEA's test environment.

In addition to classPool.appendSystemPath() you should perhaps also classPool.appendClassPath(new LoaderClassPath(Thread.getContextClassLoader())). I haven't actually tested this, though.

tazle commented 8 years ago

The latest version seems to be something similar already. I'll test with that.

Except it hasn't been released. Oh well, I guess I can test a custom-built version.

tazle commented 8 years ago

When I change getParentClassLoader() to return Thread.currentThread().getContextClassLoader() in master, I can run my tests in IDEA.