Questions about testing Griffon

classic Classic list List threaded Threaded
11 messages Options
Reply | Threaded
Open this post in threaded view
|

Questions about testing Griffon

Marcelo
Hi,

I have doubts about how to properly test drive a Griffon application, especially when the view is involved (for example to test if manual connections from controls to the griffon controller were properly set).

I saw examples both using GriffonTestFXClassRule and GriffonTestFXRule. What is the difference between the two?

I am using GriffonTestFXClassRule as shown in the guide for the functional tests, but I want to be sure that is the propper way.

If I have a griffon view named mainview annotated with @ServiceProviderFor(GriffonView.class), what woud be the proper setup for unit testing using TestFx?

What is the best way to force the test to use a mocked controller and a mocked model? Can I use the @BindTo annotation to set the Model and Controller I want to use?

I also noticed that using the annotation @TestFor I don't get a field of type GriffonView named view instantiated with the GriffonView under test. Is this because I am using the GriffonTestFXClassRule instead of the GriffonUnitRule shown in the examples in the guide?

Thanks!
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

aalmiray
Administrator
Hi Marcelo,

GriffonTestFXRule is used for integration testing whereas GriffonTestFXClassRule is used for functional testing.

Here's a table with the key difference between the two classes

|====
|                      | GriffonTestFXRule   | GriffonTestFXClassRule
| test type            | integration         | functional
| application instance | one per test method | one per test class
| override bindings    | yes                 | no
| allows mocking       | yes                 | no
| @TestFor             | yes                 | no
|===

The patterns tutorial includes some tests although none of the sample applications define an UI based integration test. See the following links for reference

Controller Unit test
https://github.com/griffon/griffon/blob/master/tutorials/patterns/pmvc/src/test/java/org/example/SampleControllerTest.java

Controller Integration test
https://github.com/griffon/griffon/blob/master/tutorials/patterns/pmvc/src/integration-test/java/org/example/SampleIntegrationTest.java

Functional test
https://github.com/griffon/griffon/blob/master/tutorials/patterns/pmvc/src/functional-test/java/org/example/SampleFunctionalTest.java

GriffonTestFXRule may be used to unit test Views too, in which case you may need to mock both Model and Controller. You can use the same technique found in the Controller unit test: instantiate a Model using ArtifactManager.

Cheers,
Andres
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

Marcelo
Hi Andres,

Thanks for the information. I used the GriffonTestFXRule to run the test, so it seems I got that right! I had studied all the examples you provided in the guide an examples, but I haven't seen any unit test of the view in isolation. I think I got a solution (well, maybe a starting point), but I feel that I had to know too much of the Griffon's internals and I am wondering if there was a cleaner way...

This is what I did so far:

    @Before
    public void injectMocks() {
        when(controllerMock.getGriffonClass())
                .thenReturn(griffonControllerClassMock);
        when(griffonControllerClassMock.getActionNames()).thenReturn(
                ((GriffonControllerClass) controller.getGriffonClass()).
                        getActionNames());
        when(controllerMock.getApplication()).thenReturn(application);

        actionManager.createActions(controllerMock);

        Injector<?> injector = application.getInjector();
        injector.injectMembers(controller);
        injector.injectMembers(modelMock);

        Mockito.reset(controllerMock, modelMock);
    }

As you can see, I was getting most of the needed behavior from the real MainViewController and using it in the mock. I was going to also to provide TestModulesOverrides to finish the job. Is this overkill? I guess you can see why I am a bit worried...

Thanks again,

Thanks
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

aalmiray
Administrator
Hi Marcelo,

I'm doing some tests on my side and I've found that testing the View in isolation is much harder than I thought. The problem is, subclasses of GriffonArtifact require specific plumbing during setup. This plumbing is performed automatically by the ArtifactManager and the MVCGroupManager.

My current suggestion is to test out Views in integration mode, with real Models and Controllers, and mocked collaborators such as Services et al.

I'll keep searching on an easy way to test out Views in isolation, if possible.

Cheers,
Andres
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

aalmiray
Administrator
In reply to this post by Marcelo
Here's another possible solution. This code is still experimental as the `getTypeClass()` method has been added to `GriffonArtifact` temporarily in 2.10.0-SNAPSHOT. this method is now used internally to resolve the correct type when creating a GriffonClass out of an artifact. In a sense it skips setting up a mock for the controller GriffonClass.

However the test must still inject members on the controller spy _and_ make sure that actions have been initialized. This is because the controller spy is created with a Provider instead of being created by the internal plumbing, thus no "NewInstance" event is triggered by this and no actions get created as a result.

This test also shows a different event that can be used to react to an MVC group being instantiated but before mvcGroupInit/initUI are called on each MVC members.

Please let me know what you think. Perhaps this code can be further simplified.

package org.example;

import griffon.core.GriffonApplication;
import griffon.core.event.EventHandler;
import griffon.core.injection.Module;
import griffon.core.mvc.MVCGroup;
import griffon.core.mvc.MVCGroupConfiguration;
import griffon.core.mvc.MVCGroupManager;
import griffon.core.test.TestModuleOverrides;
import griffon.inject.BindTo;
import griffon.javafx.test.GriffonTestFXRule;
import org.codehaus.griffon.runtime.core.injection.AbstractTestingModule;
import org.junit.Rule;
import org.junit.Test;

import javax.annotation.Nonnull;
import javax.inject.Inject;
import java.util.List;

import static java.util.Arrays.asList;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.notNullValue;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;

public class SampleViewTest {
    @Rule
    public GriffonTestFXRule testfx = new GriffonTestFXRule("mainWindow");

    @Inject private MVCGroupManager mvcGroupManager;

    @Test
    public void verify_view_setup() throws Exception {
        // given:
        // model is a real reference
        SampleModel model = mvcGroupManager.findModel("sample", SampleModel.class);
        // controller is a spy
        SampleController controller = mvcGroupManager.findController("sample", SampleController.class);

        // expectations:
        String input = "hello";
        final StringCapture capture = new StringCapture();
        // capture action method name if invoked
        doAnswer(invocation -> capture.setValue(invocation.getMethod().getName())).when(controller).sayHello();

        // when:
        testfx.clickOn("#input").write(input);
        testfx.clickOn("#sayHelloActionTarget");
        // wait for controller action to be invoked outside the UI thread
        await().timeout(2, SECONDS).until(() -> capture.getValue(), notNullValue());

        // then:
        assertThat(model.getInput(), equalTo(input));
        assertThat(capture.getValue(), equalTo("sayHello"));
    }

    @Nonnull
    @TestModuleOverrides
    private List<Module> mockedBindings() {
        return asList(new AbstractTestingModule() {
            @Override
            protected void doConfigure() {
                bind(SampleController.class)
                    .toProvider(() -> {
                        // use a spy instead of a mock
                        SampleController controller = spy(SampleController.class);
                        // return SampleController instead of Mockito subclass
                        when(controller.getTypeClass()).thenReturn(SampleController.class);
                        return controller;
                    })
                    .asSingleton();
            }
        });
    }

    @BindTo(EventHandler.class)
    public static class MVCGroupEventHandler implements EventHandler {
        @Inject private GriffonApplication application;

        public void onInitializeMVCGroup(MVCGroupConfiguration configuration, MVCGroup group) {
            // initialize members as they were not injected by the spy provider
            application.getInjector().injectMembers(group.getController());
            // initialize actions on spy before View is initialized
            application.getActionManager().createActions(group.getController());
        }
    }

    private static class StringCapture {
        private String value;

        public String getValue() {
            return value;
        }

        public StringCapture setValue(String value) {
            this.value = value;
            return this;
        }
    }
}
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

Marcelo
Hi Andres,

Thanks for the answer.
I updated my griffon dependencies to version 2.10.0-SNAPSHOT but I can not use the getTypeClass() method. Is there anything else I need to do besides refreshing my dependencies?
 
gradle --refresh-dependencies
 
Marcelo.
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

aalmiray
Administrator
I've not committed the changes yet.

Does the verbosity of the proposed solution make sense?

Cheers,
Andres
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

Marcelo
Hi Andres!

That's why I couldn't test it. hahaha

It is a little verbose, but it is better than not being able to test drive the view at all... One could then three abstract test classes with generics (for test driving views, models and controllers) that will hide the test setup complexity and will expose ready-to-use mocks (or spies).

Marcelo.
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

aalmiray
Administrator
Fair enough.

The changes have been pushed to the source repository. The pmvc project found in the MVC patterns tutorial contains a new test: SampleViewTest, which is the source I pasted yesterday on this thread.

do let me know if any other changes are needed.

Cheers,
Andres
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

Marcelo
Hi Andres,

After you pushed the changes to the repository I am unable to download griffon (which I did with the unmodified 2.10.0-SNAPSHOT with no problem). Even after deleting both the gradle and maven caches I get the following:

> Could not find org.codehaus.griffon:griffon-core-compile:2.10.0-SNAPSHOT.
  Searched in the following locations:
      file:/home/marcelo/.m2/repository/org/codehaus/griffon/griffon-core-compile/2.10.0-SNAPSHOT/maven-metadata.xml
      file:/home/marcelo/.m2/repository/org/codehaus/griffon/griffon-core-compile/2.10.0-SNAPSHOT/griffon-core-compile-2.10.0-SNAPSHOT.pom
      file:/home/marcelo/.m2/repository/org/codehaus/griffon/griffon-core-compile/2.10.0-SNAPSHOT/griffon-core-compile-2.10.0-SNAPSHOT.jar
      https://jcenter.bintray.com/org/codehaus/griffon/griffon-core-compile/2.10.0-SNAPSHOT/maven-metadata.xml
      https://jcenter.bintray.com/org/codehaus/griffon/griffon-core-compile/2.10.0-SNAPSHOT/griffon-core-compile-2.10.0-SNAPSHOT.pom
      https://jcenter.bintray.com/org/codehaus/griffon/griffon-core-compile/2.10.0-SNAPSHOT/griffon-core-compile-2.10.0-SNAPSHOT.jar

I waited a few hours after you confirmed you made the changes. Should I wait longer?

Thanks,

Marcelo.
Reply | Threaded
Open this post in threaded view
|

Re: Questions about testing Griffon

aalmiray
Administrator
This appears to be a problem with build dependencies. I suppose this problem occurs in your application project and not on your copy of the Griffon build, right?

Did you try pushing all binaries to your maven locale repository? Invoking `gradlew puTML` (short for publishToMavenLocal) should do the trick.

Cheers,
Andres