├── .gitignore
├── README.md
├── pom.xml
└── src
├── main
└── java
│ └── demo
│ └── Demo.java
└── test
└── java
└── demo
├── CounterComponentTests.java
├── CounterModelUnitTests.java
├── CoupledCounterPairComponentTests.java
└── WholeAppIntegrationTests.java
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | *.iml
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A demo of MVC in Swing with UI component testing
2 |
3 | 
4 |
5 | ## Run the build like so:
6 |
7 | ```
8 | mvn clean test
9 | ```
10 |
11 | First time through Maven downloads a bunch and makes the final jar, but second and subsequent times through for
12 | me (`mvn test` at least), it is 22 seconds for all UI-clicking automated tests.
13 |
14 | ## Or skip the build and launch the app like so:
15 |
16 | You'll do this to interact with it manually.
17 |
18 | ### Java 11 and above
19 |
20 | ```
21 | java src/main/java/demo/Demo.java
22 | ```
23 |
24 | ### Java version 8 thru 10
25 |
26 | ```
27 | cd src/main/java/
28 | javac demo/Demo.java
29 | java demo.Demo
30 | cd -
31 | ```
32 |
33 | ## View the source of the Demo app
34 |
35 | [src/main/java/demo/Demo.java](src/main/java/demo/Demo.java). It's a single source file.
36 |
37 | That's as close as you can get to pseudo-declarative in plain Java. It'd be more declarative in
38 | JavaFX, TornadoFX (Kotlin) and Swiby (JRuby, discontinued).
39 |
40 | As it is now, it is also pretty close to 1998's original Swing. Package names are different, and
41 | there's one or two helper methods that were not there back then, and also some Java 8 syntactic
42 | sugar that ends up making the same bytecode classes
43 |
44 | ## Architectural criticisms
45 |
46 | ### Declarative tricks
47 |
48 | In order to to have a nested feel inner classes are used as a syntactic
49 | trick:
50 |
51 | ```
52 | add(new JPanel() {{
53 | // components and more containers.
54 | }});
55 |
56 | ```
57 |
58 | Because of a decision in the 90's to keep compatibility with Microsoft'sJava (that was
59 | frozen at JDK 1.0 ater a lawsuit), the bytecode design for inner classes in JDK 1.1 didn't
60 | change. Thus we see a lot of $1, $2 suffixed classes that are ugly to look at if you were
61 | looking at the generated class files, and also come with a file size impediment versus
62 | other choices Sun could have made for the same feature back then.
63 |
64 | ### Views are not overridable in this demo
65 |
66 | Related to that declarative style above, we don't have overridable views. No subclasses
67 | for whatever reason - including testing purposes. Sure, we have overridable models
68 | (Mockito could mock Counter.Model), but not views as we have it here. From a purist
69 | point of view, MVC suggests that Views could be further specialized, and we can do that
70 | through composition, but not inheritance here.
71 |
72 | We also have view's as final fields that are accessible outside the class. Like
73 | `foo.view`. Java's conventions would be for a getter - `foo.getView()`but I skipped
74 | that here to save a few lines. If this were a long-lived mutli-developer solution, I
75 | would put them back in if. Or if they were truly needed for some OO reason.
76 |
77 | # Tests and coverage reports
78 |
79 | Individual test classes:
80 |
81 | 1. CounterComponentTests (6.7 seconds for 10 tests)
82 | 2. CounterModelUnitTests (0.5 second for 5 tests)
83 | 3. CoupledCounterPairComponentTests (3.7 seconds for 2 tests)
84 | 4. WholeAppIntegrationTests (6.7 seconds for 2 tests)
85 |
86 | Times above are for each test class run on their own. That is in the Intellij IDE and the times
87 | would be longer for Maven on the command line as it is less intelligent re unnecessary steps. If
88 | all are run together in one execution, that is 19 tests in 6 seconds. Clearly there's an overhead
89 | for bringing up Marathon and for the 'single test' breakdown above
90 |
91 | ## CounterModel Unit Tests (on their own).
92 |
93 | Coverage for Counter.Model class
94 |
95 | 
96 |
97 | The missed lines are two catch blocks that I can't simulate. Java's checked
98 | exceptions being the reason I can get the coverage to 100%.
99 |
100 | These tests take 1ms each, but the first takes 950ms on my 2017 MacBookAir. There's
101 | no Marathon/WebDriver involved - no UI at all. Counter's View and Controller logic
102 | is not tested.
103 |
104 | ## Counter Component Tests (on their own).
105 |
106 | Coverage for Counter and inner classes:
107 |
108 | 
109 |
110 | The coverage for the model is exactly the same as the Counter Model unit tests above. But
111 | this time is was achieved via Marathon/WebDriver. The initial test was 4.5 seconds, and each
112 | test thereafter was an average 330ms. Frames are opening and closing per test. Speed up
113 | opportunities are (as Web Selenium) not close and reopen the window between tests. Mocking
114 | the model would most likely not provide a speedup.
115 |
116 | ## CoupledCounterPair Component Tests (on their own).
117 |
118 | Coverage for CoupledCounterPair and inner classes:
119 |
120 | 
121 |
122 | 100% of the View and Controller logic is covered. There's no model logic got
123 | this component as it uses the model from Counter (covered above). There's about 5s
124 | spend getting the first test complete, and 450ms the second (of two).
125 |
126 | ## Whole App Integration Tests (on their own)
127 |
128 | Coverage for the whole "Demo" app, and all classes used:
129 |
130 | 
131 |
132 | This is closer to a happy path test. Counter's coverage lowered as there was
133 | no edge-case checking. Five seconds or the first test to complete, the 300ms
134 | for the next.
135 |
136 | ## All Tests together
137 |
138 | 
139 |
140 | Coverage accumulated for all unit, component and happy path (whole app)
141 | tests: 93%. 10.5 seconds for all tests. Here's a YouTube video of that:
142 |
143 | [](https://www.youtube.com/watch?v=kJIYdXIeZm8 "Tests suite running")
144 |
145 | # Marathon
146 |
147 | Marathon came out of a project that ThoughtWorks did for "Dixons" (a large electrical good retailer in the
148 | UK) in 2003/4. The same prooject provided some notes for Jez Humble and Dave Farley's "Continuous Delivery" book.
149 | There were a lot of developers involved and it was six months from first code to
150 | go-live. It was a Java solution (backend and frontend) for point-of-sale equipment.
151 | WinRunner was the state of the art in the industry for UI automation and we knew it
152 | [wasn't that usable in a XP/DevOps way of working](https://paulhammant.com/blog/000245.html).
153 |
154 | [Charles Lowel](https://twitter.com/cowboyd) and
155 | [Jeremy Lightsmith](https://twitter.com/lightsmith)
156 | put "MarathonMan" together and released it as open source. ThoughtWorks didn't substantially
157 | fund it like they did for Selenium soon after. In the end, others took care of the project in
158 | opensource-land: Karra "KD" Dakshinamurthy and colleagues at Jalian Systems Pvt. Ltd (India).
159 | There's a full circle aspect now, because Jalian recently made Marathon Selenium2+ compatible.
160 | See their portal [marathontesting.com/](https://marathontesting.com/) for more info.
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 | 4.0.0
3 |
4 | com.paulhammant
5 | swingcomponenttesting
6 | 1.0-SNAPSHOT
7 | jar
8 |
9 |
10 | 1.8
11 | 1.8
12 | UTF-8
13 |
14 |
15 |
16 |
17 |
18 | com.squareup.okhttp3
19 | okhttp
20 | 3.11.0
21 |
22 |
23 |
24 | junit
25 | junit
26 | 4.12
27 | test
28 |
29 |
30 |
31 | com.jaliansystems
32 | marathon-java-driver
33 | 5.2.5.0
34 | test
35 |
36 |
37 |
38 |
39 |
40 |
41 | org.jacoco
42 | jacoco-maven-plugin
43 | 0.8.3
44 |
45 |
46 |
47 | prepare-agent
48 |
49 |
50 |
51 |
52 | report
53 | test
54 |
55 | report
56 |
57 |
58 |
59 |
60 |
61 | org.apache.maven.plugins
62 | maven-surefire-plugin
63 | 2.22.1
64 |
65 |
66 | *Tests.java
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/src/main/java/demo/Demo.java:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | A single source file for java is unusual, but you
4 | can hand it to Java9+ for a seamless compilation and
5 | launch like so ...
6 |
7 | java Demo.java
8 |
9 | ... and this is just a demo.
10 |
11 | Also we like pseudo-declarative forms :)
12 |
13 | */
14 |
15 | package demo;
16 |
17 | import javax.swing.*;
18 | import javax.swing.text.AttributeSet;
19 | import javax.swing.text.BadLocationException;
20 | import javax.swing.text.PlainDocument;
21 | import java.awt.*;
22 | import java.awt.event.WindowAdapter;
23 | import java.awt.event.WindowEvent;
24 |
25 | public class Demo extends JFrame {
26 |
27 | // See also two named inner classes: CoupledCounterPair, Counter
28 | // (and inner classes of Counter: Counter.View & Counter.Model)
29 | // Also see the main() method half way down this source file.
30 |
31 | // Note: non-final fields, because of the nature of Java's anonymous inner classes.
32 | private JPanel tab1, tab2, tab2Contents;
33 | private JTabbedPane tabbedPane;
34 | private JButton addCounterPairToTab2Button;
35 |
36 | public Demo(Counter.Model sharedCounterModel) {{
37 | // View logic ...
38 | setTitle("Counter MVC demo using Swing");
39 | add(new JTabbedPane() {{
40 | tabbedPane = this;
41 | addTab("Tab 1", new JPanel() {{
42 | tab1 = this;
43 | add(new CoupledCounterPair(sharedCounterModel).view);
44 | add(new JPanel(){{
45 | setBorder(BorderFactory.createTitledBorder("Two Separate Models"));
46 | add(new Counter(new Counter.Model(3)).view);
47 | add(new Counter(new Counter.Model(17)).view);
48 | setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
49 | }});
50 | add(new JButton("Another Counter (shared model) in Tab 2") {{
51 | // store for controller access outside of view logic
52 | addCounterPairToTab2Button = this;
53 | }});
54 | setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
55 | }});
56 | addTab("Tab 2", new JPanel() {{
57 | setName("tab2");
58 | // store for controller access outside of view logic
59 | tab2 = this;
60 | setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
61 | }});
62 |
63 | }});
64 | getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
65 | pack();
66 | setLocationRelativeTo(null);
67 | setVisible(true);
68 |
69 | // controller logic
70 | addCounterPairToTab2Button.addActionListener(e -> addCounterPairToTab2(sharedCounterModel));
71 | addCloseWindowHandler(sharedCounterModel);
72 | }}
73 |
74 | private void removeCounterPairToTab2() {{
75 | addCounterPairToTab2Button.setEnabled(true);
76 | tab2.remove(tab2Contents);
77 | tabbedPane.setSelectedComponent(tab1);
78 | }}
79 |
80 | private void addCounterPairToTab2(final Counter.Model counterModel) {
81 | tab2.add(new JPanel() {{
82 | setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
83 | tab2Contents = this;
84 | add(new CoupledCounterPair(counterModel).view);
85 | add(new JButton("OK I'm done with counter pair in tab 2") {{
86 | addActionListener(e -> removeCounterPairToTab2());
87 | setAlignmentX(Component.LEFT_ALIGNMENT);
88 | setName("back2TabOne");
89 | }});
90 | }});
91 | addCounterPairToTab2Button.setEnabled(false);
92 | tabbedPane.setSelectedComponent(tab2);
93 | }
94 |
95 | private void addCloseWindowHandler(final Counter.Model counterModel) {
96 | addWindowListener(new WindowAdapter() {
97 | @Override
98 | public void windowClosing(WindowEvent e)
99 | {
100 | e.getWindow().dispose();
101 | System.out.println("Frame closed, views/controllers eligible for GC, model value = "
102 | + counterModel.getCount() + ". Now exit."); ;
103 | }
104 | });
105 | }
106 |
107 | public static void main(String[] args) {
108 | new Demo(new Counter.Model(0));
109 | }
110 |
111 | public static class CoupledCounterPair {
112 |
113 | final View view;
114 |
115 | public CoupledCounterPair(Counter.Model model) {
116 | view = new View(model);
117 | }
118 |
119 | static class View extends JPanel {
120 | View(Counter.Model counterModel) {{
121 | setBorder(BorderFactory.createTitledBorder("Single Shared Model"));
122 | add(new Counter(counterModel).view);
123 | add(new Counter(counterModel).view);
124 | setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
125 | }}
126 | }
127 | }
128 |
129 | public static class Counter {
130 |
131 | final View view;
132 |
133 | Counter(Model model) {
134 | view = new View(model);
135 | // controller logic
136 | view.plus.addActionListener(e -> {
137 | model.increment();
138 | });
139 | view.minus.addActionListener(e -> {
140 | model.decrement();
141 | });
142 | }
143 |
144 | static class View extends JPanel {
145 | final JTextField counterField;
146 | final JButton plus;
147 | final JButton minus;
148 |
149 | View(Model model) {{
150 | add(new JLabel("Count:"));
151 | counterField = new JTextField("0", 4);
152 | counterField.setDocument(model);
153 | add(counterField);
154 | plus = new JButton("+");
155 | final int s = (int) (getFont().getSize() * 1.7);
156 | plus.setPreferredSize(new Dimension(s, s));
157 | add(plus);
158 | minus = new JButton("-");
159 | minus.setPreferredSize(new Dimension(s, s));
160 | add(minus);
161 | }}
162 | }
163 |
164 | public static class Model extends PlainDocument {
165 | Model(int initialValue) {
166 | setCount(initialValue);
167 | }
168 |
169 | void increment() {
170 | setCount(getCount() + 1);
171 | }
172 |
173 | void decrement() {
174 | setCount(getCount() - 1);
175 | }
176 |
177 | private void setCount(int i) {
178 | try {
179 | super.remove(0, getLength());
180 | super.insertString(0, "" + i, null);
181 | } catch (BadLocationException e) {
182 | throw new UnsupportedOperationException(e);
183 | }
184 | }
185 |
186 | int getCount() {
187 | try {
188 | return Integer.parseInt(super.getText(0, super.getLength()));
189 | } catch (BadLocationException e) {
190 | throw new UnsupportedOperationException(e);
191 | }
192 | }
193 |
194 | public void insertString(int offset, String str, AttributeSet attr) throws BadLocationException {
195 | for (int i = 0; i < str.length(); i++) {
196 | final char c = str.charAt(i);
197 | if (!"-0123456789".contains(String.valueOf(c))) {
198 | return;
199 | } else if (offset != 0 && c == '-') {
200 | return;
201 | }
202 | }
203 | super.insertString(offset, str, attr);
204 | }
205 | }
206 | }
207 | }
--------------------------------------------------------------------------------
/src/test/java/demo/CounterComponentTests.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import org.junit.After;
4 | import org.junit.Before;
5 | import org.junit.Test;
6 | import org.openqa.selenium.WebDriver;
7 | import org.openqa.selenium.WebElement;
8 |
9 | import java.awt.*;
10 | import java.util.List;
11 |
12 | import static demo.CoupledCounterPairComponentTests.makeAndShowTestHarnessFor;
13 | import static demo.CoupledCounterPairComponentTests.setupMarathon;
14 | import static demo.CoupledCounterPairComponentTests.tearDownMarathonAndCloseWindow;
15 | import static demo.WholeAppIntegrationTests.byReflectionFieldName;
16 | import static org.junit.Assert.assertEquals;
17 |
18 | public class CounterComponentTests {
19 |
20 | private Window window;
21 | private WebDriver driver;
22 |
23 | @Before
24 | public void setUp() {
25 | driver = setupMarathon();
26 | }
27 |
28 | @After
29 | public void tearDown() throws Exception {
30 | tearDownMarathonAndCloseWindow(window, driver);
31 | }
32 |
33 | @Test
34 | public void counterCanHaveInitialValue() {
35 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(4321);
36 | window = makeAndShowTestHarnessFor(new Demo.Counter(counterModel).view);
37 | WebElement ctrTxtField = driver.findElement(byReflectionFieldName("counterField", "text-field"));
38 | assertEquals("4321", ctrTxtField.getText());
39 | assertEquals(4321, counterModel.getCount());
40 | }
41 |
42 | @Test
43 | public void counterCanBeSetThroughTheView() {
44 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
45 | window = makeAndShowTestHarnessFor(new Demo.Counter(counterModel).view);
46 | WebElement ctrTxtField = driver.findElement(byReflectionFieldName("counterField", "text-field"));
47 | ctrTxtField.sendKeys("5678");
48 | assertEquals("5678", ctrTxtField.getText());
49 | assertEquals(5678, counterModel.getCount());
50 |
51 | }
52 |
53 | @Test
54 | public void counterWontAllowNonNumericCharsThroughTheView() {
55 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
56 | window = makeAndShowTestHarnessFor(new Demo.Counter(counterModel).view);
57 | WebElement ctrTxtField = driver.findElement(byReflectionFieldName("counterField", "text-field"));
58 | ctrTxtField.sendKeys("a-1b2c3d");
59 | assertEquals("-123", ctrTxtField.getText());
60 | assertEquals(-123, counterModel.getCount());
61 | }
62 |
63 | @Test
64 | public void counterWontAllowAMinusCharAfterInitialPositionThroughTheView() {
65 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
66 | window = makeAndShowTestHarnessFor(new Demo.Counter(counterModel).view);
67 | WebElement ctrTxtField = driver.findElement(byReflectionFieldName("counterField", "text-field"));
68 | ctrTxtField.sendKeys("2-2");
69 | assertEquals(22, counterModel.getCount());
70 | }
71 |
72 | @Test
73 | public void counterViewUpdatesAfterModelChange() {
74 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
75 | window = makeAndShowTestHarnessFor(new Demo.Counter(counterModel).view);
76 | counterModel.increment();
77 | counterModel.increment();
78 | WebElement ctrTxtField = driver.findElement(byReflectionFieldName("counterField", "text-field"));
79 | assertEquals("2", ctrTxtField.getText());
80 | assertEquals(2, counterModel.getCount());
81 | }
82 |
83 | @Test
84 | public void counterCanBeIncremented() {
85 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
86 | window = makeAndShowTestHarnessFor(new Demo.Counter(counterModel).view);
87 | WebElement plusButton = driver.findElement(byReflectionFieldName("plus", "button"));
88 | plusButton.click();
89 | assertEquals("1", driver.findElement(byReflectionFieldName("counterField", "text-field")).getText());
90 | assertEquals(1, counterModel.getCount());
91 | plusButton.click();
92 | assertEquals("2", driver.findElement(byReflectionFieldName("counterField", "text-field")).getText());
93 | assertEquals(2, counterModel.getCount());
94 | }
95 |
96 | @Test
97 | public void counterCanBeDecremented() {
98 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
99 | window = makeAndShowTestHarnessFor(new Demo.Counter(counterModel).view);
100 | WebElement minusButton = driver.findElement(byReflectionFieldName("minus", "button"));
101 | minusButton.click();
102 | assertEquals("-1", driver.findElement(byReflectionFieldName("counterField", "text-field")).getText());
103 | assertEquals(-1, counterModel.getCount());
104 | minusButton.click();
105 | assertEquals("-2", driver.findElement(byReflectionFieldName("counterField", "text-field")).getText());
106 | assertEquals(-2, counterModel.getCount());
107 | }
108 |
109 | @Test
110 | public void twoCountersCanHaveTheSameModel() {
111 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
112 | window = makeAndShowTestHarnessFor(
113 | new Demo.Counter(counterModel).view,
114 | new Demo.Counter(counterModel).view);
115 | WebElement plusButton = driver.findElement(byReflectionFieldName("plus", "button"));
116 | plusButton.click();
117 | final List counterFields = driver.findElements(byReflectionFieldName("counterField", "text-field"));
118 | assertEquals(2, counterFields.size());
119 | assertEquals("1", counterFields.get(0).getText());
120 | assertEquals("1", counterFields.get(1).getText());
121 | assertEquals(1, counterModel.getCount());
122 | }
123 |
124 | @Test
125 | public void twoCountersCanHaveTheDifferentModels() {
126 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(44);
127 | window = makeAndShowTestHarnessFor(
128 | new Demo.Counter(counterModel).view,
129 | new Demo.Counter(new Demo.Counter.Model(999)).view);
130 | WebElement plusButton = driver.findElement(byReflectionFieldName("plus", "button"));
131 | plusButton.click();
132 | final List counterFields = driver.findElements(byReflectionFieldName("counterField", "text-field"));
133 | assertEquals(2, counterFields.size());
134 | assertEquals("45", counterFields.get(0).getText());
135 | assertEquals("999", counterFields.get(1).getText());
136 | assertEquals(45, counterModel.getCount());
137 | }
138 |
139 | @Test
140 | public void threeCountersCanHaveTheSameModel() {
141 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
142 | window = makeAndShowTestHarnessFor(
143 | new Demo.Counter(counterModel).view,
144 | new Demo.Counter(counterModel).view,
145 | new Demo.Counter(counterModel).view);
146 | WebElement plusButton = driver.findElement(byReflectionFieldName("plus", "button"));
147 | plusButton.click();
148 | plusButton.click();
149 | final List counterFields = driver.findElements(byReflectionFieldName("counterField", "text-field"));
150 | assertEquals(3, counterFields.size());
151 | assertEquals("2", counterFields.get(0).getText());
152 | assertEquals("2", counterFields.get(1).getText());
153 | assertEquals("2", counterFields.get(2).getText());
154 | assertEquals(2, counterModel.getCount());
155 | }
156 |
157 | }
158 |
--------------------------------------------------------------------------------
/src/test/java/demo/CounterModelUnitTests.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import org.junit.Test;
4 |
5 | import javax.swing.text.BadLocationException;
6 |
7 | import static org.junit.Assert.assertEquals;
8 |
9 | public class CounterModelUnitTests {
10 |
11 | @Test
12 | public void modelCanHoldValue() {
13 | Demo.Counter.Model model = new Demo.Counter.Model(14);
14 | assertEquals(14, model.getCount());
15 | }
16 |
17 | @Test
18 | public void modelCanTakeANewValue() throws BadLocationException {
19 | Demo.Counter.Model model = new Demo.Counter.Model(444);
20 | model.insertString(1,"0", null);
21 | assertEquals(4044, model.getCount());
22 | }
23 |
24 | @Test
25 | public void modelCantTakeNonNumericValues() throws BadLocationException {
26 | Demo.Counter.Model model = new Demo.Counter.Model(555);
27 | model.insertString(1,"a", null);
28 | assertEquals(555, model.getCount());
29 | model.insertString(1,"-", null);
30 | assertEquals(555, model.getCount());
31 | model.insertString(0,"-", null);
32 | assertEquals(-555, model.getCount());
33 | }
34 |
35 | @Test
36 | public void modelCanBeIncremeneted() {
37 | Demo.Counter.Model model = new Demo.Counter.Model(24);
38 | model.increment();
39 | assertEquals(25, model.getCount());
40 | }
41 |
42 | @Test
43 | public void modelCanBeDecremeneted() {
44 | Demo.Counter.Model model = new Demo.Counter.Model(34);
45 | model.decrement();
46 | assertEquals(33, model.getCount());
47 | }
48 |
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/src/test/java/demo/CoupledCounterPairComponentTests.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import net.sourceforge.marathon.javadriver.JavaDriver;
4 | import net.sourceforge.marathon.javadriver.JavaProfile;
5 | import net.sourceforge.marathon.javadriver.JavaProfile.LaunchMode;
6 | import net.sourceforge.marathon.javadriver.JavaProfile.LaunchType;
7 | import org.junit.After;
8 | import org.junit.Before;
9 | import org.junit.Test;
10 | import org.openqa.selenium.WebDriver;
11 | import org.openqa.selenium.WebElement;
12 |
13 | import javax.swing.*;
14 | import java.awt.*;
15 | import java.lang.reflect.InvocationTargetException;
16 | import java.util.List;
17 |
18 | import static demo.WholeAppIntegrationTests.byReflectionFieldName;
19 | import static org.junit.Assert.assertEquals;
20 |
21 | public class CoupledCounterPairComponentTests {
22 |
23 | private Window window;
24 | private WebDriver driver;
25 |
26 | @Before
27 | public void setUp() {
28 | driver = setupMarathon();
29 | }
30 |
31 | static WebDriver setupMarathon() {
32 | JavaProfile profile = new JavaProfile(LaunchMode.EMBEDDED);
33 | profile.setLaunchType(LaunchType.SWING_APPLICATION);
34 | return new JavaDriver(profile);
35 | }
36 |
37 | @After
38 | public void tearDown() throws Exception {
39 | tearDownMarathonAndCloseWindow(window, driver);
40 | }
41 |
42 | static void tearDownMarathonAndCloseWindow(Window window, WebDriver driver) throws InterruptedException, InvocationTargetException {
43 | if (window != null)
44 | SwingUtilities.invokeAndWait(window::dispose);
45 | if (driver != null)
46 | driver.quit();
47 | }
48 |
49 | @Test
50 | public void counterPairCanHaveInitialValue() {
51 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(4321);
52 | window = makeAndShowTestHarnessFor(new Demo.CoupledCounterPair(counterModel).view);
53 | List ctrTxtField = driver.findElements(byReflectionFieldName("counterField", "text-field"));
54 | assertEquals("4321", ctrTxtField.get(0).getText());
55 | assertEquals("4321", ctrTxtField.get(1).getText());
56 | assertEquals(4321, counterModel.getCount());
57 | }
58 |
59 | @Test
60 | public void counterPairCanBeSetThroughTheView() {
61 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
62 | window = makeAndShowTestHarnessFor(new Demo.CoupledCounterPair(counterModel).view);
63 | List ctrTxtField = driver.findElements(byReflectionFieldName("counterField", "text-field"));
64 | ctrTxtField.get(0).sendKeys("5678");
65 | assertEquals("5678", ctrTxtField.get(1).getText());
66 | assertEquals(5678, counterModel.getCount());
67 | ctrTxtField.get(1).sendKeys("2345");
68 | assertEquals("2345", ctrTxtField.get(0).getText());
69 | }
70 |
71 | static Window makeAndShowTestHarnessFor(final JComponent... views) {
72 | JFrame f = new JFrame() {{
73 | getContentPane().add(new JPanel() {{
74 | for (JComponent view : views) {
75 | add(view);
76 | }
77 | setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
78 | }});
79 | pack();
80 | setLocationRelativeTo(null);
81 | }};
82 | SwingUtilities.invokeLater(() -> f.setVisible(true));
83 | return f;
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/src/test/java/demo/WholeAppIntegrationTests.java:
--------------------------------------------------------------------------------
1 | package demo;
2 |
3 | import org.junit.After;
4 | import org.junit.Before;
5 | import org.junit.Test;
6 | import org.openqa.selenium.By;
7 | import org.openqa.selenium.WebDriver;
8 | import org.openqa.selenium.WebElement;
9 |
10 | import javax.swing.*;
11 | import java.awt.*;
12 |
13 | import static demo.CoupledCounterPairComponentTests.setupMarathon;
14 | import static demo.CoupledCounterPairComponentTests.tearDownMarathonAndCloseWindow;
15 | import static junit.framework.TestCase.assertTrue;
16 | import static org.junit.Assert.assertEquals;
17 | import static org.junit.Assert.assertFalse;
18 |
19 | public class WholeAppIntegrationTests {
20 |
21 | private Window window;
22 | private WebDriver driver;
23 |
24 | @Before
25 | public void setUp() {
26 | driver = setupMarathon();
27 | }
28 |
29 | @After
30 | public void tearDown() throws Exception {
31 | tearDownMarathonAndCloseWindow(window, driver);
32 | }
33 |
34 | @Test
35 | public void basicCounterTestInWholeApp() {
36 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
37 | window = new Demo(counterModel);
38 | WebElement ctrTxtField = driver.findElement(byReflectionFieldName("counterField", "text-field"));
39 | ctrTxtField.sendKeys("444");
40 | assertEquals("444", ctrTxtField.getText());
41 | assertEquals(444, counterModel.getCount());
42 | }
43 |
44 | @Test
45 | public void clickToSecondTabAndBackWorks() throws InterruptedException {
46 | final Demo.Counter.Model counterModel = new Demo.Counter.Model(0);
47 | window = new Demo(counterModel);
48 | SwingUtilities.invokeLater(() -> window.setVisible(true));
49 | WebElement secondTabButton = driver.findElements(By.tagName("button")).get(8);
50 | secondTabButton.click();
51 | WebElement tab2 = driver.findElement(By.name("tab2"));
52 | WebElement ctrTxtField = tab2.findElement(byReflectionFieldName("counterField", "text-field"));
53 | assertTrue(ctrTxtField.isDisplayed());
54 | ctrTxtField.sendKeys("765");
55 | assertEquals(765, counterModel.getCount());
56 | tab2.findElement(By.name("back2TabOne")).click();
57 | assertFalse(ctrTxtField.isDisplayed());
58 | WebElement tab1CtrTxtField = driver.findElement(byReflectionFieldName("counterField", "text-field"));
59 | assertEquals("765", tab1CtrTxtField.getText());
60 | }
61 |
62 |
63 | static By byReflectionFieldName(String fName, final String type) {
64 | return By.cssSelector(type + "[fieldName='" + fName + "']");
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------