├── .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 | ![image](https://user-images.githubusercontent.com/82182/56163536-8c022b00-5f9c-11e9-8c70-5a30f70c7aa3.png) 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 | ![2019-04-15_1605](https://user-images.githubusercontent.com/82182/56143385-72e28580-5f6e-11e9-965f-2b9cc86cfba9.png) 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 | ![image](https://user-images.githubusercontent.com/82182/56144431-58111080-5f70-11e9-81a9-b476b06350ce.png) 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 | ![image](https://user-images.githubusercontent.com/82182/56144982-6dd30580-5f71-11e9-8a4a-3dbb962905b8.png) 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 | ![2019-04-15_1633](https://user-images.githubusercontent.com/82182/56145480-5ea08780-5f72-11e9-817a-bbe7d1a86dcf.png) 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 | ![2019-04-15_1636](https://user-images.githubusercontent.com/82182/56145684-bccd6a80-5f72-11e9-96c7-6488a689509e.png) 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 | [![Test suite running](http://img.youtube.com/vi/kJIYdXIeZm8/0.jpg)](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 | --------------------------------------------------------------------------------