source: josm/trunk/test/unit/org/openstreetmap/josm/testutils/mockers/JOptionPaneSimpleMocker.java@ 14052

Last change on this file since 14052 was 14052, checked in by Don-vip, 6 years ago

see #16010 - use JMockit to enable more extensive test coverage (patch by ris, modified)

see https://github.com/openstreetmap/josm/pull/24/commits for details

  • Property svn:eol-style set to native
File size: 12.4 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.testutils.mockers;
3
4import static org.junit.Assert.fail;
5
6import java.awt.Component;
7import java.util.Arrays;
8import java.util.Map;
9import java.util.WeakHashMap;
10
11import javax.swing.Icon;
12import javax.swing.JOptionPane;
13
14import org.openstreetmap.josm.gui.ConditionalOptionPaneUtil.MessagePanel;
15import org.openstreetmap.josm.tools.Logging;
16
17import com.google.common.collect.ImmutableMap;
18import com.google.common.primitives.Ints;
19
20import mockit.Invocation;
21import mockit.Mock;
22import mockit.MockUp;
23
24/**
25 * MockUp for {@link JOptionPane} allowing a test to pre-seed uses of {@link JOptionPane}'s
26 * {@code showInputDialog(...)}, {@code showMessageDialog(...)} and {@code showConfirmDialog(...)}
27 * with mock "responses". This works best with calls which use simple string-based {@code message}
28 * parameters. In such a case, responses can be defined through a mapping from content {@link String}s
29 * to button integer codes ({@link Integer}s) in the case of {@code showConfirmDialog(...)} calls or
30 * arbitrary Objects ( but probably {@link String}s) in the case of {@code showInputDialog(...)} calls.
31 * {@code showMessageDialog(...)} calls' contents should be mapped to {@link JOptionPane#OK_OPTION}.
32 * Example:
33 *
34 * <pre>
35 * new JOptionPaneSimpleMocker(ImmutableMap.of(
36 * "Number of tags to delete", "17", // a showInputDialog(...) call
37 * "Please select the row to edit.", JOptionPane.OK_OPTION, // a showMessageDialog(...) call
38 * "Do you want to save foo bar?", JOptionPane.CANCEL_OPTION // a showConfirmDialog(...) call
39 * ));
40 * </pre>
41 *
42 * Testing examples with more complicated contents would require overriding
43 * {@link #getStringFromMessage(Object)} or even {@link #getMockResultForMessage(Object)} with custom logic.
44 * The class is implemented as a number of small methods with the main aim being to allow overriding of
45 * only the parts necessary for a particular case.
46 *
47 * The default {@link #getMockResultForMessage(Object)} will raise an
48 * {@link junit.framework.AssertionFailedError} on an activation without a matching mapping entry or if
49 * the mapped result value is invalid for the call.
50 *
51 * The public {@link #getMockResultMap()} method returns the modifiable result map to allow for situations
52 * where the desired result might need to be changed mid-test.
53 *
54 * This class should also work with dialogs shown using
55 * {@link org.openstreetmap.josm.gui.ConditionalOptionPaneUtil}.
56 *
57 * NOTE that this class does NOT handle {@code showOptionDialog(...)} calls or direct {@link JOptionPane}
58 * instantiations. These are probably too flexible to be universally mocked with a "Simple" interface and
59 * are probably best handled with case-specific mockers.
60 */
61public class JOptionPaneSimpleMocker extends BaseDialogMockUp<JOptionPane> {
62 protected static final Map<Integer, int[]> optionTypePermittedResults = ImmutableMap.of(
63 JOptionPane.YES_NO_OPTION, new int[] {
64 JOptionPane.YES_OPTION,
65 JOptionPane.NO_OPTION,
66 JOptionPane.CLOSED_OPTION
67 },
68 JOptionPane.YES_NO_CANCEL_OPTION, new int[] {
69 JOptionPane.YES_OPTION,
70 JOptionPane.NO_OPTION,
71 JOptionPane.CANCEL_OPTION,
72 JOptionPane.CLOSED_OPTION
73 },
74 JOptionPane.OK_CANCEL_OPTION, new int[] {
75 JOptionPane.OK_OPTION,
76 JOptionPane.CANCEL_OPTION,
77 JOptionPane.CLOSED_OPTION
78 }
79 );
80
81 protected final MessagePanelMocker messagePanelMocker;
82
83 /**
84 * Construct a {@link JOptionPaneSimpleMocker} with an empty {@link #mockResultMap}.
85 */
86 public JOptionPaneSimpleMocker() {
87 this(null);
88 }
89
90 /**
91 * Construct an {@link JOptionPaneSimpleMocker} with the provided {@link #mockResultMap} and a
92 * default {@link MessagePanelMocker}.
93 * @param mockResultMap mapping of {@link JOptionPaneSimpleMocker} {@code message} string to
94 * result Object.
95 */
96 public JOptionPaneSimpleMocker(
97 final Map<String, Object> mockResultMap
98 ) {
99 this(mockResultMap, null);
100 }
101
102 /**
103 * Construct an {@link JOptionPaneSimpleMocker} with the provided {@link #mockResultMap} and the
104 * provided {@link MessagePanelMocker} instance.
105 * @param mockResultMap mapping of {@link JOptionPaneSimpleMocker} {@code message} string to
106 * result Object.
107 * @param messagePanelMocker {@link MessagePanelMocker} instace to use for {@link org.openstreetmap.josm.gui.ConditionalOptionPaneUtil}
108 * message-string retrieval.
109 */
110 public JOptionPaneSimpleMocker(
111 final Map<String, Object> mockResultMap,
112 final MessagePanelMocker messagePanelMocker
113 ) {
114 super(mockResultMap);
115 this.messagePanelMocker = messagePanelMocker != null ? messagePanelMocker : new MessagePanelMocker();
116 }
117
118 protected String getStringFromOriginalMessage(final Object originalMessage) {
119 return originalMessage.toString();
120 }
121
122 protected String getStringFromMessage(final Object message) {
123 final Object originalMessage = message instanceof MessagePanel ?
124 this.messagePanelMocker.getOriginalMessage((MessagePanel) message) : message;
125 return this.getStringFromOriginalMessage(originalMessage);
126 }
127
128 protected Object getMockResultForMessage(final Object message) {
129 final String messageString = this.getStringFromMessage(message);
130 if (!this.getMockResultMap().containsKey(messageString)) {
131 fail("Unexpected JOptionPane message string: " + messageString);
132 }
133 return this.getMockResultMap().get(messageString);
134 }
135
136 protected Object[] getInvocationLogEntry(
137 final Object message,
138 final String title,
139 final Integer optionType,
140 final Integer messageType,
141 final Icon icon,
142 final Object[] selectionValues,
143 final Object initialSelectionValue,
144 final Object mockResult
145 ) {
146 return new Object[] {
147 mockResult,
148 this.getStringFromMessage(message),
149 title
150 };
151 }
152
153 @Mock
154 protected Object showInputDialog(
155 final Component parentComponent,
156 final Object message,
157 final String title,
158 final int messageType,
159 final Icon icon,
160 final Object[] selectionValues,
161 final Object initialSelectionValue
162 ) {
163 try {
164 final Object result = this.getMockResultForMessage(message);
165 if (selectionValues == null) {
166 if (!(result instanceof String)) {
167 fail(String.format(
168 "Only valid result type for showInputDialog with null selectionValues is String: received %s",
169 result
170 ));
171 }
172 } else {
173 if (!Arrays.asList(selectionValues).contains(result)) {
174 fail(String.format(
175 "Result for showInputDialog not present in selectionValues: %s",
176 result
177 ));
178 }
179 }
180
181 Logging.info(
182 "{0} answering {1} to showInputDialog with message {2}",
183 this.getClass().getName(),
184 result,
185 this.getStringFromMessage(message)
186 );
187
188 this.getInvocationLogInternal().add(this.getInvocationLogEntry(
189 message,
190 title,
191 null,
192 messageType,
193 icon,
194 selectionValues,
195 initialSelectionValue,
196 result
197 ));
198
199 return result;
200 } catch (AssertionError e) {
201 // in case this exception gets ignored by the calling thread we want to signify this failure
202 // in the invocation log. it's hard to know what to add to the log in these cases as it's
203 // probably unsafe to call getInvocationLogEntry, so add the exception on its own.
204 this.getInvocationLogInternal().add(new Object[] {e});
205 throw e;
206 }
207 }
208
209 @Mock
210 protected void showMessageDialog(
211 final Component parentComponent,
212 final Object message,
213 final String title,
214 final int messageType,
215 final Icon icon
216 ) {
217 try {
218 // why look up a "result" for a message dialog which can only have one possible result? it's
219 // a good opportunity to assert its contents
220 final Object result = this.getMockResultForMessage(message);
221 if (!(result instanceof Integer && (int) result == JOptionPane.OK_OPTION)) {
222 fail(String.format(
223 "Only valid result for showMessageDialog is %d: received %s",
224 JOptionPane.OK_OPTION,
225 result
226 ));
227 }
228
229 Logging.info(
230 "{0} answering {1} to showMessageDialog with message {2}",
231 this.getClass().getName(),
232 result,
233 this.getStringFromMessage(message)
234 );
235
236 this.getInvocationLogInternal().add(this.getInvocationLogEntry(
237 message,
238 title,
239 null,
240 messageType,
241 icon,
242 null,
243 null,
244 JOptionPane.OK_OPTION
245 ));
246 } catch (AssertionError e) {
247 // in case this exception gets ignored by the calling thread we want to signify this failure
248 // in the invocation log. it's hard to know what to add to the log in these cases as it's
249 // probably unsafe to call getInvocationLogEntry, so add the exception on its own.
250 this.getInvocationLogInternal().add(new Object[] {e});
251 throw e;
252 }
253 }
254
255 @Mock
256 protected int showConfirmDialog(
257 final Component parentComponent,
258 final Object message,
259 final String title,
260 final int optionType,
261 final int messageType,
262 final Icon icon
263 ) {
264 try {
265 final Object result = this.getMockResultForMessage(message);
266 if (!(result instanceof Integer && Ints.contains(optionTypePermittedResults.get(optionType), (int) result))) {
267 fail(String.format(
268 "Invalid result for showConfirmDialog with optionType %d: %s",
269 optionType,
270 result
271 ));
272 }
273
274 Logging.info(
275 "{0} answering {1} to showConfirmDialog with message {2}",
276 this.getClass().getName(),
277 result,
278 this.getStringFromMessage(message)
279 );
280
281 this.getInvocationLogInternal().add(this.getInvocationLogEntry(
282 message,
283 title,
284 optionType,
285 messageType,
286 icon,
287 null,
288 null,
289 result
290 ));
291
292 return (int) result;
293 } catch (AssertionError e) {
294 // in case this exception gets ignored by the calling thread we want to signify this failure
295 // in the invocation log. it's hard to know what to add to the log in these cases as it's
296 // probably unsafe to call getInvocationLogEntry, so add the exception on its own.
297 this.getInvocationLogInternal().add(new Object[] {e});
298 throw e;
299 }
300 }
301
302 /**
303 * MockUp for {@link MessagePanel} to allow mocking to work with ConditionalOptionPaneUtil dialogs
304 */
305 public static class MessagePanelMocker extends MockUp<MessagePanel> {
306 protected final Map<MessagePanel, Object> originalMessageMemo = new WeakHashMap<>();
307
308 @Mock
309 private void $init(
310 final Invocation invocation,
311 final Object message,
312 final boolean displayImmediateOption
313 ) {
314 this.originalMessageMemo.put(
315 (MessagePanel) invocation.getInvokedInstance(),
316 message
317 );
318 invocation.proceed();
319 }
320
321 /**
322 * Returns the original message.
323 * @param instance message panel
324 * @return the original message
325 */
326 public Object getOriginalMessage(final MessagePanel instance) {
327 return this.originalMessageMemo.get(instance);
328 }
329
330 /* TODO also allow mocking of getNotShowAgain() */
331 }
332}
Note: See TracBrowser for help on using the repository browser.