Add test templates

This commit is contained in:
Oystein Kristoffer Tveit 2021-04-15 14:17:46 +00:00
parent 38ea6a91fe
commit 94e81e5e08
17 changed files with 570 additions and 40 deletions

View File

@ -3,15 +3,33 @@
# and
# https://gitlab.stud.idi.ntnu.no/tdt4140-staff/examples/-/blob/master/.gitlab-ci.yml
image: maven:3.6.3-openjdk-15
image: maven:3-openjdk-15-slim
variables:
# This will suppress any download for dependencies and plugins or upload messages which would clutter the console log.
# `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
MAVEN_OPTS: "-Dhttps.protocols=TLSv1.2 -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN -Dorg.slf4j.simpleLogger.showDateTime=true -Djava.awt.headless=true"
MAVEN_OPTS: " \
-Dhttps.protocols=TLSv1.2 \
-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository \
-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=WARN \
-Dorg.slf4j.simpleLogger.showDateTime=true \
-Djava.awt.headless=true"
# As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
# when running from the command line.
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version"
MAVEN_CLI_OPTS: " \
--batch-mode \
--errors \
--fail-at-end \
--show-version \
-Dprism.verbose=true \
-Dtestfx.robot=glass \
-Dtestfx.headless=true \
-Dglass.platform=Monocle \
-Dprism.order=sw \
-Dprism.text=t2k \
-Dtestfx.setup.timeout=60000"
# Cache downloaded dependencies and plugins between builds.
# To keep cache across branches add 'key: "$CI_JOB_NAME"'
@ -38,6 +56,8 @@ unittest:
stage: test
needs: [build]
script:
- "apt update"
- "apt install -y openjfx"
- "mvn package $MAVEN_CLI_OPTS"
artifacts:
paths:
@ -46,12 +66,15 @@ unittest:
reports:
junit:
- target/surefire-reports/TEST-*.xml
- target/failsafe-reports/TEST-*.xml
# TODO: Separate unit tests and integration tests
# - target/failsafe-reports/TEST-*.xml
generate-coverage:
stage: docs
script:
- 'mvn clean jacoco:prepare-agent test jacoco:report'
- "apt update"
- "apt install -y openjfx"
- 'mvn clean jacoco:prepare-agent test $MAVEN_CLI_OPTS jacoco:report'
- 'cat target/site/jacoco/index.html'
coverage: '/Total.*?([0-9]{1,3})%/'
artifacts:

32
pom.xml
View File

@ -42,7 +42,6 @@
<version>3.2.0-01</version>
</dependency>
<!-- JUnit 5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
@ -65,6 +64,36 @@
<scope>test</scope>
</dependency>
<!-- Hamcrest - Matchers to help testing the JavaFX UI -->
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.1</version>
<scope>test</scope>
</dependency>
<!-- Monocle - Headless UI testing in CI -->
<dependency>
<groupId>org.testfx</groupId>
<artifactId>openjfx-monocle</artifactId>
<version>jdk-12.0.1+2</version>
<scope>test</scope>
</dependency>
<!-- Mockito - Mocking Library for performing isolated unit tests -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.8.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>2.23.0</version>
<scope>test</scope>
</dependency>
<!-- JavaDoc -->
<dependency>
<groupId>org.apache.maven.plugins</groupId>
@ -133,7 +162,6 @@
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -35,7 +35,8 @@ public class Main extends Application {
*/
private void setupWindow(Stage window) {
window.setTitle(TITLE);
window.getIcons().add(new Image(getClass().getResourceAsStream(ICON_PATH)));
if (window.getIcons().isEmpty())
window.getIcons().add(new Image(getClass().getResourceAsStream(ICON_PATH)));
}
/**
@ -53,6 +54,7 @@ public class Main extends Application {
*/
private void createScene() {
this.scene = new Scene(fxmlRoot);
this.scene.setUserData(this.fxmlLoader);
Model.setScene(scene);
}

View File

@ -66,6 +66,16 @@ public class MainController implements Initializable {
return hostServices;
}
//TODO: Document
public List<Controller> getInnerControllers() {
return List.of(
editorController,
filetreeController,
modelineController,
menubarController
);
}
/**
* Set a reference to the global Host Services API
*

View File

@ -66,6 +66,11 @@ public class EditorController implements Initializable, Controller, FileManageme
this.eventBus.register(this);
}
// TODO: document
public CodeArea getEditor() {
return editor;
}
/**
* Applies highlighting to the editor.
*
@ -146,21 +151,20 @@ public class EditorController implements Initializable, Controller, FileManageme
* @throws FileNotFoundException
*/
public void setEditorContent(String filePath) {
if (filePath == null) {
editor.clear();
editor.appendText("// New File");
return;
}
// if (filePath == null) {
// editor.clear();
// editor.appendText("// New File");
// return;
// }
try (Scanner sc = new Scanner(new File(filePath))) {
if (filePath.endsWith(".java") || filePath.endsWith(".md")) {
// if (filePath.endsWith(".java") || filePath.endsWith(".md")) {
editor.clear();
while (sc.hasNextLine()) {
editor.appendText(sc.nextLine());
editor.appendText("\n");
editor.appendText(sc.nextLine() + "\n");
}
} else {
throw new FileNotFoundException();
}
// } else {
// throw new FileNotFoundException();
// }
} catch (FileNotFoundException ex) {
Alert error = new Alert(AlertType.ERROR);
@ -208,8 +212,8 @@ public class EditorController implements Initializable, Controller, FileManageme
* Updates Code Area (read from file) whenever the FileSelected is changed
*/
@Subscribe
private void handle(FileSelectedEvent event) {
this.setEditorContent(event.getPath());
public void handle(FileSelectedEvent event) {
this.setEditorContent(event.getPath());
}
/**
@ -226,7 +230,7 @@ public class EditorController implements Initializable, Controller, FileManageme
}
@Subscribe
private void handle(ToggleCommentEvent event) {
public void handle(ToggleCommentEvent event) {
this.toggleComment();
}

View File

@ -9,6 +9,7 @@ import javafx.scene.image.ImageView;
import javafx.scene.input.MouseEvent;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
@ -124,12 +125,26 @@ public class FiletreeController implements Initializable, Controller {
String name = file.getName();
String ext = (name.substring(file.getName().lastIndexOf(".") + 1, file.getName().length()));
if ("java".equals(ext))
createExtension(name, java, parent);
else if ("md".equals(ext))
createExtension(name, md, parent);
else
createExtension(name, placeholder, parent);
try {
createExtension(name, getIconForFile(file), parent);
} catch (Exception e) {
System.err.println("ICON NOT FOUND: " + file.getPath());
}
// if ("java".equals(ext))
// createExtension(name, java, parent);
// else if ("md".equals(ext))
// createExtension(name, md, parent);
// else
// createExtension(name, placeholder, parent);
}
private Image getIconForFile(File file) throws IOException {
String mimeType = Files.probeContentType(file.toPath()).replace('/', '-');
String iconPath = (mimeType != null)
? "/graphics/filetreeicons/" + mimeType + ".png"
: "/graphics/filetreeicons/file.png";
return new Image(getClass().getResourceAsStream(iconPath));
}
private void createExtension(String name, Image image, CheckBoxTreeItem<String> parent) {

View File

@ -3,4 +3,4 @@ package app.events;
/**
* Base class for any type of event of the eventbus
*/
abstract class Event {}
public abstract class Event {}

View File

@ -13,7 +13,7 @@ import app.model.ProgrammingLanguage;
* Common static operations that can be executed on any class
* that implements {@link app.model.ProgrammingLanguage ProgrammingLanguage}
*/
public class LanguageOperations {
public final class LanguageOperations {
/**
* Use a matcher to find the styleclass of the next match

View File

@ -10,7 +10,8 @@
prefHeight="400"
xmlns="http://javafx.com/javafx/8.0.65"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="app.MainController">
fx:controller="app.MainController"
fx:id="root">
<top>
<!-- Menubar -->

View File

@ -0,0 +1,44 @@
package app;
import javafx.scene.Node;
import javafx.stage.Stage;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.testfx.api.FxToolkit;
import org.testfx.framework.junit5.ApplicationTest;
import org.testfx.util.WaitForAsyncUtils;
import java.util.concurrent.TimeoutException;
public class FxTestTemplate extends ApplicationTest {
private Stage stage;
@BeforeEach
public void runAppToTests() throws Exception {
FxToolkit.registerPrimaryStage();
FxToolkit.setupApplication(Main::new);
FxToolkit.showStage();
WaitForAsyncUtils.waitForFxEvents(100);
}
@AfterEach
public void stopApp() throws TimeoutException {
FxToolkit.cleanupStages();
}
@Override
public void start(Stage primaryStage){
this.stage = primaryStage;
primaryStage.toFront();
}
public Stage getStage() {
return stage;
}
public <T extends Node> T find(final String query) {
/** TestFX provides many operations to retrieve elements from the loaded GUI. */
return lookup(query).query();
}
}

View File

@ -1,16 +1,74 @@
package app;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.IOException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.platform.commons.annotation.Testable;
public class MainTest {
import app.controllers.*;
@Test
@DisplayName("Temp Test")
public void tempTest() {
assertEquals(1, 1);
}
}
import java.util.List;
import java.util.stream.Collectors;
import javafx.scene.layout.BorderPane;
import app.testing.FxTestTemplate;
@Testable
public class MainTest extends FxTestTemplate {
@Test
@DisplayName("Check that the stage title is correct")
public void should_have_stage_title() {
assertEquals("Banana Editor", this.getStage().getTitle());
}
@Test
@Order(1)
@DisplayName("Check that the stage has an icon")
public void should_have_stage_icon() {
assertEquals(1, this.getStage().getIcons().size());
}
@Test
@Order(2)
@DisplayName("Check that the root element is present")
public void should_have_root() {
BorderPane app = (BorderPane) find("#root");
assertNotNull(app);
}
@Test
@Order(3)
@DisplayName("Check that all subcontrollers are present")
public void should_have_subcontrollers() {
this
.getMainController()
.getInnerControllers()
.forEach((Controller controller) -> assertNotNull(controller));
}
@Test
@DisplayName("Check that the scene is correct")
public void should_have_scene() throws IOException {
assertNotNull(this.getStage().getScene());
}
@Test
@DisplayName("Check that the CSS is set")
public void should_have_css() {
List<String> expectedCSS =
List.of("/styling/themes/monokai.css", "/styling/languages/java.css")
.stream()
.map(p -> getClass().getResource(p).toExternalForm())
.collect(Collectors.toList());
assertEquals(expectedCSS, this.getStage().getScene().getStylesheets());
}
}

View File

@ -0,0 +1,238 @@
package app.controllers;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.List;
import java.util.stream.Collectors;
import org.fxmisc.richtext.CodeArea;
import org.fxmisc.richtext.model.StyleSpans;
import com.google.common.eventbus.EventBus;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockedStatic;
import org.mockito.junit.jupiter.MockitoExtension;
import app.testing.FxTestTemplate;
import app.model.Model;
import app.model.ProgrammingLanguage;
import app.service.LanguageOperations;
import app.events.CopyEvent;
import app.events.CutEvent;
import app.events.FileSelectedEvent;
import app.events.LanguageChangedEvent;
import app.events.PasteEvent;
import app.events.RedoEvent;
import app.events.ToggleCommentEvent;
import app.events.ToggleWrapTextEvent;
import app.events.UndoEvent;
@ExtendWith(MockitoExtension.class)
public class EditorControllerTest extends FxTestTemplate {
@Captor
private ArgumentCaptor<String> captor;
@Mock
private CodeArea editor;
private EventBus eventBus;
@InjectMocks
private EditorController controller;
private String mockContent = """
class HelloWorld {
private String message = "Hello world";
public String getMessage() {
return message;
}
}
""";
private String mockLine = "private String message = \"Hello world\";";
@BeforeEach
public void insertEventBus() {
this.eventBus = new EventBus();
this.controller.setEventBus(eventBus);
}
@Test
@DisplayName("Test handling of FileSelectedEvent with a real file")
public void testFileSelectedEventWithRealFile() throws IOException {
String resourcePath = "/testfile.txt";
String filePath = getClass().getResource(resourcePath).getPath();
File file = new File(filePath);
List<String> content =
Files.readAllLines(file.toPath())
.stream()
.map(s -> s + "\n")
.collect(Collectors.toList());
eventBus.post(new FileSelectedEvent(filePath));
verify(editor, times(content.size())).appendText(captor.capture());
assertEquals(content, captor.getAllValues());
}
@Test
@DisplayName("Test handling of FileSelectedEvent with a file that doesn't exist")
public void testFileSelectedEventWithUnrealFile() throws IOException {
String brokenFilePath = "/doesNotExist.txt";
eventBus.post(new FileSelectedEvent(brokenFilePath));
verify(editor, never()).clear();
}
@Test
@DisplayName("Test handling of LanguageChangedEvent")
public void testLanguageChangedEvent(){
when(editor.getText()).thenReturn(mockContent);
try (MockedStatic<LanguageOperations> mocked = mockStatic(LanguageOperations.class)) {
mocked.when(() -> LanguageOperations.syntaxHighlight(anyString(), any()))
.thenReturn(StyleSpans.singleton(null, 0));
eventBus.post(new LanguageChangedEvent("markdown"));
mocked.verify(() -> LanguageOperations.syntaxHighlight(anyString(), any()));
}
}
@Test
@DisplayName("Test handling of ToggleCommentEvent when not selected")
public void testToggleCommentEventNotSelect(){
ProgrammingLanguage lang = mock(ProgrammingLanguage.class);
when(editor.getSelectedText()).thenReturn("");
when(editor.getText(anyInt())).thenReturn(mockLine);
try (MockedStatic<Model> mocked = mockStatic(Model.class)) {
mocked.when(() -> Model.getLanguage()).thenReturn(lang);
when(lang.isCommentedLine(anyString())).thenReturn(false);
eventBus.post(new ToggleCommentEvent());
verify(lang).commentLine(anyString());
when(lang.isCommentedLine(anyString())).thenReturn(true);
eventBus.post(new ToggleCommentEvent());
verify(lang).unCommentLine(anyString());
}
}
@Test
@DisplayName("Test handling of ToggleCommentEvent when selected")
public void testToggleCommentEventSelect(){
ProgrammingLanguage lang = mock(ProgrammingLanguage.class);
when(editor.getSelectedText()).thenReturn("Selected Text");
try (MockedStatic<Model> mocked = mockStatic(Model.class)) {
mocked.when(() -> Model.getLanguage()).thenReturn(lang);
when(lang.isCommentedSelection(anyString())).thenReturn(false);
eventBus.post(new ToggleCommentEvent());
verify(lang).commentSelection(anyString());
when(lang.isCommentedSelection(anyString())).thenReturn(true);
eventBus.post(new ToggleCommentEvent());
verify(lang).unCommentSelection(anyString());
}
}
@Test
@DisplayName("Test handling of ToggleWrapTextEvent")
public void testToggleWrapTextEvent(){
eventBus.post(new ToggleWrapTextEvent(true));
verify(editor).setWrapText(true);
eventBus.post(new ToggleWrapTextEvent(false));
verify(editor).setWrapText(false);
}
@Test
@DisplayName("Test handling of UndoEvent")
public void testUndoEvent(){
when(editor.isFocused()).thenReturn(true);
eventBus.post(new UndoEvent());
verify(editor, times(1)).undo();
when(editor.isFocused()).thenReturn(false);
eventBus.post(new UndoEvent());
// Should not have been called one more time
verify(editor, times(1)).undo();
}
@Test
@DisplayName("Test handling of RedoEvent")
public void testRedoEvent(){
when(editor.isFocused()).thenReturn(true);
eventBus.post(new RedoEvent());
verify(editor, times(1)).redo();
when(editor.isFocused()).thenReturn(false);
eventBus.post(new RedoEvent());
verify(editor, times(1)).redo();
}
@Test
@DisplayName("Test handling of CopyEvent")
public void testCopyEvent(){
when(editor.isFocused()).thenReturn(true);
eventBus.post(new CopyEvent());
verify(editor, times(1)).copy();
when(editor.isFocused()).thenReturn(false);
eventBus.post(new CopyEvent());
verify(editor, times(1)).copy();
}
@Test
@DisplayName("Test handling of CutEvent")
public void testCutEvent(){
when(editor.isFocused()).thenReturn(true);
eventBus.post(new CutEvent());
verify(editor, times(1)).cut();
when(editor.isFocused()).thenReturn(false);
eventBus.post(new CutEvent());
verify(editor, times(1)).cut();
}
@Test
@DisplayName("Test handling of PasteEvent")
public void testPasteEvent(){
when(editor.isFocused()).thenReturn(true);
eventBus.post(new PasteEvent());
verify(editor, times(1)).paste();
when(editor.isFocused()).thenReturn(false);
eventBus.post(new PasteEvent());
verify(editor, times(1)).paste();
}
}

View File

@ -0,0 +1,18 @@
package app.events;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import app.model.Model;
import app.testing.EventTestTemplate;
public class FileSaveStateChangedEventTest extends EventTestTemplate {
@Test
@DisplayName("Check that model gets changed on constructor")
public void checkModel() {
new FileSaveStateChangedEvent(true);
this.mockModel.verify(() -> Model.setFileIsSaved(true));
}
}

View File

@ -0,0 +1,5 @@
package app.testing;
public class ControllerTestTemplate extends FxTestTemplate {
}

View File

@ -0,0 +1,25 @@
package app.testing;
import static org.mockito.Mockito.mockStatic;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.mockito.MockedStatic;
import app.model.Model;
public class EventTestTemplate extends FxTestTemplate {
public MockedStatic<Model> mockModel;
@BeforeEach
public void openModel() {
mockModel = mockStatic(Model.class);
}
@AfterEach
public void closeModel() {
mockModel.close();
}
}

View File

@ -0,0 +1,56 @@
package app.testing;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.stage.Stage;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.testfx.api.FxToolkit;
import org.testfx.framework.junit5.ApplicationTest;
import org.testfx.util.WaitForAsyncUtils;
import java.util.concurrent.TimeoutException;
import app.Main;
import app.MainController;
public class FxTestTemplate extends ApplicationTest {
private Stage stage;
private Application application;
@BeforeEach
public void runAppToTests() throws Exception {
FxToolkit.registerPrimaryStage();
this.application = FxToolkit.setupApplication(Main::new);
FxToolkit.showStage();
WaitForAsyncUtils.waitForFxEvents(100);
}
@AfterEach
public void stopApp() throws TimeoutException {
FxToolkit.cleanupStages();
FxToolkit.cleanupApplication(this.application);
}
@Override
public void start(Stage primaryStage){
this.stage = primaryStage;
primaryStage.toFront();
}
public Stage getStage() {
return stage;
}
public MainController getMainController() {
return ((FXMLLoader) this.stage.getScene().getUserData()).getController();
}
public <T extends Node> T find(final String query) {
/** TestFX provides many operations to retrieve elements from the loaded GUI. */
return lookup(query).query();
}
}

View File

@ -0,0 +1,3 @@
public class testfile {
}