Skip to content

Commit

Permalink
docs(testing): Add main section and subsections for controllers and s…
Browse files Browse the repository at this point in the history
…etup
  • Loading branch information
LeStegii committed May 15, 2024
1 parent 2f9dd59 commit 9840f92
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 2 deletions.
47 changes: 47 additions & 0 deletions docs/testing/1-setup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Setup

In order to properly test your application, it is recommended to use [TestFX](https://github.com/TestFX/TestFX) alongside [Mockito](https://github.com/mockito/mockito).
For a full explanation of both libraries, checkout their official documentation, as the following documentation will only cover a small part of what the projects have to offer.
## TestFX

TestFX can be used to test the frontend of your application by checking if certain requirements are met, for example view elements being visible or having a certain property.

Alongside TestFX, we also include Monocle which allows for headless testing without the app having to be open on your screen every time the tests are ran.

```groovy
testImplementation group: 'org.testfx', name: 'testfx-junit5', version: testFxVersion
testImplementation group: 'org.testfx', name: 'openjfx-monocle', version: monocleVersion
```

To enable headless testing, the following lines can be added to your `test` gradle task:

```groovy
test {
// ...
if (hasProperty('headless') || System.getenv('CI')) {
systemProperties = [
'java.awt.headless': 'true',
'testfx.robot' : 'glass',
'testfx.headless' : 'true',
'glass.platform' : 'Monocle',
'monocle.platform' : 'Headless',
'prism.order' : 'sw',
'prism.text' : 't2k',
]
}
}
```

Whenever the tests are ran with `CI=true`, headless mode will be enabled allowing for testing in CI environments like GH Actions.

## Mockito

Mockito is used to redefine certain methods in the code which currently aren't being tested but could influence the test results, for example by accessing an external API.

```groovy
testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: mockitoVersion
```

---

[Overview](README.md) | [Testing Controllers ➡](2-controllers)
83 changes: 83 additions & 0 deletions docs/testing/2-controllers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Testing Controllers

In the following section, you will learn how to test a basic controller using TestFX and Mockito.

## ControllerTest

Testing controllers using TestFX requires the test to extend from `ApplicationTest`.
It is however recommended to create a helper class called something like `ControllerTest` extending `ApplicationTest` instead of extending it directly.
This class will contain some common code to reduce the amount of boilerplate required for each controller test.

```java
public class ControllerTest extends ApplicationTest {

@Spy
public final App app = new App();
@Spy
protected final ResourceBundle resources = ...; // Define common instances here and mock/spy them

protected Stage stage; // Useful for checking the title for example

@Override
public void start(Stage stage) throws Exception {
super.start(stage);
this.stage = stage;
app.start(stage);
stage.requestFocus(); // Make the test use the correct stage
}
}
```

The main annotations offered by Mockito are `@Spy` and `@Mock`.
Mocking an instance completely removes all default behaviour and content of methods, fields and such, resulting in an empty shell which can later be redefined.
This is useful if the real behaviour isn't needed at all, but the instance itself has to exist.
Spying an instance doesn't touch the default behaviour but allows redefining parts of the logic.

Spies and Mocks can later be injected into the controller instance which is being tested using `@InjectMocks`.

## Writing a real test

Since most of the setup is already defined in the `ControllerTest` class we can just extend it for our own tests.

```java
@ExtendWith(MockitoExtension.class)
public class SetupControllerTest extends ControllerTest {

@InjectMocks
SetupController setupController;

@Override
public void start(Stage stage) throws Exception {
super.start(stage); // It is important to call super.start(stage) to setup the test correctly
app.show(setupController);
}

@Test
public void test() {
// Since we don't really want to show a different controller, we mock the show() method's behaviour to just return a vbox
doReturn(new VBox()).when(app).show(any(), any());

assertEquals("Ludo - Set up the game", app.stage().getTitle());

// TestFX offers different methods for interacting with the application
moveTo("2");
moveBy(0, -20);
press(MouseButton.PRIMARY);
release(MouseButton.PRIMARY);
clickOn("#startButton");

// Mockito can be used to check if the show() method was called with certain arguments
verify(app, times(1)).show("ingame", Map.of("playerAmount", 2));

}

}
```

Whenever something is loading asynchronously the method `waitForFxEvents()` should be called before checking the results.
This assures that all JavaFX events have been run before continuing the tests.
Another way of waiting is the `sleep()` method, which allows to wait for a predefined time.

---

[⬅ Setup](1-setup.md) | [Overview](README.md) | [Testing SubComponents ➡](2-subcomponents.md)
10 changes: 10 additions & 0 deletions docs/testing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Testing

There are plenty of ways to test different parts of your application.
This section covers the testing of controllers including view tests using TestFX and mocking using Mockito.
Since fulibFx uses Dagger internally and for example applications, the last subsection also contains some hints for working with dagger in tests.

1. [Setup](1-setup.md)
2. [Testing Controllers](2-controllers.md)
3. [Testing SubComponents](3-subcomponents.md)
4. [Testing with Dagger](4-dagger.md)
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import javafx.scene.effect.BlurType;
import javafx.scene.effect.Shadow;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
Expand Down Expand Up @@ -94,12 +95,13 @@ void drawPieces() {

@OnRender
void setupDice() {
this.diceSubComponent.setOnMouseClicked(event -> rollDice());
this.diceSubComponent.setOnMouseClicked(event -> rollDice(null));
this.subscriber.bind(this.diceSubComponent.eyesLabel.textFillProperty(), this.currentPlayer.map(player -> Color.web(Constants.COLORS.get(player.getId()))));
}

@OnKey(code = KeyCode.R)
void rollDice() {
void rollDice(KeyEvent event) {
System.out.println("TEST");
if (!this.diceSubComponent.isEnabled()) return;
LudoUtil.playSound(Constants.SOUND_ROLL_DICES);
this.subscriber.subscribe(this.diceSubComponent.roll(), Schedulers.computation(),
Expand Down

0 comments on commit 9840f92

Please sign in to comment.