Issues with launching SimpleApplication as JUnit Testcase

Hey JME Community,

even though I am using JMonkey Engine now for nearly over a year, I am new to the community here and this is my first post :slight_smile:

It was always possible for me to solve my issues thanks to the posts here and the documentation, but since the update today to JMonkey Engine 3.5.0-stable (previously I used 3.4.1-stable) I have some issues with my “personal setup”.

First – here is a little example of my issue:

import org.junit.Test;

import com.jme3.app.SimpleApplication;

public class Example extends SimpleApplication {

	public static void main(String[] args) {
		Example app = new Example();
		app.start();
	}

	@Test
	public void launchAsTest() {
		this.start();
	}

	@Override
	public void simpleInitApp() {
		System.out.println("Initializing!");
	}
}

Since a few months, I prefer it to launch my SimpleApplications as a JUnit test case. Please, I want to avoid discussing if it is useful to launch an application as a test case here. Launching an application as a test case was never a problem. Since the update to version 3.5.0 today, however, no application window pops up anymore when I run my applications as a JUnit test case. It stands out that no error is thrown, and even some output shows up on the console:

Gtk-Message: 09:52:54.342: Failed to load module "canberra-gtk-module"
Jan 31, 2022 9:52:57 AM com.jme3.system.JmeDesktopSystem initialize
INFO: Running on jMonkeyEngine 3.5.0-stable
 * Branch: HEAD
 * Git Hash: 7add915
 * Build Date: 2022-01-23

But, no application window appears.

If I run my examples as a Java application (“calling” the main method), however, the application starts as usual (some output to the console and then the application window appears).

What strikes me, are the following two things:

  1. If I run the application via the test case approach, the line “Initializing” never gets printed to the console.
  2. No error is thrown, but as mentioned, no application window appears. It seems like the application is not launching properly.

Does anyone of you experienced something similar after updating to version 3.5.0? Of course, I could run all my applications via the main method (“Run as Java Application”). But I try to, and want to understand, why it is not possible anymore for me to launch my applications as a JUnit test case.

Greetings
Nico

1 Like

Hi nhahn, good to meet you.

I do exactly the same thing, I think its imminently sensible to have automated tests for the whole application (if you call them system tests it calms people down).

I’ve just run some of my JUnit system tests using 3.5.0-stable so we should be able to find a way to make it work. Did you by any chance change from jme3-lwjgl to jme3-lwjgl3? Both work fine with JUnit tests but their threading behaviour is different so the way the game is launched needs to be changed.

I presume your Junit test case and JMonkey game run on different threads?

I boot the real application as follows

static CrossThreadGameStarter starter = new CrossThreadGameStarter();

later

starter.executeStartCommand( () -> Main.main(new String[]{TEST_MODE}));

CrossThreadGameStarter is a nasty busy wait loop and a static call to somewhere my Main application class puts itself but it works

public class CrossThreadGameStarter {
    private Executor executor = Executors.newSingleThreadExecutor();

    /**
     * Will start up One million worlds (given the correct command)
     * then will block the calling thread until it is initialised.
     * At which point the method will return
     */
    public void executeStartCommand(Runnable startCommand){
        Main.main = null; // <--- This Main.main probably isn't a good idea and is specific to my set up
        executor.execute( startCommand );

        while(Main.main == null || !Main.main.hasBeenInitialised){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

}

Edit

Looking at your example source though. While its perfectly fine to unit test a whole SimpleApplication, having the test actually be a SimpleApplication is a little surprising. I would have expected a seperate test class that spun up a thread and called the main method of your simpleApplication (and did that once per test). I am increasingly convinced that this is a jme3-lwjgl vs jme3-lwjgl3 problem though. As in jme3-lwjgl the app.start(); returned having spawned a thread, while in jme3-lwjgl3 it doesn’t. See LWJGL3 JME doesn't return from app.start() but LWJGL2 did for more

2 Likes

Hey richtea,

thank you very much for your reply!

I just had a quick view on your answer, but was unable to analyze and test it yet. I hope I can study your answer later today :sunglasses:

You are right, with the update to version 3.5.0-stable, I have also added jme3-lwjgl3 to the dependencies of my project. After finding out, that no application window appears anymore, I changed the dependency of my project from LWJGL3 back to LWJGL again. But the problem remains.

However, I will study your solution later. I am honest, I have never seen an approach like yours to launch an application. Would it be possible for you, to post a minimal working example? I would highly appreciate that!

Finally, a brief comment on why my test cases are actually a SimpleApplication. I am not working on a single application, I am developing a whole framework to simulate robots. I want to build a framework that simplifies the use of JME3 to simulate robots, e.g., during manufacturing. So in my case, each test case is a small application that tests some features of my framework. I hope this clarifies things :slight_smile:

Absolutely, I’m at work currently but I’ll try and put a minimal example together later tonight.

1 Like

I’ve created a simple example of how a system test can be written for JMonkeyengine. Its at GitHub - richardTingle/jmeSystemTestExample: A minimal example of a JMonkey engine being system tested but I’ll describe the main classes here.

I’ve considered the following key principles

  • A test is a more or less linear series of steps and checks; a script that runs from top to bottom.
  • A JMonkey application is a continuously updating application (it has an update loop that runs 60 times a second and it shouldn’t need to care about what the test is doing).

System Test

So the test I created looks like the following

@Test(timeout = 30000)
public void exampleSystemTest() {
    TestDriver testDriver = TestDriver.bootAppForTest();

    //mutate the app, giving the box a velocity
    testDriver.actOnTick( app -> app.setBoxVelocity(new Vector3f(0,0.1f,0)));

    //wait for the box to move
    testDriver.waitFor(5);

    //check it's where we expect it to be after that 5 seconds has elapsed
    float boxHeight = testDriver.getOnTick(app -> (float)app.getBoxPosition().y);
    assertEquals(0.5f,boxHeight, 0.1f );
}

That looks more or less like a normal unit test, but it has some interesting things in it, like `testDriver.waitFor(5). That shows why the test and app must be on different threads. The test wants to wait, but the app wants to just do whatever it would normally do over those 5 seconds (during which the box will slowly move).

testDriver.actOnTick and testDriver.getOnTick allow the test to schedule things to happen within the applications thread while waiting for that to happen. You could probably get away with directly modifying and querying the app from within the test in this case but its a bad idea, you don’t know what the app will happen to be doing while you’re mutating it from another thread so more complex things could fail badly if directly mutated.

TestDriver

The test driver provides a safe way to interact with the main application. Its basically just a queue of code to run, and the application will run the code when its the right point in the update tick. Note that everything I have here blocks the test, but in my real code not everything is blocking. I call these non-blocking things watches as they typically take readings over a period while the test may execute further commands, then later the watch is asked what it observed.

public class TestDriver extends BaseAppState{

    private static Executor executor = Executors.newSingleThreadExecutor( (r) -> {
        Thread thread = new Thread(r);
        thread.setDaemon(true);
        return thread;
    });

    private CopyOnWriteArrayList<InAppEvent> actOnTickQueue = new CopyOnWriteArrayList<>();

    @Override
    public void update(float tpf){
        super.update(tpf);

        actOnTickQueue.forEach(event -> {
            event.timeTillRun-=tpf;
            if (event.timeTillRun<0){
                actOnTickQueue.remove(event);
                event.runnable.accept((App)getApplication());
                synchronized(event.waitObject){
                    event.waitObject.notify();
                }
            }
        });

    }

    /**
     * Passes a piece of code to run within the game's update thread.
     *
     * Blocks the calling thread until that tick has occurred
     */
    public void actOnTick(Consumer<App> runnable){
        getOnTick(app -> {
            runnable.accept(app);
            return null;
        } );
    }

    /**
     * Obtains some object from the application within the update tick. Blocks until the object is obtained
     */
    public <T> T getOnTick(Function<App, T> obtainFunction){
        List<T> mutable = new ArrayList<>();

        Object waitObject = new Object();
        actOnTickQueue.add(new InAppEvent((app) -> mutable.add(obtainFunction.apply(app)), 0, waitObject));
        synchronized(waitObject){
            try{
                waitObject.wait();
            } catch(InterruptedException e){
                throw new RuntimeException(e);
            }
        }

        return mutable.get(0);
    }

    /**
     * Pauses the test until the specified time has elapsed
     * @param timeToWait
     */
    public void waitFor(double timeToWait){
        Object waitObject = new Object();
        actOnTickQueue.add(new InAppEvent((app) -> {}, timeToWait, waitObject));
        synchronized(waitObject){
            try{
                waitObject.wait();
            } catch(InterruptedException e){
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * Boots up the application on a separate thread (blocks until that happens) and returns a test driver that can be
     * used to safely interact with it
     */
    public static TestDriver bootAppForTest(){
        TestDriver testDriver = new TestDriver();

        App app = new App(testDriver);
        AppSettings appSettings = new AppSettings(true);
        appSettings.setFrameRate(60);
        app.setSettings(appSettings);
        app.setShowSettings(false);

        executor.execute(() -> app.start());

        while( !app.hasBeenInitialised){
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return testDriver;
    }

    private static class InAppEvent{
        Consumer<App> runnable;
        double timeTillRun;
        final Object waitObject;

        public InAppEvent(Consumer<App> runnable, double timeTillRun, Object waitObject){
            this.runnable = runnable;
            this.timeTillRun = timeTillRun;
            this.waitObject = waitObject;
        }
    }
}

The app itself is pretty normal and doesn’t really need to know its under test (other than the hasBeenInitialised which probably could be avoided now I think about it by using a similar concept but putting it in the TestDriver.)

public class App extends SimpleApplication {

    // A real application wouldn't just dump these in the SimpleApplication. This is a minimal example
    Vector3f boxPosition = new Vector3f(0,0,0);
    Vector3f boxVelocity = new Vector3f(0,0,0);
    Geometry boxGeometry;

    public boolean hasBeenInitialised = false;

    public static void main(String[] args){
        App app = new App();
        app.start(); // start the game
    }

    public App(AppState... initialStates){
        super(initialStates);
    }

    @Override
    public void simpleUpdate(float tpf){
        super.simpleUpdate(tpf);
        boxPosition = boxPosition.add(boxVelocity.mult(tpf));

        boxGeometry.setLocalTranslation(boxPosition);
    }

    public void setBoxVelocity(Vector3f boxVelocity){
        this.boxVelocity = boxVelocity;
    }

    public Vector3f getBoxPosition(){
        return boxPosition;
    }

    @Override
    public void simpleInitApp() {
        stateManager.attach(new TestDriver());

        Box b = new Box(1, 1, 1);
        boxGeometry = new Geometry("Box", b);
        Material mat = new Material(assetManager,
                "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        boxGeometry.setMaterial(mat);
        rootNode.attachChild(boxGeometry);

        hasBeenInitialised = true;
    }

}

Thoughts

Even if your use of JUnit isn’t exactly the system tests I’m talking about I think its still very sensible to separate out the “script” that the test will describe and the “free wheeling” JMonkey application

There’s no reason TestDriver couldn’t be generic, and so be usable for the many applications you wish to script. I haven’t done this here to more clearly show the concept

Other considerations

What if the application crashes

For the purpose of brevity (and because I didn’t want to write it) I haven’t included some considerations, like what if the application crashes. As it stands that will lead to the test just timing out (or running forever if no timeout is set). What should actually happen is that the test should fail immediately. Thats all doable but requires special handling

Library?

I have been thinking this sort of approach could be rolled into a JMonkey testing library, but I’m trying not to have too many parallel projects.

4 Likes

Thank you very much for this small example project! This is remarkable, great support. :slight_smile:

Unfortunately, I was busy today, so I was not able yet to investigate your example. But I created a little “system test” example in my project according to your example above, and it runs. I will look into your example more detailed and come back, if there are any questions. Hopefully, there will be time for that tomorrow or on Thursday.

Kind regards,
Nico

PS: I would support a JME3 testing library!

2 Likes

That’s the sort of high quality answer that should also be on the wiki, in my humblest opinion.

Maybe the code should also be in either jME or in a library?

2 Likes