├── .gitignore ├── .gitpod.yml ├── .idea └── .gitignore ├── LICENSE ├── README.md ├── graphics ├── EyalAvatar.png ├── NikolayAndMia.JPG ├── best-practices-java.jpeg ├── chris.jpg └── large_Sauce_Bkpk_2021.png └── workshop ├── docs ├── ATOMIC-TESTS.MD ├── CONCLUSIONS.MD ├── E2E-TESTS.MD ├── PARALLEL.MD ├── TEST-STRATEGY.MD └── VISUAL.MD ├── images ├── run-time.jpg └── run-time2.jpg ├── pom.xml └── src └── test └── java └── com └── saucedemo ├── exercises ├── E2ETests.java ├── SanityTest.java ├── VisualDataDrivenTests.java └── VisualTests.java └── solution ├── AbstractTestBase.java ├── E2ESolutionTests.java ├── VisualDataDrivenSolutionTests.java ├── VisualSolutionTests.java └── pages ├── AbstractBasePage.java ├── CheckoutCompletePage.java ├── CheckoutOverviewPage.java ├── CheckoutStepOnePage.java ├── LoginPage.java ├── ProductsPage.java └── ShoppingCartPage.java /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | 25 | *.idea 26 | target 27 | /workshop/workshop.iml 28 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - before: | 3 | cd workshop 4 | init: | 5 | mvn clean install -DskipTests=true 6 | clear 7 | command: echo 'Ready to test!' 8 | 9 | vscode: 10 | extensions: 11 | - vscjava.vscode-java-test 12 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices-java/e8ab9d344e284f9ddc3920ab224c704e096b2f24/.idea/.gitignore -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 saucelabs-training 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Automation best practices w/ Java workshop 2 | 3 | testing-good 4 | 5 | [#testing4good](https://twitter.com/hashtag/Testing4Good) 6 | 7 | In this automation best practices workshop you will learn the latest and greatest tools and techniques to drastically improve your testing! 8 | 9 | We will focus on a holistic approach of risk mitigation by doing: 10 | 11 | * functional web testing, 12 | * visual testing, 13 | * accessibility testing, 14 | * and many other things in between 😁 15 | 16 | [👉**Register for workshop**](https://info.saucelabs.com/testing-for-good-workshop-java-113021.html) 17 | 18 | [Join Slack](https://join.slack.com/t/testingforgood/shared_invite/zt-zc64x3pc-9ebUXVeXW1fB0JxU1R_9ew) 19 | 20 | **This workshop serves 2 purposes** 21 | 22 | 1. For me to give back to the testing world and help us all upskill 🚀 23 | 2. For us all to help a greater cause than ourselves 🌍 24 | 25 | ### [ABOUT CHARITY](https://code.org/) 26 | 27 | Code.org® is a nonprofit dedicated to expanding access to computer science in schools and increasing participation by young women and students from other underrepresented groups. Their vision is that every student in every school has the opportunity to learn computer science as part of their core K-12 education. The leading provider of K-12 computer science curriculum in the largest school districts in the United States, Code.org also created the annual Hour of Code campaign, which has engaged more than 15% of all students in the world. 28 | 29 | **Working together, we can reduce the digital divide!** 30 | 31 | With the Testing for Good event, we're helping to give every student the opportunity to learn computer science — online and in schools where Code.org will establish permanent courses and train teachers. For every dollar you donate, one child will be introduced to computer science. 32 | 33 | To make your donations go even further, Sauce Labs will match up to $2,500. 34 | 35 | 👇👇👇 36 | 37 | [Please donate whatever you feel appropriate.](https://www.gofundme.com/f/testing-for-good-codeorg) 100% of the donations go to the cause. 38 | 39 | ## 🧠You will learn to 40 | 41 | * Create a framework for doing comprehensive web testing 42 | * Use industry-standard best practices 43 | * Create functional browser tests using Selenium 44 | * Code visual e2e tests using Screener 45 | * Run in massive parallel (100s of tests in < 5 min) 46 | * Automatically get robust test reports with logs + videos 47 | 48 | ## 🔧Technologies you will use 49 | 50 | 1. Sauce Labs 51 | 2. Selenium 52 | 3. Sauce Visual 53 | 4. Github Actions 54 | 5. Java 55 | 6. Maven 56 | 57 | ## Table Of Contents 58 | 59 | * Introduction to workshop 60 | * [Local environment setup](#local-environment-setup) 61 | * [If you can't setup local, then use Gitpod](#gitpod-setup) 62 | * [E2E browser tests](./workshop/docs/E2E-TESTS.MD) 63 | * [Atomic tests](./workshop/docs/ATOMIC-TESTS.MD) 64 | * [Visual e2e tests](./workshop/docs/VISUAL.MD) 65 | * [Parallelization](./workshop/docs/PARALLEL.MD) 66 | * [Conclusions](./workshop/docs/CONCLUSIONS.MD) 67 | 68 | 69 | ## Requirements 70 | 71 | **This is NOT a beginners course and you will not learn Java testing fundamentals here. However, you will learn a number of amazing skills, techniques, and tools to help you test web applications** 72 | 73 | * At least 1 year of Java programming 74 | * Deep understanding of Selenium WebDriver 75 | * Deep understanding of OOP 76 | * Java 8 installed 77 | * Java IDE installed 78 | * [Git](https://git-scm.com/downloads) 79 | * [Maven installed](https://maven.apache.org/install.html) 80 | 81 | 82 | 83 | ## Your Instructor: Nikolay Advolodkin 84 | 85 | me 86 | 87 | - 🔭 I’m the founder of [Ultimate QA](https://ultimateqa.com/) 88 | - 🏢 I’m a Sr Solutions Architect at Sauce Labs 89 | - 🌱 I’m currently working on [Sauce Bindings](https://github.com/saucelabs/sauce_bindings) 90 | - 💬 Ask me about environmentalism, veganism, test automation, and fitness 91 | - 😄 Pronouns: he/him 92 | - ⚡ Fun fact: I'm a vegan that's super pasionate about saving the planet, saving animals, and helping underpriveleged communities 93 | - 📫 Follow me for testing and dev training 94 | - [Java Testing Newsletter](https://ultimateqa.ck.page/selenium-java-tips 95 | ) 96 | - [Youtube](https://youtube.com/ultimateqa) 97 | - [LinkedIn](https://www.linkedin.com/in/nikolayadvolodkin/) 98 | - [Twitter](https://twitter.com/intent/follow?screen_name=nikolay_a00®ion=follow_link) 99 | 100 | ## Your TAs 101 | 102 | ### Eyal Yovel 103 | 104 | eyal 105 | 106 | ### Chris Eccleston 107 | 108 | chris 109 | 110 | [💻Join Slack #help-desk for tech support](https://join.slack.com/t/testingforgood/shared_invite/zt-zc64x3pc-9ebUXVeXW1fB0JxU1R_9ew) 111 | 112 | ## Setup 113 | 114 | ### Sign up for account 115 | 116 | 1. Free [Sauce account](https://saucelabs.com/sign-up) 117 | 2. Request [Demo Secreener account](https://saucelabs.com/demo-request-vt). **!You must request this at least a week before the workshop as it's a manual process to add users.** 118 | 119 | ### Get your username and api key 120 | 121 | 1. Save your Sauce Labs Username and Access Key by going to the [Sauce Labs user settings page](https://app.saucelabs.com/user-settings) 122 | 2. Save your Screener API Key by going to the [API key](https://screener.io/v2/account/api-key) page in your Screener settings 123 | 1. Need to sign up for [demo account before](https://saucelabs.com/demo-request-vt) 124 | 125 | 126 | ### Local environment setup 127 | 128 | [💻Join Slack #help-desk for tech support](https://join.slack.com/t/testingforgood/shared_invite/zt-zc64x3pc-9ebUXVeXW1fB0JxU1R_9ew) 129 | 130 | Fork then clone the repo 131 | 132 | 1. Sign up for a free [GitHub account](https://github.com/) 133 | 2. [Fork this repository](https://docs.github.com/en/get-started/quickstart/fork-a-repo) 134 | * Make sure you are logged into GitHub 135 | * Click the Fork in the upper right of the GitHub. 136 | 3. Clone your fork of the repository to your machine. Must have [Git installed](https://git-scm.com/downloads) 137 | 138 | ```bash 139 | git clone URL_OF_YOUR_FORK 140 | ``` 141 | 142 | Setup environment variables on your system 143 | * [Mac/Linux](https://docs.saucelabs.com/basics/environment-variables/#setting-up-environment-variables-on-macos-and-linux-systems) 144 | * [Windows](https://docs.saucelabs.com/basics/environment-variables/#setting-up-environment-variables-on-windows-systems) 145 | 146 | Navigate to the directory of where you cloned your repo 147 | 148 | `cd YOUR_FORK_DIR/automation-best-practices/workshop` 149 | 150 | Run sanity tests 151 | 152 | ```java 153 | mvn test -Dtest=SanityTest -X 154 | ``` 155 | 156 |
157 |
158 | 159 | Click here to see an example console output. 160 | 161 | 162 | 163 | Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 54.305 sec 164 | 165 | Results : 166 | 167 | Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 168 | 169 | [INFO] ------------------------------------------------------------------------ 170 | [INFO] BUILD SUCCESS 171 | [INFO] ------------------------------------------------------------------------ 172 | [INFO] Total time: 56.063 s 173 | [INFO] Finished at: 2021-11-03T16:03:20-04:00 174 | [INFO] ------------------------------------------------------------------------ 175 | 176 |
177 | 178 |
179 | 180 | 181 | ### ✅👏Environment setup is complete if tests passed 182 | 183 | > If you weren't successful at setting up you local env, then use the [Gitpod approach](#gitpod-setup)👇 184 | 185 | ### Add static code analysis 186 | 187 | :information_source: Optional Bonus 188 | 189 | * Follow [Codacy instructions to setup static code analysis for your first repo](https://docs.codacy.com/getting-started/codacy-quickstart/) 190 | * Adding and analyzing takes a bit 191 | * [Configure code patterns for the repo](https://docs.codacy.com/repositories-configure/configuring-code-patterns/#pattern-filters) 192 | 193 | --- 194 | 195 | ### Gitpod setup 196 | 197 | [💻Join Slack #help-desk for tech support](https://join.slack.com/t/testingforgood/shared_invite/zt-zc64x3pc-9ebUXVeXW1fB0JxU1R_9ew) 198 | 199 | 200 | :information_source:  Gitpod lets you run an entire Dev environment from a browser! You can use this approach if you don't know how to setup a local Java environment. 201 | 202 | 1. Sign up for a free [GitHub account](https://github.com/) 203 | 2. Fork this repository 204 | * Make sure you are logged into GitHub 205 | * Click the fork in the upper right of GitHub 206 | * Select your username as the location to fork the repo 207 | 3. In the browser address bar, prepend the GitHub url (`https://github.com/USERNAME/automation-best-practices-java`) with `https://gitpod.io/#` 208 | * The resulting url should look as follows: 209 | 210 | > https://gitpod.io/#https://github.com/USERNAME/automation-best-practices-java 211 | 212 | 4. Once the Gitpod.io URL is loaded, you will need to sign in with the GitHub account you created earlier 213 | 5. Once the development environment is loaded, you should see 'Ready to test!' in the Terminal window in the lower portion of the window, run the following commands in that Terminal to set your `SAUCE_USERNAME`, `SAUCE_ACCESS_KEY`, and `SCREENER_API_KEY`: 214 | 215 | :information_source:  You can get your Sauce Labs Username and Access Key by going to the [Sauce Labs user settings page](https://app.saucelabs.com/user-settings) 216 | 217 | :information_source:  You can get your Screener API Key by going to the [API key](https://screener.io/v2/account/api-key) page in your Screener settings 218 | 219 | ```bash 220 | eval $(gp env -e SAUCE_USERNAME=) 221 | eval $(gp env -e SAUCE_ACCESS_KEY=) 222 | eval $(gp env -e SCREENER_API_KEY=) 223 | ``` 224 | 225 | > Replace , , and with your credentials 226 | 227 | Once you have run those 3 commands, you can run the following commands to test your environment variables: 228 | 229 | ```bash 230 | echo $SAUCE_USERNAME 231 | echo $SAUCE_ACCESS_KEY 232 | echo $SCREENER_API_KEY 233 | ``` 234 | 235 | Run sanity tests 236 | 237 | ```bash 238 | mvn test -Dtest=SanityTest -X 239 | ``` 240 | 241 |
242 |
243 | 244 | Click here to see an example console output. 245 | 246 | 247 | 248 | Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 54.305 sec 249 | 250 | Results : 251 | 252 | Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 253 | 254 | [INFO] ------------------------------------------------------------------------ 255 | [INFO] BUILD SUCCESS 256 | [INFO] ------------------------------------------------------------------------ 257 | [INFO] Total time: 56.063 s 258 | [INFO] Finished at: 2021-11-03T16:03:20-04:00 259 | [INFO] ------------------------------------------------------------------------ 260 | 261 |
262 | 263 |
264 | 265 | 266 | ### ✅👏Environment setup is complete if tests passed 267 | 268 | ## Stay to the end and win a prize! 269 | 270 | Stay to the end and 2 lucky people can win a snazzy Back Pack! 271 | 272 | me 273 | 274 | ## Key 275 | 276 | 💡 this is a tip 277 | 278 | 🏋️‍♀️ this is an exercise for you to do 279 | 280 | ❓ this is a question for us to think and talk about. Try not to scroll beyond this question before we discuss 281 | 282 | 283 | -------------------------------------------------------------------------------- /graphics/EyalAvatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices-java/e8ab9d344e284f9ddc3920ab224c704e096b2f24/graphics/EyalAvatar.png -------------------------------------------------------------------------------- /graphics/NikolayAndMia.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices-java/e8ab9d344e284f9ddc3920ab224c704e096b2f24/graphics/NikolayAndMia.JPG -------------------------------------------------------------------------------- /graphics/best-practices-java.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices-java/e8ab9d344e284f9ddc3920ab224c704e096b2f24/graphics/best-practices-java.jpeg -------------------------------------------------------------------------------- /graphics/chris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices-java/e8ab9d344e284f9ddc3920ab224c704e096b2f24/graphics/chris.jpg -------------------------------------------------------------------------------- /graphics/large_Sauce_Bkpk_2021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices-java/e8ab9d344e284f9ddc3920ab224c704e096b2f24/graphics/large_Sauce_Bkpk_2021.png -------------------------------------------------------------------------------- /workshop/docs/ATOMIC-TESTS.MD: -------------------------------------------------------------------------------- 1 | # Automated Atomic Tests 2 | 3 | not 4 | 5 | No, not the best tests that we can code 6 | 7 | An **automated atomic test (AAT)** is one that tests only a single feature or component. An AAT should form a single irreducible unit. An automated test should not do something like end-to-end automation. 8 | 9 | We can usually tell that a test is atomic when: 10 | 11 | * The test will only have one assertion or two assertions at most. Because sometimes we need one assertion to make sure our state is correct 12 | * Atomic tests have very few UI interactions and they’re only on a maximum of two screens. In rare cases, an atomic test might navigate through 3 screens (although I’d like to see this example) 13 | 14 | ## Advantages of atomic tests 15 | 1. Atomic tests fail fast 16 | 2. Atomic tests decrease flaky behavior 17 | 3. Atomic checks allow for focused testing 18 | 4. Atomic tests are short and fast 19 | 20 | As an aside, this concept is already well understood in unit and integration tests, but UI tests continue to lag behind. 21 | 22 | [Read more](https://ultimateqa.com/automated-atomic-tests/) 23 | 24 | ## The steps to creating atomic tests 25 | 26 | 1. Find the functionality that you want to test 27 | 2. Isolate/Mock/Fake all irrelevant actions 28 | 3. Test the relevant feature through UI 29 | 30 | ## 👀How does our app work under the hood? 31 | 32 | Let's take a look at how a login and cart functionality works at the code level 33 | 34 | ## Let's make our tests atomic 35 | 36 | 1. 🏋️‍♀️Go to E2ETests.java and follow instructions in `userCanCheckoutAtomic()` to create an atomic test that validates checkout logic 37 | 2. Don't forget to run the test until it passes 38 | 39 | --- 40 | 41 | ### ❓Questions or concerns about this process 42 | 43 | --- 44 | 45 | [🧪Let's review test coverage](TEST-STRATEGY.MD) 46 | 47 | ## ⏭️Next, we will use visual tests to: 48 | 49 | * Massively increase browser coverage 50 | * Drastically decrease the code we need to maintain 51 | * Drastically increase test suite stability 52 | 53 | [Visual testing](VISUAL.MD) 54 | -------------------------------------------------------------------------------- /workshop/docs/CONCLUSIONS.MD: -------------------------------------------------------------------------------- 1 | # before you go! 2 | 3 | wait 4 | 5 | 1. If you enjoyed this free workshop, please pay it forward and [donate whatever you feel appropriate.](https://www.gofundme.com/f/testing-for-good-codeorg) 6 | 7 | > 100% of the donations go to help kids learn computer science. 8 | 9 | 2. 📫 Follow me to stay up to date on the next Testing for Good workshop! 10 | 11 | - [Java Testing Newsletter](https://ultimateqa.ck.page/selenium-java-tips) 12 | - [Youtube](https://youtube.com/ultimateqa) 13 | - [LinkedIn](https://www.linkedin.com/in/nikolayadvolodkin/) 14 | - [Twitter](https://twitter.com/Nikolay_A00) 15 | 16 | 3. Please give me [anonymous feedback on the workshop](https://forms.gle/UT1SVtuZDq84XWFR7) 17 | 4. 💃Let's pick 2 winners for backpacks! 18 | 19 | backpack 20 | 21 | 22 | ## Thanks so much for your time and generosity 🙌👏 23 | 24 | thanks 25 | 26 | 27 | ## Extra resources 28 | 29 | * [Complete Selenium WebDriver Java Bootcamp](https://ultimateqa.com/selenium-java) 30 | * [Parallelization with Junit4,Junit5,TestNg](https://youtu.be/ufccoaURMIc) 31 | * [21+ Automation best practices](https://ultimateqa.com/automation-patterns-antipatterns/) 32 | 33 | -------------------------------------------------------------------------------- /workshop/docs/E2E-TESTS.MD: -------------------------------------------------------------------------------- 1 | # E2E Tests 2 | 3 | ## Review test strategy 4 | 5 | [Let's review our testing gaps](TEST-STRATEGY.MD) 6 | 7 | ## Covering business risk 8 | 9 | We can use **functional e2e UI** tests to cover some of the risks 10 | 11 | ## 🏋️‍♀️Write tests using best practices 12 | 13 | 1. Go to `com.saucedemo.exercises.E2ETests.java` and finish all of the tests. 14 | 2. Run each test to make sure they work 15 | 3. At the end, run all tests with `mvn test -Dtest=E2ETests` 16 | 17 | 18 | **Rules** 19 | 20 | ✅ Use page objects 21 | 22 | ✅ One test for one feature 23 | 24 | ❌ Don't create any new code, simply reuse existing code 25 | 26 | ### ️👀How does this work? 27 | 28 | Let's walk through the code 29 | 30 | [🧪Let's review test coverage](TEST-STRATEGY.MD) 31 | 32 | --- 33 | 34 | ## ❓Are these the best tests that we can write 35 | 36 | --- 37 | 38 | [👉Answer in the next section](ATOMIC-TESTS.MD) 39 | -------------------------------------------------------------------------------- /workshop/docs/PARALLEL.MD: -------------------------------------------------------------------------------- 1 | # Parallel Execution 2 | 3 | parallel 4 | 5 | 6 | ## 🧠You will learn 7 | 8 | ✅ How to create ridiculously fast test suites 9 | 10 | ✅ How to implement parallelization 11 | 12 | ## In today's world, it's improbable to succeed without parallelization 13 | 14 | > "Once you have these automated tests, our analysis shows it’s important to run them regularly. Every commit should trigger a build of the software and running a set of fast, automated tests. Developers should get feedback from a more comprehensive suite of acceptance and performance tests every day. Furthermore, current builds should be available to testers for exploratory testing." (Nicole Forsgren PhD, Jez Humble, Gene Kim, Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations) 15 | 16 | Try to run the current suite of tests: 17 | 18 | ```text 19 | mvn test -Dtest="E2ESolutionTests,VisualDataDrivenSolutionTests" 20 | ``` 21 | 22 | This is how long my tests took 23 | 24 | run-time 25 | 26 | 241 sec/8 tests = 30 sec/test 27 | 28 | --- 29 | 30 | ### ❓What's the problem with this approach? 31 | 32 | --- 33 | 34 | ## 🏋️‍♀Implement parallelization 35 | 36 | 1. Go to `pom.xml` and add the following at the same level as the `` node 37 | 38 | ```xml 39 | 40 | 41 | 42 | org.apache.maven.plugins 43 | maven-surefire-plugin 44 | 3.0.0-M5 45 | 46 | all 47 | 100 48 | true 49 | false 50 | 51 | 52 | 53 | 54 | ``` 55 | 56 | 2. In terminal run 57 | 58 | ```text 59 | mvn test -Dtest="E2ESolutionTests,VisualDataDrivenSolutionTests" 60 | ``` 61 | 62 | 3. Login to saucelabs.com and watch tests run in parallel 63 | 64 | My results 65 | 66 | run-time2 67 | 68 | ## What we just achieved ✅💪 69 | 70 | ✅ 78% speed improvement in < 5 min 71 | 72 | ✅ 0 degradation to our test quality 73 | 74 | ✅ Enabled parallel scaling 75 | 76 | ## 📝Summary 77 | 78 | ✅ Parallel testing is awesome 79 | 80 | ## Extra resources 81 | 82 | [Achieving 97% test suite run time improvement](https://devops.com/4-steps-to-achieve-a-66-reduction-in-test-run-time/) 83 | 84 | ## [⏭️Let's summarize](CONCLUSIONS.MD) 85 | -------------------------------------------------------------------------------- /workshop/docs/TEST-STRATEGY.MD: -------------------------------------------------------------------------------- 1 | ## 🧪Our Testing Strategy 2 | 3 | | Expected Behavior | Tested? | Test Type | Technologies/Comments | 4 | |---|---|---|---| 5 | | Login page renders | ✅ | visual e2e | Selenium, Sauce Labs, Screener, | 6 | | Valid user can login | ✅ | ui/e2e/functional | Selenium, Sauce Labs | 7 | | User can checkout with a product | ✅ | ui/e2e/functional | Selenium, Sauce Labs, Cookie injection, and JS injection | 8 | | Login page looks as expected on Chrome on most popular resolution | ✅ | visual e2e | Screener, Sauce, Selenium | 9 | | Products page looks as expected on Chrome on most popular resolution | ✅ | visual e2e | Screener, Sauce, Selenium | 10 | | Cart page looks as expected on Chrome on most popular resolution | ✅ | visual e2e | Screener, Sauce, Selenium | 11 | | Login page looks as expected on Safari on most popular resolution | ✅ | visual e2e | Screener, Sauce, Selenium | 12 | | Products page looks as expected on Safari on most popular resolution | ✅ | visual e2e | Screener, Sauce, Selenium | 13 | | Cart page looks as expected on Safari on most popular resolution | ✅ | visual e2e | Screener, Sauce, Selenium | 14 | | App is accessibility friendly | 🙅‍♂️ | | | 15 | | Front-end performance is at least a B | 🙅‍♂️ | | | 16 | | App is secure | 🙅‍♂️ | | | 17 | | Multiple other testing types... | 🙅‍♂️ | | | 18 | -------------------------------------------------------------------------------- /workshop/docs/VISUAL.MD: -------------------------------------------------------------------------------- 1 | # Visual Testing 2 | 3 | not 4 | 5 | ## 🧠You will learn 6 | 7 | ✅ What is visual testing 8 | 9 | ✅ How to implement a visual test 10 | 11 | ## What is visual testing? 12 | 13 | [Quick presentation](https://docs.google.com/presentation/d/13jYXXoKb36aFt1HLnNnAmsPqw9yaFhVrB4iFH_5_WkI/edit#slide=id.gcc181d5a54_0_21) 14 | 15 | [🧪Let's review test coverage](TEST-STRATEGY.MD) 16 | 17 | ## 🏋️‍♀Implement visual tests 18 | 19 | Go to `VisualTests.java` and follow instructions to implement visual tests 20 | 21 | --- 22 | 23 | ### ❓Questions? 24 | 25 | --- 26 | 27 | --- 28 | 29 | ### ❓How do we do the same thing on Safari? 30 | 31 | --- 32 | 33 | ## 🏋️‍♀Implement data-driven visual tests 34 | 35 | 1. Go to VisualDataDrivenTests.java and follow instructions. Be sure to read the explanations. 36 | 2. Run data-driven tests use `mvn test -Dtest="VisualData*"` 37 | 3. Check results in Screener.io dashboard 38 | 39 | ## [🧪Let's review test coverage](TEST-STRATEGY.MD) 40 | 41 | --- 42 | 43 | ### ❓Any redundant tests? 44 | 45 | --- 46 | 47 | ## 📝Summary 48 | 49 | ✅Visual e2e testing is a simple and efficient way to test your web app cross-platform 50 | 51 | ✅Visual e2e testing should be part of every web app's test suite 52 | 53 | ## [⏭️ Next, let's make our suite really fast!](PARALLEL.MD) 54 | -------------------------------------------------------------------------------- /workshop/images/run-time.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices-java/e8ab9d344e284f9ddc3920ab224c704e096b2f24/workshop/images/run-time.jpg -------------------------------------------------------------------------------- /workshop/images/run-time2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saucelabs-training/automation-best-practices-java/e8ab9d344e284f9ddc3920ab224c704e096b2f24/workshop/images/run-time2.jpg -------------------------------------------------------------------------------- /workshop/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | org.example 8 | workshop 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 8 13 | 8 14 | 15 | 16 | 17 | 18 | com.saucelabs 19 | sauce_bindings 20 | 1.2.0 21 | test 22 | 23 | 24 | 25 | org.seleniumhq.selenium 26 | selenium-java 27 | 4.0.0 28 | test 29 | 30 | 31 | 32 | com.saucelabs 33 | saucebindings-junit4 34 | 1.0.1 35 | test 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/exercises/E2ETests.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.exercises; 2 | 3 | import com.saucedemo.solution.pages.*; 4 | import com.saucelabs.saucebindings.junit4.SauceBaseTest; 5 | import org.junit.Ignore; 6 | import org.junit.Test; 7 | import org.openqa.selenium.By; 8 | import org.openqa.selenium.Cookie; 9 | import org.openqa.selenium.JavascriptExecutor; 10 | import org.openqa.selenium.NoSuchElementException; 11 | 12 | import static org.junit.Assert.assertEquals; 13 | import static org.junit.Assert.assertTrue; 14 | 15 | public class E2ETests extends SauceBaseTest { 16 | // Here's the first test to get you started. Try to run it 17 | @Test() 18 | public void appRenders() { 19 | LoginPage loginPage = new LoginPage(driver); 20 | loginPage.visit(); 21 | assertTrue(loginPage.isDisplayed()); 22 | } 23 | @Test() 24 | public void loginWorks() { 25 | LoginPage loginPage = new LoginPage(driver); 26 | /* 27 | * Add your code below this 28 | * */ 29 | 30 | 31 | 32 | /* 33 | * ^^^^^^^^ AddYour code above this ^^^^^^^^^ 34 | * */ 35 | assertTrue(new ProductsPage(driver).isDisplayed()); 36 | } 37 | @Test() 38 | public void userCanCheckout() { 39 | /* 40 | * Add your code below this 41 | * */ 42 | 43 | 44 | 45 | /* 46 | * ^^^^^^^^ AddYour code above this ^^^^^^^^^ 47 | * */ 48 | assertTrue(new CheckoutCompletePage(driver).isDisplayed()); 49 | } 50 | 51 | /* 52 | * Don't do or look at the test below until the atomic tests section 53 | * */ 54 | @Test() 55 | @Ignore("Ignoring until atomic tests section") 56 | public void userCanCheckoutAtomic() { 57 | /* 58 | * Add your code below this 59 | * */ 60 | 61 | /* 62 | * 1. First navigate to the LoginPage 63 | * */ 64 | 65 | /* 66 | * 2. Removing UI Login 67 | * We already know that our user can successfully login with loginWorks() 68 | * hence, we don't need to waste time, web requests, or add flakiness 69 | * 70 | * Uncomment the code below to make this possible 71 | * */ 72 | // driver.manage().deleteAllCookies(); 73 | // ((JavascriptExecutor)driver).executeScript("localStorage.clear();"); 74 | // Cookie loginCookie = new Cookie("session-username", "standard_user"); 75 | // //try document.cookie="session-username=standard_user" in browser Console 76 | // driver.manage().addCookie(loginCookie); 77 | //PS. In production code you can Hide this behavior in an App object. 78 | // I put it here only for clarity 79 | //You can create an App.setState(AppState appStateObject) 80 | // or Browser.clearLocalStorage() 81 | 82 | /* 83 | * 3. Add item to cart without UI interactions 84 | * 85 | * We also don't care whether or not clicking a button will add an item to a cart 86 | * We can easily cover this risk with another test 87 | * Hence, let's simulate adding an item to a cart by updating localStorage 88 | * 89 | * Uncomment the code below 90 | * */ 91 | // ShoppingCartPage cart = new ShoppingCartPage(driver); 92 | // //this won't be possible if you're not logged in 93 | // cart.visit(); 94 | // ((JavascriptExecutor)driver).executeScript("localStorage.setItem(\"cart-contents\", \"[4]\")"); 95 | // driver.navigate().refresh(); 96 | // //checking that app is in correct state 97 | // assertEquals(1, cart.getItemsCount()); 98 | 99 | /* 100 | * 4. Truly test the checkout flow 101 | * All the preconditions have been met 102 | * - User is logged in 103 | * - User has item in a cart 104 | * Does the checkout process work? 105 | * 106 | * Fill in the code, you've done this before 107 | * */ 108 | 109 | 110 | /* 111 | * ^^^^^^^^ AddYour code above this ^^^^^^^^^ 112 | * */ 113 | assertTrue(new CheckoutCompletePage(driver).isDisplayed()); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/exercises/SanityTest.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.exercises; 2 | 3 | import com.saucelabs.saucebindings.SauceSession; 4 | import org.junit.After; 5 | import org.junit.Test; 6 | import org.openqa.selenium.JavascriptExecutor; 7 | import org.openqa.selenium.MutableCapabilities; 8 | import org.openqa.selenium.remote.CapabilityType; 9 | import org.openqa.selenium.remote.RemoteWebDriver; 10 | 11 | import java.net.MalformedURLException; 12 | import java.net.URL; 13 | import java.util.Map; 14 | 15 | import static org.junit.Assert.assertNotNull; 16 | import static org.junit.Assert.assertNull; 17 | 18 | public class SanityTest { 19 | RemoteWebDriver driver; 20 | @Test 21 | public void functionalWorks() { 22 | driver = new SauceSession().start(); 23 | assertNotNull("Register for your free sauce account https://saucelabs.com/sign-up", driver); 24 | } 25 | 26 | @Test 27 | public void visualWorks() throws MalformedURLException { 28 | MutableCapabilities capabilities = new MutableCapabilities(); 29 | capabilities.setCapability(CapabilityType.BROWSER_NAME, "chrome"); 30 | capabilities.setCapability(CapabilityType.BROWSER_VERSION, "latest"); 31 | capabilities.setCapability(CapabilityType.PLATFORM_NAME, "Windows 10"); 32 | 33 | MutableCapabilities sauceOptions = new MutableCapabilities(); 34 | sauceOptions.setCapability("username", System.getenv("SAUCE_USERNAME")); 35 | sauceOptions.setCapability("accesskey", System.getenv("SAUCE_ACCESS_KEY")); 36 | capabilities.setCapability("sauce:options", sauceOptions); 37 | 38 | MutableCapabilities visualOptions = new MutableCapabilities(); 39 | visualOptions.setCapability("apiKey", System.getenv("SCREENER_API_KEY")); 40 | visualOptions.setCapability("projectName", "java-sanity"); 41 | visualOptions.setCapability("viewportSize", "1280x1024"); 42 | visualOptions.setCapability("failOnNewStates", false); 43 | capabilities.setCapability("sauce:visual", visualOptions); 44 | 45 | URL url = new URL("https://hub.screener.io/wd/hub"); 46 | driver = new RemoteWebDriver(url, capabilities); 47 | 48 | driver.get("https://saucedemo.com"); 49 | 50 | ((JavascriptExecutor)driver).executeScript("/*@visual.init*/", "Simple visual test"); 51 | ((JavascriptExecutor)driver).executeScript("/*@visual.snapshot*/", "Home"); 52 | Map response = (Map) ((JavascriptExecutor)driver).executeScript("/*@visual.end*/"); 53 | assertNull(response.get("message")); 54 | } 55 | 56 | @After 57 | public void tearDown() { 58 | if(driver != null){ 59 | driver.quit(); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/exercises/VisualDataDrivenTests.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.exercises; 2 | 3 | import com.saucedemo.solution.AbstractTestBase; 4 | import com.saucedemo.solution.pages.LoginPage; 5 | import com.saucedemo.solution.pages.ProductsPage; 6 | import com.saucedemo.solution.pages.ShoppingCartPage; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.junit.runners.Parameterized; 11 | import org.openqa.selenium.MutableCapabilities; 12 | import org.openqa.selenium.remote.CapabilityType; 13 | import org.openqa.selenium.remote.RemoteWebDriver; 14 | 15 | import java.net.URL; 16 | import java.util.Arrays; 17 | import java.util.Collection; 18 | import java.util.Map; 19 | 20 | import static org.junit.Assert.assertNull; 21 | 22 | @RunWith(Parameterized.class) 23 | public class VisualDataDrivenTests extends AbstractTestBase { 24 | //declare a bunch of variables 25 | private RemoteWebDriver driver; 26 | 27 | /* 28 | * Configure our data driven parameters 29 | * Each field gets a @Parameterized.Parameter annotation with an index 30 | * The order of these indices corresponds to the order of the strings 31 | * in the crossBrowserData() 32 | * For example, "chrome" = public String browserName 33 | * Hence, any value in the 0th index position will go into browserName 34 | * */ 35 | @Parameterized.Parameter 36 | public String browserName; 37 | @Parameterized.Parameter(1) 38 | public String platform; 39 | @Parameterized.Parameter(2) 40 | public String browserVersion; 41 | @Parameterized.Parameter(3) 42 | public String viewportSize; 43 | // resolutionName is an identifier of the browser resolution 44 | @Parameterized.Parameter(4) 45 | public String resolutionName; 46 | 47 | /* 48 | * This is our collection of data driven values. 49 | * For demonstration purposes the data is hardcoded in the class. 50 | * It can also be read from an external data source. 51 | * 52 | * For each row of data, a new test method will be created. 53 | * In this case, we have 2 rows of data meaning that visualFlow() 54 | * will run 2 times, using the data from each row. 55 | * If we had 10 rows, the visualFlow() would execute 10 times 56 | * 57 | * In visual testing it's very valuable to data-drive a test method 58 | * across many browser/os/resolution combinations as rendering bugs 59 | * are extremely common in responsive web apps. 60 | * However, it's also possible to data drive on something like languages 61 | * if you want to see each page rendered in a corresponding language 62 | * */ 63 | @Parameterized.Parameters() 64 | public static Collection crossBrowserData() { 65 | return Arrays.asList(new Object[][]{ 66 | {"Chrome", "Windows 10", "latest", "1080x720", "1080p"}, 67 | {"Safari", "macOS 10.15", "latest", "1080x720", "1080p"} 68 | }); 69 | } 70 | 71 | @Before 72 | public void setUp() throws Exception { 73 | //setting our browser/os capabilities 74 | /* 75 | * Add your code below this 76 | * */ 77 | 78 | //1. Replace the hardcoded values with the correct fields 79 | MutableCapabilities browserOptions = new MutableCapabilities(); 80 | browserOptions.setCapability(CapabilityType.BROWSER_NAME, "firefox"); 81 | browserOptions.setCapability(CapabilityType.BROWSER_VERSION, "oldest"); 82 | browserOptions.setCapability(CapabilityType.PLATFORM_NAME, "Windows ME"); 83 | 84 | //pass information to sauce labs 85 | MutableCapabilities sauceOptions = new MutableCapabilities(); 86 | sauceOptions.setCapability("username", SAUCE_USERNAME); 87 | sauceOptions.setCapability("accessKey", SAUCE_ACCESS_KEY); 88 | sauceOptions.setCapability("name", testName.getMethodName()); 89 | sauceOptions.setCapability("build", buildName); 90 | browserOptions.setCapability("sauce:options", sauceOptions); 91 | 92 | 93 | MutableCapabilities visualOptions = new MutableCapabilities(); 94 | visualOptions.setCapability("apiKey", SCREENER_API_KEY); 95 | visualOptions.setCapability("projectName", "Sauce Demo Java"); 96 | //2. also pass in the correct viewport size field instead of hardcoding 97 | visualOptions.setCapability("viewportSize", "1x1"); 98 | visualOptions.setCapability("failOnNewStates", false); 99 | 100 | /* 101 | * ^^^^^^^^ AddYour code above this ^^^^^^^^^ 102 | * */ 103 | 104 | browserOptions.setCapability("sauce:visual", visualOptions); 105 | 106 | //point to Screener hub 107 | URL url = new URL("https://hub.screener.io/wd/hub"); 108 | driver = new RemoteWebDriver(url, browserOptions); 109 | } 110 | @Test() 111 | public void visualFlow() { 112 | LoginPage loginPage = new LoginPage(driver); 113 | loginPage.visit(); 114 | //3. We are dynamically setting test name from our crossBrowserData() 115 | driver.executeScript("/*@visual.init*/", resolutionName); 116 | //take a visual snapshot of our page 117 | loginPage.takeSnapshot(); 118 | 119 | loginPage.login("standard_user"); 120 | ProductsPage productsPage = new ProductsPage(driver); 121 | productsPage.takeSnapshot(); 122 | 123 | productsPage.addAnyProductToCart(); 124 | ShoppingCartPage cart = new ShoppingCartPage(driver); 125 | cart.visit(); 126 | cart.takeSnapshot(); 127 | 128 | //we only need a single assertion per 'init' 129 | //get the results object and check to see if any visual discrepancies were found 130 | Map response = cart.getVisualResults(); 131 | assertNull(response.get("message")); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/exercises/VisualTests.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.exercises; 2 | 3 | import com.saucedemo.solution.AbstractTestBase; 4 | import com.saucedemo.solution.pages.LoginPage; 5 | import org.junit.Before; 6 | import org.junit.Test; 7 | import org.openqa.selenium.MutableCapabilities; 8 | import org.openqa.selenium.remote.CapabilityType; 9 | import org.openqa.selenium.remote.RemoteWebDriver; 10 | 11 | import java.net.URL; 12 | 13 | public class VisualTests extends AbstractTestBase { 14 | //declare a bunch of variables 15 | private RemoteWebDriver driver; 16 | 17 | public String browserName = "chrome"; 18 | public String browserVersion = "latest"; 19 | public String platform = "Windows 10"; 20 | public String viewportSize = "1080x720"; 21 | 22 | @Before 23 | public void setUp() throws Exception { 24 | //setting our browser/os capabilities 25 | MutableCapabilities browserOptions = new MutableCapabilities(); 26 | browserOptions.setCapability(CapabilityType.BROWSER_NAME, browserName); 27 | browserOptions.setCapability(CapabilityType.BROWSER_VERSION, browserVersion); 28 | browserOptions.setCapability(CapabilityType.PLATFORM_NAME, platform); 29 | 30 | //pass information to sauce labs 31 | MutableCapabilities sauceOptions = new MutableCapabilities(); 32 | sauceOptions.setCapability("username", SAUCE_USERNAME); 33 | sauceOptions.setCapability("accessKey", SAUCE_ACCESS_KEY); 34 | sauceOptions.setCapability("name", testName.getMethodName()); 35 | sauceOptions.setCapability("build", buildName); 36 | browserOptions.setCapability("sauce:options", sauceOptions); 37 | 38 | //pass information to screener.io 39 | MutableCapabilities visualOptions = new MutableCapabilities(); 40 | visualOptions.setCapability("apiKey", SCREENER_API_KEY); 41 | visualOptions.setCapability("projectName", "Sauce Demo Java"); 42 | visualOptions.setCapability("viewportSize", viewportSize); 43 | visualOptions.setCapability("failOnNewStates", false); 44 | 45 | browserOptions.setCapability("sauce:visual", visualOptions); 46 | 47 | //point to Screener hub 48 | URL url = new URL("https://hub.screener.io/wd/hub"); 49 | driver = new RemoteWebDriver(url, browserOptions); 50 | } 51 | @Test() 52 | public void visualFlow() { 53 | LoginPage loginPage = new LoginPage(driver); 54 | loginPage.visit(); 55 | /* 56 | * Add your code below this 57 | * */ 58 | 59 | //1. uncomment the line below, it starts the test session in screener and takes a test 60 | //name as the arg 61 | //driver.executeScript("/*@visual.init*/", "1080p"); 62 | 63 | //2. uncomment to take a visual snapshot of our page 64 | //look inside of the takeSnapshot() to see how it's implemented 65 | //loginPage.takeSnapshot(); 66 | 67 | //3. Now we want to capture the snapshot of the products page 68 | // use the ProductsPage() to capture it's snapshot 69 | //loginPage.login("standard_user"); 70 | //create new products page 71 | // takeSnapshot() 72 | 73 | //4. use the ProductsPage object to addAnyProductToCart(); 74 | // then visit() to the ShoppingCartPage() 75 | // and then takeSnapshot() of the cart 76 | //productsPage.addAnyProductToCart(); 77 | 78 | /* 79 | * ^^^^^^^^ AddYour code above this ^^^^^^^^^ 80 | * */ 81 | 82 | //5. Finally we perform an assertion and check for any visual differences 83 | //we only need a single assertion per 'init' 84 | //get the results object and check to see if any visual discrepancies were found 85 | // uncomment the code below, run your test, it should pass 86 | // Map response = cart.getVisualResults(); 87 | // assertNull(response.get("message")); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/AbstractTestBase.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution; 2 | 3 | import org.junit.Rule; 4 | import org.junit.rules.TestName; 5 | import org.junit.rules.TestWatcher; 6 | import org.junit.runner.Description; 7 | import org.openqa.selenium.remote.RemoteWebDriver; 8 | 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | 12 | public class AbstractTestBase { 13 | public static final String buildName = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); 14 | 15 | @Rule 16 | public TestName testName = new TestName() { 17 | public String getMethodName() { 18 | return String.format("%s", super.getMethodName()); 19 | } 20 | }; 21 | @Rule 22 | public SauceTestWatcher resultReportingTestWatcher = new SauceTestWatcher(); 23 | 24 | protected static final String SAUCE_USERNAME = System.getenv("SAUCE_USERNAME"); 25 | protected static final String SAUCE_ACCESS_KEY = System.getenv("SAUCE_ACCESS_KEY"); 26 | protected static final String SCREENER_API_KEY = System.getenv("SCREENER_API_KEY"); 27 | protected RemoteWebDriver driver; 28 | 29 | /** 30 | * Custom TestWatcher for Sauce Labs projects. 31 | */ 32 | public class SauceTestWatcher extends TestWatcher { 33 | @Override 34 | protected void succeeded(Description description) { 35 | if (driver != null) { 36 | driver.executeScript("sauce:job-result=passed"); 37 | driver.quit(); 38 | } 39 | } 40 | 41 | @Override 42 | protected void failed(Throwable e, Description description) { 43 | if (driver != null) { 44 | driver.executeScript("sauce:job-result=failed"); 45 | driver.quit(); 46 | } 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/E2ESolutionTests.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution; 2 | 3 | import com.saucedemo.solution.pages.*; 4 | import com.saucelabs.saucebindings.junit4.SauceBaseTest; 5 | import org.junit.Ignore; 6 | import org.junit.Test; 7 | import org.openqa.selenium.*; 8 | 9 | import static org.junit.Assert.*; 10 | 11 | public class E2ESolutionTests extends SauceBaseTest { 12 | @Test() 13 | public void appRenders() { 14 | LoginPage loginPage = new LoginPage(driver); 15 | loginPage.visit(); 16 | assertTrue(loginPage.isDisplayed()); 17 | } 18 | @Test() 19 | public void loginWorks() { 20 | LoginPage loginPage = new LoginPage(driver); 21 | loginPage.visit(); 22 | loginPage.login("standard_user"); 23 | assertTrue(new ProductsPage(driver).isDisplayed()); 24 | } 25 | @Test() 26 | public void userCanCheckout() { 27 | LoginPage loginPage = new LoginPage(driver); 28 | loginPage.visit(); 29 | loginPage.login("standard_user"); 30 | ProductsPage productsPage = new ProductsPage(driver); 31 | productsPage.addAnyProductToCart(); 32 | CheckoutStepOnePage stepOnePage = new CheckoutStepOnePage(driver); 33 | stepOnePage.visit(); 34 | stepOnePage.enterPersonalDetails(); 35 | new CheckoutOverviewPage(driver).finish(); 36 | assertTrue(new CheckoutCompletePage(driver).isDisplayed()); 37 | } 38 | @Test() 39 | public void userCanCheckoutAtomic() { 40 | LoginPage loginPage = new LoginPage(driver); 41 | loginPage.visit(); 42 | 43 | //Hide this behavior in an App object. I put it here only for clarity 44 | //You can create a App.setState(AppState appStateObject) 45 | driver.manage().deleteAllCookies(); 46 | ((JavascriptExecutor)driver).executeScript("localStorage.clear();"); 47 | Cookie loginCookie = new Cookie("session-username", "standard_user"); 48 | //try document.cookie="session-username=standard_user" in browser Console 49 | driver.manage().addCookie(loginCookie); 50 | 51 | ShoppingCartPage cart = new ShoppingCartPage(driver); 52 | cart.visit(); 53 | //checking that app is in correct state 54 | assertEquals(0, cart.getItemsCount()); 55 | 56 | ((JavascriptExecutor)driver).executeScript("localStorage.setItem(\"cart-contents\", \"[4]\")"); 57 | driver.navigate().refresh(); 58 | //checking that app is in correct state 59 | assertEquals(1, cart.getItemsCount()); 60 | 61 | //now we can actually do the checkout logic that we want to test 62 | CheckoutStepOnePage stepOnePage = new CheckoutStepOnePage(driver); 63 | stepOnePage.visit(); 64 | stepOnePage.enterPersonalDetails(); 65 | new CheckoutOverviewPage(driver).finish(); 66 | assertTrue(new CheckoutCompletePage(driver).isDisplayed()); 67 | } 68 | @Test(expected = NoSuchElementException.class) 69 | @Ignore("not used") 70 | public void appRendersError() { 71 | LoginPage loginPage = new LoginPage(driver); 72 | loginPage.visit(); 73 | assertTrue(driver.findElement(By.cssSelector("usernamefoo")).isDisplayed()); 74 | } 75 | @Test(expected = NoSuchElementException.class) 76 | @Ignore("not used") 77 | public void sameErrorLessRedundancy() { 78 | LoginPage loginPage = new LoginPage(driver); 79 | loginPage.visit(); 80 | //attempt to login here 81 | driver.findElement(By.cssSelector("usernamefoo")).sendKeys("standard_user"); 82 | //will continue to login, unless there is an error 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/VisualDataDrivenSolutionTests.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution; 2 | 3 | import com.saucedemo.solution.pages.LoginPage; 4 | import com.saucedemo.solution.pages.ProductsPage; 5 | import com.saucedemo.solution.pages.ShoppingCartPage; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.junit.runners.Parameterized; 10 | import org.openqa.selenium.MutableCapabilities; 11 | import org.openqa.selenium.remote.CapabilityType; 12 | import org.openqa.selenium.remote.RemoteWebDriver; 13 | 14 | import java.net.URL; 15 | import java.util.Arrays; 16 | import java.util.Collection; 17 | import java.util.Map; 18 | 19 | import static org.junit.Assert.assertNull; 20 | 21 | @RunWith(Parameterized.class) 22 | public class VisualDataDrivenSolutionTests extends AbstractTestBase { 23 | //declare a bunch of variables 24 | private RemoteWebDriver driver; 25 | 26 | /* 27 | * Configure our data driven parameters 28 | * */ 29 | @Parameterized.Parameter 30 | public String browserName; 31 | @Parameterized.Parameter(1) 32 | public String platform; 33 | @Parameterized.Parameter(2) 34 | public String browserVersion; 35 | @Parameterized.Parameter(3) 36 | public String viewportSize; 37 | // resolutionName is an identifier of the browser resolution 38 | @Parameterized.Parameter(4) 39 | public String resolutionName; 40 | 41 | @Parameterized.Parameters() 42 | public static Collection crossBrowserData() { 43 | return Arrays.asList(new Object[][]{ 44 | {"Chrome", "Windows 10", "latest", "1080x720", "1080p"}, 45 | {"Safari", "macOS 10.15", "latest", "1080x720", "1080p"} 46 | }); 47 | } 48 | 49 | @Before 50 | public void setUp() throws Exception { 51 | //setting our browser/os capabilities 52 | MutableCapabilities browserOptions = new MutableCapabilities(); 53 | browserOptions.setCapability(CapabilityType.BROWSER_NAME, browserName); 54 | browserOptions.setCapability(CapabilityType.BROWSER_VERSION, browserVersion); 55 | browserOptions.setCapability(CapabilityType.PLATFORM_NAME, platform); 56 | 57 | //pass information to sauce labs 58 | MutableCapabilities sauceOptions = new MutableCapabilities(); 59 | sauceOptions.setCapability("username", SAUCE_USERNAME); 60 | sauceOptions.setCapability("accessKey", SAUCE_ACCESS_KEY); 61 | sauceOptions.setCapability("name", testName.getMethodName()); 62 | sauceOptions.setCapability("build", buildName); 63 | browserOptions.setCapability("sauce:options", sauceOptions); 64 | 65 | //pass information to screener.io 66 | MutableCapabilities visualOptions = new MutableCapabilities(); 67 | visualOptions.setCapability("apiKey", SCREENER_API_KEY); 68 | visualOptions.setCapability("projectName", "Sauce Demo Java"); 69 | visualOptions.setCapability("viewportSize", viewportSize); 70 | visualOptions.setCapability("failOnNewStates", false); 71 | 72 | browserOptions.setCapability("sauce:visual", visualOptions); 73 | 74 | //point to Screener hub 75 | URL url = new URL("https://hub.screener.io/wd/hub"); 76 | driver = new RemoteWebDriver(url, browserOptions); 77 | } 78 | @Test() 79 | public void visualFlow() { 80 | LoginPage loginPage = new LoginPage(driver); 81 | loginPage.visit(); 82 | //provide the test name 83 | driver.executeScript("/*@visual.init*/", resolutionName); 84 | //take a visual snapshot of our page 85 | loginPage.takeSnapshot(); 86 | 87 | loginPage.login("standard_user"); 88 | ProductsPage productsPage = new ProductsPage(driver); 89 | productsPage.takeSnapshot(); 90 | 91 | productsPage.addAnyProductToCart(); 92 | ShoppingCartPage cart = new ShoppingCartPage(driver); 93 | cart.visit(); 94 | cart.takeSnapshot(); 95 | 96 | //we only need a single assertion per 'init' 97 | //get the results object and check to see if any visual discrepancies were found 98 | Map response = cart.getVisualResults(); 99 | assertNull(response.get("message")); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/VisualSolutionTests.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution; 2 | 3 | import com.saucedemo.solution.pages.LoginPage; 4 | import com.saucedemo.solution.pages.ProductsPage; 5 | import com.saucedemo.solution.pages.ShoppingCartPage; 6 | import org.junit.Before; 7 | import org.junit.Test; 8 | import org.openqa.selenium.MutableCapabilities; 9 | import org.openqa.selenium.remote.CapabilityType; 10 | import org.openqa.selenium.remote.RemoteWebDriver; 11 | 12 | import java.net.URL; 13 | import java.util.Map; 14 | 15 | import static org.junit.Assert.assertNull; 16 | 17 | public class VisualSolutionTests extends AbstractTestBase { 18 | //declare a bunch of variables 19 | private RemoteWebDriver driver; 20 | 21 | public String browserName = "chrome"; 22 | public String browserVersion = "latest"; 23 | public String platform = "Windows 10"; 24 | public String viewportSize = "1080x720"; 25 | 26 | @Before 27 | public void setUp() throws Exception { 28 | //setting our browser/os capabilities 29 | MutableCapabilities browserOptions = new MutableCapabilities(); 30 | browserOptions.setCapability(CapabilityType.BROWSER_NAME, browserName); 31 | browserOptions.setCapability(CapabilityType.BROWSER_VERSION, browserVersion); 32 | browserOptions.setCapability(CapabilityType.PLATFORM_NAME, platform); 33 | 34 | //pass information to sauce labs 35 | MutableCapabilities sauceOptions = new MutableCapabilities(); 36 | sauceOptions.setCapability("username", SAUCE_USERNAME); 37 | sauceOptions.setCapability("accessKey", SAUCE_ACCESS_KEY); 38 | sauceOptions.setCapability("name", testName.getMethodName()); 39 | sauceOptions.setCapability("build", buildName); 40 | browserOptions.setCapability("sauce:options", sauceOptions); 41 | 42 | //pass information to screener.io 43 | MutableCapabilities visualOptions = new MutableCapabilities(); 44 | visualOptions.setCapability("apiKey", SCREENER_API_KEY); 45 | visualOptions.setCapability("projectName", "Sauce Demo Java"); 46 | visualOptions.setCapability("viewportSize", viewportSize); 47 | visualOptions.setCapability("failOnNewStates", false); 48 | 49 | browserOptions.setCapability("sauce:visual", visualOptions); 50 | 51 | //point to Screener hub 52 | URL url = new URL("https://hub.screener.io/wd/hub"); 53 | driver = new RemoteWebDriver(url, browserOptions); 54 | } 55 | @Test() 56 | public void visualFlow() { 57 | LoginPage loginPage = new LoginPage(driver); 58 | loginPage.visit(); 59 | //provide the test name 60 | driver.executeScript("/*@visual.init*/", "1080p"); 61 | //take a visual snapshot of our page 62 | loginPage.takeSnapshot(); 63 | 64 | loginPage.login("standard_user"); 65 | ProductsPage productsPage = new ProductsPage(driver); 66 | productsPage.takeSnapshot(); 67 | 68 | productsPage.addAnyProductToCart(); 69 | ShoppingCartPage cart = new ShoppingCartPage(driver); 70 | cart.visit(); 71 | cart.takeSnapshot(); 72 | 73 | //we only need a single assertion per 'init' 74 | //get the results object and check to see if any visual discrepancies were found 75 | Map response = cart.getVisualResults(); 76 | assertNull(response.get("message")); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/pages/AbstractBasePage.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution.pages; 2 | 3 | import org.openqa.selenium.remote.RemoteWebDriver; 4 | import org.openqa.selenium.support.ui.WebDriverWait; 5 | 6 | import java.time.Duration; 7 | import java.util.Map; 8 | 9 | /** 10 | * All page objects inherit from the base page. 11 | */ 12 | public abstract class AbstractBasePage { 13 | protected final RemoteWebDriver driver; 14 | 15 | public RemoteWebDriver getDriver() { 16 | return this.driver; 17 | } 18 | 19 | public WebDriverWait getWait() { 20 | return new WebDriverWait(getDriver(), Duration.ofSeconds(5)); 21 | } 22 | 23 | public AbstractBasePage(RemoteWebDriver driver) { 24 | this.driver = driver; 25 | } 26 | 27 | /** 28 | * Executes a visual test. 29 | */ 30 | public final void takeSnapshot() { 31 | //a JS command that Screener understands. The arg is the snapshot name 32 | driver.executeScript("/*@visual.snapshot*/", this.getClass().getSimpleName()); 33 | } 34 | 35 | public void visit() { 36 | driver.get("https://www.saucedemo.com/" + getPagePart()); 37 | } 38 | 39 | public abstract String getPagePart(); 40 | 41 | /** 42 | * Screener uses this JavaScript to provide results of visual snapshot. 43 | * 44 | * @return Map of visual results 45 | */ 46 | @SuppressWarnings("unchecked") 47 | public Map getVisualResults() { 48 | return (Map) driver.executeScript("/*@visual.end*/"); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/pages/CheckoutCompletePage.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution.pages; 2 | 3 | import com.saucedemo.solution.pages.AbstractBasePage; 4 | import org.openqa.selenium.By; 5 | import org.openqa.selenium.remote.RemoteWebDriver; 6 | import org.openqa.selenium.support.ui.ExpectedConditions; 7 | 8 | public class CheckoutCompletePage extends AbstractBasePage { 9 | public CheckoutCompletePage(RemoteWebDriver driver) { 10 | super(driver); 11 | } 12 | 13 | @Override 14 | public String getPagePart() { 15 | return "/checkout-complete.html"; 16 | } 17 | 18 | public boolean isDisplayed() { 19 | return getWait().until( 20 | ExpectedConditions.visibilityOfElementLocated(By.cssSelector("#back-to-products"))). 21 | isDisplayed(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/pages/CheckoutOverviewPage.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution.pages; 2 | 3 | import com.saucedemo.solution.pages.AbstractBasePage; 4 | import org.openqa.selenium.By; 5 | import org.openqa.selenium.remote.RemoteWebDriver; 6 | 7 | public class CheckoutOverviewPage extends AbstractBasePage { 8 | public CheckoutOverviewPage(RemoteWebDriver driver) { 9 | super(driver); 10 | } 11 | 12 | @Override 13 | public String getPagePart() { 14 | return "checkout-step-two.html"; 15 | } 16 | 17 | public void finish() { 18 | getDriver().findElement(By.cssSelector("#finish")).click(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/pages/CheckoutStepOnePage.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.remote.RemoteWebDriver; 5 | 6 | /** 7 | * Page Object for Checkout Step One. 8 | */ 9 | public class CheckoutStepOnePage extends AbstractBasePage { 10 | public CheckoutStepOnePage(RemoteWebDriver driver) { 11 | super(driver); 12 | } 13 | 14 | @Override 15 | public String getPagePart() { 16 | return "checkout-step-one.html"; 17 | } 18 | 19 | public void enterPersonalDetails() { 20 | getDriver().findElement(By.cssSelector("#first-name")).sendKeys("test"); 21 | getDriver().findElement(By.cssSelector("#last-name")).sendKeys("user"); 22 | getDriver().findElement(By.cssSelector("#postal-code")).sendKeys("12345"); 23 | getDriver().findElement(By.cssSelector("#continue")).click(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/pages/LoginPage.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.remote.RemoteWebDriver; 6 | import org.openqa.selenium.support.ui.ExpectedConditions; 7 | import org.openqa.selenium.support.ui.WebDriverWait; 8 | 9 | import java.time.Duration; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | /** 14 | * Page Object for Login page. 15 | */ 16 | public class LoginPage extends AbstractBasePage { 17 | private final By usernameFieldLocator = By.id("user-name"); 18 | private final By passwordFieldLocator = By.id("password"); 19 | private final By submitButtonLocator = By.id("login-button"); 20 | 21 | public LoginPage(RemoteWebDriver driver) { 22 | super(driver); 23 | } 24 | 25 | @Override 26 | public String getPagePart() { 27 | return ""; 28 | } 29 | 30 | /** 31 | * Log in on page. 32 | * 33 | * @param userName the name of the user to log in 34 | */ 35 | public void login(String userName) { 36 | //Create an instance of a Selenium explicit wait to dynamically wait for an element 37 | WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5)); 38 | //wait for the user name field to be visible and store that element into a variable 39 | wait.until((driver) -> driver.findElement(usernameFieldLocator).isDisplayed()); 40 | 41 | WebElement userNameField = driver.findElement(usernameFieldLocator); 42 | WebElement passwordField = driver.findElement(passwordFieldLocator); 43 | WebElement submitButton = driver.findElement(submitButtonLocator); 44 | 45 | userNameField.sendKeys(userName); 46 | passwordField.sendKeys("secret_sauce"); 47 | submitButton.click(); 48 | } 49 | 50 | /** 51 | * How long it takes to load the page. 52 | * 53 | * @return duration of time to load the page 54 | */ 55 | @SuppressWarnings("unchecked") 56 | public Integer getPageLoadTime() { 57 | HashMap metrics = new HashMap<>(); 58 | metrics.put("type", "sauce:performance"); 59 | Map perfMetrics = (Map) driver.executeScript("sauce:log", metrics); 60 | return Integer.parseInt(perfMetrics.get("load").toString()); 61 | } 62 | 63 | public boolean isDisplayed() { 64 | WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5)); 65 | return 66 | wait.until( 67 | ExpectedConditions.visibilityOfElementLocated(usernameFieldLocator)).isDisplayed(); 68 | } 69 | 70 | public void visit() { 71 | getDriver().navigate().to("https://www.saucedemo.com"); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/pages/ProductsPage.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.remote.RemoteWebDriver; 5 | import org.openqa.selenium.support.ui.ExpectedConditions; 6 | import org.openqa.selenium.support.ui.WebDriverWait; 7 | 8 | import java.sql.Time; 9 | import java.time.Duration; 10 | 11 | /** 12 | * Page Object representing Products page. 13 | */ 14 | public class ProductsPage extends AbstractBasePage { 15 | 16 | public ProductsPage(RemoteWebDriver driver) { 17 | super(driver); 18 | } 19 | 20 | @Override 21 | public String getPagePart() { 22 | return "inventory.html"; 23 | } 24 | 25 | public boolean isDisplayed() { 26 | //new Selenium 4 constructor of WebDriverWait() 27 | WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5)); 28 | By userNameFieldLocator = By.id("inventory_container"); 29 | return 30 | wait.until( 31 | ExpectedConditions.visibilityOfElementLocated(userNameFieldLocator)).isDisplayed(); 32 | } 33 | 34 | public void addAnyProductToCart() { 35 | By userNameFieldLocator = By.cssSelector("#add-to-cart-sauce-labs-backpack"); 36 | getWait().until( 37 | ExpectedConditions.visibilityOfElementLocated(userNameFieldLocator)).click(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /workshop/src/test/java/com/saucedemo/solution/pages/ShoppingCartPage.java: -------------------------------------------------------------------------------- 1 | package com.saucedemo.solution.pages; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.NoSuchElementException; 5 | import org.openqa.selenium.WebElement; 6 | import org.openqa.selenium.remote.RemoteWebDriver; 7 | 8 | /** 9 | * Page Object representing shopping cart page. 10 | */ 11 | public class ShoppingCartPage extends AbstractBasePage { 12 | public ShoppingCartPage(RemoteWebDriver driver) { 13 | super(driver); 14 | } 15 | 16 | @Override 17 | public String getPagePart() { 18 | return "cart.html"; 19 | } 20 | 21 | public int getItemsCount() { 22 | WebElement itemCounter; 23 | try{ 24 | itemCounter = driver.findElement(By.cssSelector(".shopping_cart_badge")); 25 | return Integer.parseInt(itemCounter.getText()); 26 | } 27 | catch (NoSuchElementException e) 28 | { 29 | return 0; 30 | } 31 | } 32 | } 33 | --------------------------------------------------------------------------------