Quellcode durchsuchen

Properly separate responsibilities for FileController so that it can be tested.

Sam Jaffe vor 5 Jahren
Ursprung
Commit
5d77ac54a0

+ 0 - 1
pom.xml

@@ -147,7 +147,6 @@
       <groupId>org.mockito</groupId>
       <artifactId>mockito-junit-jupiter</artifactId>
       <version>3.6.28</version>
-      <scope>test</scope>
     </dependency>
     <dependency>
       <groupId>org.mockito</groupId>

+ 18 - 50
src/main/lombok/org/leumasjaffe/recipe/controller/FileController.java

@@ -6,14 +6,9 @@ import java.io.BufferedWriter;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.FileReader;
-import java.io.FileWriter;
 import java.io.IOException;
 import java.io.Reader;
 import java.io.Writer;
-import java.util.Optional;
-
-import javax.swing.JFileChooser;
-import javax.swing.JOptionPane;
 
 import org.leumasjaffe.recipe.model.Recipe;
 
@@ -21,7 +16,6 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
 
 import lombok.AccessLevel;
-import lombok.NonNull;
 import lombok.RequiredArgsConstructor;
 import lombok.SneakyThrows;
 import lombok.experimental.FieldDefaults;
@@ -29,15 +23,14 @@ import lombok.experimental.NonFinal;
 
 @RequiredArgsConstructor
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class FileController<T extends Component & FileController.Model> {
-	public static interface Model {
+public class FileController {
+	public static interface ViewModel {
 		void setModel(Recipe model);
 	}
 	
 	private static final ObjectMapper mapper;
-	JFileChooser chooser = new JFileChooser();
-	T owner;
-	@NonFinal File filename = null;
+	SaveLoadHandle handle;
+	ViewModel viewmodel;
 	@NonFinal Recipe model = null;
 	
 	static {
@@ -45,79 +38,54 @@ public class FileController<T extends Component & FileController.Model> {
 		mapper.registerModule(new Jdk8Module());
 	}
 	
+	public <T extends Component & ViewModel> FileController(T viewmodel) {
+		this.handle = new SwingSaveLoadHandle(viewmodel);
+		this.viewmodel = viewmodel;
+	}
+	
 	public void create() {
 		setModel(new Recipe());
 	}
 	
 	public void save() {
-		getSaved().ifPresent(this::save);
+		handle.writer().ifPresent(this::save);
 	}
 	
 	public void saveAs() {
-		this.filename = null;
+		handle.clear();
 		save();
 	}
 	
 	public void open() {
-		getOpened().ifPresent(this::load);
+		handle.reader().ifPresent(this::load);
 	}
 
 	@Deprecated
 	@SneakyThrows(FileNotFoundException.class)
 	public void open(final String newFilename) {
-		this.filename = new File(newFilename);
-		load(new FileReader(this.filename));
-	}
-	
-	@SuppressWarnings("resource")
-	@SneakyThrows(IOException.class)
-	Optional<Writer> getSaved() {
-		if (filename != null) {
-			return Optional.of(new FileWriter(filename));
-		} else if (chooser.showOpenDialog(owner) == JFileChooser.APPROVE_OPTION) {
-			filename = chooser.getSelectedFile();
-			return Optional.of(new FileWriter(filename));
-		} else {
-			return Optional.empty();
-		}
+		load(new FileReader(new File(newFilename)));
 	}
 	
-	@SuppressWarnings("resource")
-	@SneakyThrows(FileNotFoundException.class)
-	Optional<Reader> getOpened() {
-		if (chooser.showOpenDialog(owner) == JFileChooser.APPROVE_OPTION) {
-			filename = chooser.getSelectedFile();
-			return Optional.of(new FileReader(filename));
-		} else {
-			return Optional.empty();
-		}
-	}
-	
-	private void load(final @NonNull Reader reader) {
+	private void load(final Reader reader) {
 		try (Reader in = reader;
 				BufferedReader buf = new BufferedReader(in)) {
 			setModel(mapper.readValue(in, Recipe.class));
 		} catch (IOException ioe) {
-			errorPopup(ioe);
+			handle.error(ioe);
 		}
 	}
 
-	private void save(final @NonNull Writer writer) {
+	private void save(final Writer writer) {
 		try (Writer out = writer;
 				BufferedWriter buf = new BufferedWriter(out)) {
 			mapper.writeValue(buf, model);
 		} catch (IOException e) {
-			errorPopup(e);
+			handle.error(e);
 		}
 	}
-
-	void errorPopup(final Exception ex) {
-		JOptionPane.showMessageDialog(owner, ex.getLocalizedMessage(),
-				"File Error", JOptionPane.ERROR_MESSAGE);
-	}
 	
 	private void setModel(Recipe recipe) {
 		this.model = recipe;
-		owner.setModel(recipe);
+		viewmodel.setModel(recipe);
 	}
 }

+ 12 - 0
src/main/lombok/org/leumasjaffe/recipe/controller/SaveLoadHandle.java

@@ -0,0 +1,12 @@
+package org.leumasjaffe.recipe.controller;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Optional;
+
+public interface SaveLoadHandle {
+	Optional<Writer> writer();
+	Optional<Reader> reader();
+	void clear();
+	void error(Exception ex);
+}

+ 66 - 0
src/main/lombok/org/leumasjaffe/recipe/controller/SwingSaveLoadHandle.java

@@ -0,0 +1,66 @@
+package org.leumasjaffe.recipe.controller;
+
+import java.awt.Component;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.Optional;
+
+import javax.swing.JFileChooser;
+import javax.swing.JOptionPane;
+
+import lombok.AccessLevel;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.experimental.FieldDefaults;
+import lombok.experimental.NonFinal;
+
+@RequiredArgsConstructor
+@FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
+class SwingSaveLoadHandle implements SaveLoadHandle {
+	JFileChooser chooser = new JFileChooser();
+	Component owner;
+	@NonFinal File filename = null;
+	
+	@Override
+	@SuppressWarnings("resource")
+	@SneakyThrows(IOException.class)
+	public Optional<Writer> writer() {
+		if (filename != null) {
+			return Optional.of(new FileWriter(filename));
+		} else if (chooser.showOpenDialog(owner) == JFileChooser.APPROVE_OPTION) {
+			filename = chooser.getSelectedFile();
+			return Optional.of(new FileWriter(filename));
+		} else {
+			return Optional.empty();
+		}
+	}
+	
+	@Override
+	@SuppressWarnings("resource")
+	@SneakyThrows(FileNotFoundException.class)
+	public Optional<Reader> reader() {
+		if (chooser.showOpenDialog(owner) == JFileChooser.APPROVE_OPTION) {
+			filename = chooser.getSelectedFile();
+			return Optional.of(new FileReader(filename));
+		} else {
+			return Optional.empty();
+		}
+	}
+
+	@Override
+	public void error(Exception ex) {
+		JOptionPane.showMessageDialog(owner, ex.getLocalizedMessage(),
+				"File Error", JOptionPane.ERROR_MESSAGE);
+	}
+
+	@Override
+	public void clear() {
+		this.filename = null;
+	}
+
+}

+ 2 - 2
src/main/lombok/org/leumasjaffe/recipe/view/RecipeFrame.java

@@ -21,12 +21,12 @@ import java.awt.event.InputEvent;
 
 @SuppressWarnings("serial")
 @FieldDefaults(level=AccessLevel.PRIVATE, makeFinal=true)
-public class RecipeFrame extends JFrame implements FileController.Model {
+public class RecipeFrame extends JFrame implements FileController.ViewModel {
 	SummaryPanel summaryPanel;
 	JTabbedPane tabbedPane;
 	
 	public RecipeFrame() {
-		FileController<RecipeFrame> fileController = new FileController<>(this);
+		FileController fileController = new FileController(this);
 
 		final int MASK = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
 		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

+ 64 - 29
src/test/java/org/leumasjaffe/recipe/controller/FileControllerTest.java

@@ -3,53 +3,47 @@ package org.leumasjaffe.recipe.controller;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
 
-import java.awt.Component;
+import java.io.IOException;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.io.Writer;
 import java.util.Optional;
 
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.platform.runner.JUnitPlatform;
+import org.junit.runner.RunWith;
 import org.leumasjaffe.recipe.model.Recipe;
 import org.mockito.ArgumentCaptor;
 import org.mockito.InOrder;
+import org.mockito.InjectMocks;
 import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+import org.mockito.junit.jupiter.MockitoExtension;
 
-class FileControllerTest {
-	@SuppressWarnings("serial")
-	private static abstract class StubComponent extends Component implements FileController.Model {}
-	
-	StubComponent stub;
-	FileController<StubComponent> controller;
-	
-	@BeforeEach
-	void setUp() {
-		stub = mock(StubComponent.class);
-		controller = spy(new FileController<>(stub));
-		// Make sure that any errors are directed into the test case's failure trace,
-		// rather than a JOptionDialog
-		doAnswer(inv -> { throw inv.getArgument(0, Exception.class); })
-			.when(controller).errorPopup(any());
-	}
+@ExtendWith(MockitoExtension.class)
+@RunWith(JUnitPlatform.class)
+class FileControllerTest {	
+	@Mock SaveLoadHandle handle;
+	@Mock FileController.ViewModel viewmodel;
+	@InjectMocks FileController controller;
 	
 	@Test
 	void testCanCreateNewModel() {
 		controller.create();
-		verify(stub).setModel(any(Recipe.class));
+		verify(viewmodel).setModel(any(Recipe.class));
 	}
 
 	@Test
 	void testRepeatedCreatesProvideNewObjects() {
-		InOrder inOrder = inOrder(stub);
+		InOrder inOrder = inOrder(viewmodel);
 		final ArgumentCaptor<Recipe> first = ArgumentCaptor.forClass(Recipe.class);
 		final ArgumentCaptor<Recipe> second = ArgumentCaptor.forClass(Recipe.class);
 
 		controller.create();
-		inOrder.verify(stub).setModel(first.capture());
+		inOrder.verify(viewmodel).setModel(first.capture());
 
 		controller.create();
-		inOrder.verify(stub).setModel(second.capture());
+		inOrder.verify(viewmodel).setModel(second.capture());
 		
 		assertNotSame(first.getValue(), second.getValue());
 	}
@@ -57,32 +51,73 @@ class FileControllerTest {
 	@Test
 	void testOpensContentIntoRecipe() {
 		final String data = "{ \"title\": \"Example\" }";
-		doReturn(Optional.of(new StringReader(data))).when(controller).getOpened();
+		doReturn(Optional.of(new StringReader(data))).when(handle).reader();
 
 		controller.open();
 		
-		verify(stub).setModel(argThat(r -> r.getTitle().equals("Example")));
+		verify(viewmodel).setModel(argThat(r -> r.getTitle().equals("Example")));
 	}
 
 	@Test
 	void testEmitsErrorWithMalformedData() {
 		final String data = "{ \"name\": \"Example\" }";
-		doReturn(Optional.of(new StringReader(data))).when(controller).getOpened();
-		doNothing().when(controller).errorPopup(any());
+		doReturn(Optional.of(new StringReader(data))).when(handle).reader();
+		doNothing().when(handle).error(any());
 
 		controller.open();
 		
-		verify(controller).errorPopup(any());
+		verify(handle).error(any());
 	}
 	
 	@Test
 	void testCanWriteRecipeToStream() {
 		StringWriter writer = new StringWriter();
-		doReturn(Optional.of(writer)).when(controller).getSaved();
+		doReturn(Optional.of(writer)).when(handle).writer();
 		
 		controller.create();
 		controller.save();
 		assertNotEquals("", writer.toString());
 	}
+	
+	@Test
+	void testSaveAsClearsHandle() {
+		StringWriter writer = new StringWriter();
+		doReturn(Optional.of(writer)).when(handle).writer();
+		
+		controller.create();
+		controller.saveAs();
+		verify(handle).clear();
+		assertNotEquals("", writer.toString());
+	}
+	
+	@Test
+	void testWriteErrorCallsHandle() {
+		Writer writer = mock(Writer.class, inv -> { throw new IOException(); });
+		
+		doReturn(Optional.of(writer)).when(handle).writer();
+		doNothing().when(handle).error(any());
+		
+		controller.create();
+		controller.save();
+		verify(handle).error(any());
+	}
+	
+	@Test
+	void testDoesNothingIfHandleReturnsMissingReader() {
+		doReturn(Optional.empty()).when(handle).reader();
+		
+		controller.open();
+		verify(viewmodel, never()).setModel(any());
+		verify(handle, never()).error(any());
+	}
+	
+	@Test
+	void testDoesNothingIfHandleReturnsMissingWriter() {
+		doReturn(Optional.empty()).when(handle).writer();
+		
+		controller.create();
+		controller.save();
+		verify(handle, never()).error(any());
+	}
 
 }