Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add documentation about tests #98

Merged
merged 7 commits into from
May 16, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ More detailed information can be found in the categories below.

- [Controllers and Components](controller/README.md)
- [Tutorial](tutorial/how-to-start.md)
- [Other Features](features/README.md)
- [Other Features](features/README.md)
- [Testing](testing/README.md)
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

LeStegii marked this conversation as resolved.
Show resolved Hide resolved
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.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved

```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.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved

## 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)
84 changes: 84 additions & 0 deletions docs/testing/2-controllers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# 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.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved
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();
LeStegii marked this conversation as resolved.
Show resolved Hide resolved
@Spy
protected final ResourceBundle resources = ...; // Define common instances here and mock/spy them
LeStegii marked this conversation as resolved.
Show resolved Hide resolved

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
}
LeStegii marked this conversation as resolved.
Show resolved Hide resolved
}
```

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.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved

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.
In order to get Mockito working, the class has to be annotated with `@ExtendWith(MockitoExtension.class)`.

```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());
LeStegii marked this conversation as resolved.
Show resolved Hide resolved

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.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved
This assures that all JavaFX events have been run before continuing the tests.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved
Another way of waiting is the `sleep()` method, which allows to wait for a predefined time.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved

---

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

As subcomponents extend from JavaFX nodes mocking them destroys their functionality making them useless and prevents them from being rendered.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved
Spying has similar issues. Another problem with subcomponents is that they often require multiple dependencies like services themselves.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved

Therefor the best way of testing a subcomponent is by creating a field inside the controller test and annotating it with `@InjectMocks` so that all the dependencies are injected into it as well.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved
Since fields annotated with `@InjectMocks` cannot be injected into other fields annotated with the same annotation, this has to be done manually.

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

@Spy
GameService gameService;
@InjectMocks
DiceSubComponent diceSubComponent;
// ...

@InjectMocks
IngameController ingameController;

@Override
public void start(Stage stage) throws Exception {
super.start(stage);
ingameController.diceSubComponent = diceSubComponent; // Manually set the component instance
app.show(ingameController, Map.of("playerAmount", 2));
}

@Test
public void test() {
// ...
}
}
```

---

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

When using Dagger inside the application, testing the app requires a testcomponent to be present.
This component contains all the dependencies the main module provides, but modified in a way that doesn't require a connection for example.

The component itself can just extend the main component and then use modules to override certain dependencies.
Inside the modules Mockito methods such as `spy()` and `mock()` can be used to create the required instances.
LeStegii marked this conversation as resolved.
Show resolved Hide resolved
If specific behaviour is required, the instances can also be created manually.

```java
@Component(modules = {MainModule.class, TestModule.class})
@Singleton
public interface TestComponent extends MainComponent {

@Component.Builder
interface Builder extends MainComponent.Builder {
TestComponent build();
}
}
```

```java
@Module
public class TestModule {

@Provides
GameService gameService() {
return new GameService(new Random(42));
}

}
```

Now that the component and modules exist, we have to create a way of setting the component our app uses.
This step however is dependent on how the application is structured.
The easiest way is to create a setter method and call it, before the app starts.

```java
@Override
public void start(Stage stage) throws Exception {
super.start(stage);
app.setComponent(DaggerTestComponent.builder().mainApp(app).build());
LeStegii marked this conversation as resolved.
Show resolved Hide resolved
app.start(stage);
stage.requestFocus();
}
```
---

[⬅ Testing SubComponents](3-subcomponents.md) | [Overview](README.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)
1 change: 1 addition & 0 deletions ludo/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies {
testImplementation group: 'org.testfx', name: 'openjfx-monocle', version: monocleVersion
testImplementation group: 'org.mockito', name: 'mockito-junit-jupiter', version: mockitoVersion
testAnnotationProcessor group: 'com.google.dagger', name: 'dagger-compiler', version: daggerVersion
testImplementation group: 'org.hamcrest', name: 'hamcrest', version: hamcrestVersion
}

java {
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public class DiceSubComponent extends VBox {
public Label eyesLabel;

@Inject
public GameService gameService;
GameService gameService;

private final BooleanProperty enabled = new SimpleBooleanProperty(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public class IngameControllerTest extends ControllerTest {
GameService gameService;
@Spy
Subscriber subscriber;
@Spy
@InjectMocks
DiceSubComponent diceSubComponent;

@InjectMocks
Expand All @@ -44,7 +44,7 @@ public class IngameControllerTest extends ControllerTest {
@Override
public void start(Stage stage) throws Exception {
super.start(stage);
diceSubComponent.gameService = gameService;
ingameController.diceSubComponent = diceSubComponent;
app.show(ingameController, Map.of("playerAmount", 2));
}

Expand Down