From 17fda388df955801039bdb6e08e0afa58b05c5ea Mon Sep 17 00:00:00 2001 From: James Tacker Date: Mon, 11 Mar 2019 09:17:23 -0700 Subject: [PATCH] Updated Exercise guide, code sample --- exercise-guides/exercise1.md | 144 +++++++++++++++- exercise-guides/exercise2.md | 160 +++++++----------- exercise-guides/exercise3.md | 108 +++++------- exercise-guides/exercise4.md | 91 ++++++---- exercise-guides/exercise5.md | 114 ++++--------- exercise-guides/exercise6.md | 108 ++++++------ exercise-guides/images/100-parallel.png | Bin 0 -> 13334 bytes exercise-guides/images/last-15-mins.png | Bin 0 -> 7152 bytes .../images/test-and-build-stats.png | Bin 0 -> 27454 bytes exercise-guides/images/test-trends.png | Bin 0 -> 28056 bytes .../optional-cross-browser-parallelization.md | 86 ++++++++++ .../optional/optional-explicit-waits.md | 43 +++++ pom.xml | 2 +- 13 files changed, 516 insertions(+), 340 deletions(-) create mode 100644 exercise-guides/images/100-parallel.png create mode 100644 exercise-guides/images/last-15-mins.png create mode 100644 exercise-guides/images/test-and-build-stats.png create mode 100644 exercise-guides/images/test-trends.png create mode 100644 exercise-guides/optional/optional-cross-browser-parallelization.md create mode 100644 exercise-guides/optional/optional-explicit-waits.md diff --git a/exercise-guides/exercise1.md b/exercise-guides/exercise1.md index 1de375c..5e22077 100644 --- a/exercise-guides/exercise1.md +++ b/exercise-guides/exercise1.md @@ -1,6 +1,6 @@ # Exercise 1: Configure Automated Testing on SauceLabs -##Part One: Set Sauce Labs Account Credentials +## Part One: Set Sauce Labs Account Credentials 1. Checkout branch `01_set_sauce_credentials `. Open `src > test > java > exercises > FullJourneyTest.java` 2. Login to [www.saucelabs.com](https://www.sauceslabs.com), and navigate to the User Settings section of your account profile. @@ -18,7 +18,7 @@ ``` you should see the results appear in your Sauce Labs Test Dashboard -##Part Two: Set Environment Variables +## Part Two: Set Environment Variables 6. Next, modify the `sauceUserName` and `sauceAccessKey` variables to use Environment Variables: @@ -40,6 +40,7 @@ ``` > To set an environment variables permanently in Windows, you must append it to the `PATH` variable. > Go to "Control Panel > System > Windows version > Advanced System Settings > Environment Variables > System Variables > Edit > New. Then enter the "Name" and "Value" + 9. Test the environment variables ###### Mac OSX: ``` @@ -51,16 +52,145 @@ echo %SAUCE_USERNAME% echo %SAUCE_ACCESS_KEY% ``` +10. Run your test using Maven + ``` + mvn test + ``` + You should see the following build info appear (after sometime) in your console: - > To refresh a bash shell if you don't see the values run any of the following commands: + ![Successful Test Build Info](images/ex1-test-build.png) + > ** Warning:** + > If you have problems propogating your envirnoment variables into IntelliJ, try refreshing your by running any of the following commands: > * `$ source ~/.bashrc` > * `$ source ~/.bash_profile` > * `$ source /etc/profile` - 8. Run your test using Maven + > Or append the details to your `.bash_profile` to set them globally + > ``` + > vim ~/.bash_profile + > export SAUCE_USERNAME="xxx" + > export SAUCE_ACCESS_KEY="XXXXXXX-XXXX-XXXX-XXXXXXXXXXX" + > launchctl setenv SAUCE_USERNAME $SAUCE_USERNAME + > launchctl setenv SAUCE_ACCESS_KEY $SAUCE_ACCESS_KEY + > ``` + +
+ +## Part Three: Abstract Test Details: +1. In `src/test/java/exercises/` create a new class called `LoginFeatureTest`. +2. Create a new class method with the following: ``` - mvn test + public class LoginFeatureTest { + protected WebDriver driver; + @Test + public void ShouldBeAbleToLogin(Method method) + throws MalformedURLException + { + + } + } ``` - You should see the following build info appear (after sometime) in your console: +3. In `FullJourneyTest`, copy everything from: - ![Successful Test Build Info](images/ex1-test-build.png) \ No newline at end of file + `Line 21` + ``` + // Input your SauceLabs Credentials + ``` + to `Line 87`: + ``` + driver.findElement(By.cssSelector(loginBtn)).click(); + ``` + and paste it into the `LoginFeatureTest` class method: `ShouldBeAbleToLogin` +4. Delete unecessary element locators such as: + ``` + String backpack = "div:nth-child(1) > div.pricebar > button"; + String jacket = "div:nth-child(4) > div.pricebar > button"; + String cart = "#shopping_cart_container"; + String rmvBtn = "div:nth-child(4) > div.cart_item_label > div.item_pricebar > button"; + String continueShopping = "a.cart_cancel_link"; + ... + ``` +5. Add this `Assertion` at the end of the test: + ``` + Assert.assertEquals("https://www.saucedemo.com/inventory.html", driver.getCurrentUrl()); + ``` +6. Run the test: + ``` + mvn test -Dtest=LoginFeatureTest + ``` +7. Next we need to create an **`@AfterMethod` to send the test results to Sauce Labs and add a `@BeforeMethod` that takes care of the driver instantiation before we run our test: + * ``` + @AfterMethod + public void teardown(ITestResult result) { + ((JavascriptExecutor)driver).executeScript( + "sauce:job-result=" + (result.isSuccess() ? "passed" : "failed")); + driver.quit(); + } + ``` + * ``` + @BeforeMethod + public void setUp(Method method) throws MalformedURLException + { + // Input your SauceLabs Credentials + String sauceUsername = System.getenv("SAUCE_USERNAME"); + String sauceAccessKey = System.getenv("SAUCE_ACCESS_KEY"); + + MutableCapabilities capabilities = new MutableCapabilities(); + + //sets browser to Safari + capabilities.setCapability("browserName", "Safari"); + + //sets operating system to macOS version 10.13 + capabilities.setCapability("platform", "macOS 10.13"); + + //sets the browser version to 11.1 + capabilities.setCapability("version", "11.1"); + + //sets your test case name so that it shows up in Sauce Labs + capabilities.setCapability("name", method.getName()); + capabilities.setCapability("username", sauceUsername); + capabilities.setCapability("accessKey", sauceAccessKey); + + //instantiates a remote WebDriver object with your desired capabilities + driver = new RemoteWebDriver(new URL("https://ondemand.saucelabs.com/wd/hub"), capabilities); + } + ``` + Your test class should now look like this: + ``` + @Test + public void ShouldBeAbleToLogin() { + + //navigate to the url of the Sauce Labs Sample app + driver.navigate().to("https://www.saucedemo.com"); + + // Ignore the following selectors + String username = "standard_user"; + String password = "secret_sauce"; + String userField = "[data-test='username']"; + String passField = "[data-test='password']"; + String loginBtn = "[value='LOGIN']"; + + // wait 5 seconds + driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); + + // send username keystrokes + driver.findElement(By.cssSelector(userField)).sendKeys(username); + + // send password keystrokes + driver.findElement(By.cssSelector(passField)).sendKeys(password); + + // click login button to submit keystrokes + driver.findElement(By.cssSelector(loginBtn)).click(); + + // assert that the next page opened + Assert.assertEquals("https://www.saucedemo.com/inventory.html", driver.getCurrentUrl()); + } + ``` +8. Run the final test of this exercise: + ``` + mvn test -Dtest=LoginFeatureTest + ``` +9. Use `git stash` or `git commit` to discard or save your changes. Checkout the next branch to proceed to the next exercise + ``` + git checkout 02_page_objects + ``` \ No newline at end of file diff --git a/exercise-guides/exercise2.md b/exercise-guides/exercise2.md index d6f81a4..8cc1eb5 100644 --- a/exercise-guides/exercise2.md +++ b/exercise-guides/exercise2.md @@ -1,111 +1,81 @@ # Exercise 2: Create Page Objects -## Part One: Create Test Methods -1. Checkout branch `02_create_page_objects`. Open `src > test > java > exercises > FullJourneyTest.java` -2. Create a new package in **`src > test > java`** called **`pages`**. -3. In the **`pages`** package, create a new class called **`LogInPage`** -4. Instantiate the `LogInPage` object: - ``` - public static LogInPage visit(WebDriver driver) { - LogInPage page = new LogInPage(driver); +## Part One: Create a `LoginPage` +1. Checkout branch `02_create_page_objects`. +3. In the **`pages`** package, navigate to the class called **`LoginPage`** +4. Create a `visit` method in the `LoginPage` object: + ``` + public LoginPage visit() { driver.get("https://www.saucedemo.com"); - return page; - } - - public LogInPage(WebDriver driver) { - this.driver = driver; + return this; } ``` -5. Identify any repeatable test actions and create a method for each action. For example, a method for entering user information into a contact form could look like this: +5. Create the constructor for the page object: ``` - public void signInSuccessfully() { - logIn(driver); + public LoginPage(WebDriver driver) { + this.driver = driver; } - - private void logIn(WebDriver driver) { - String username = "standard_user"; - String password = "secret_sauce"; - driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); - driver.findElement(userField).sendKeys(username); - driver.findElement(passwordField).sendKeys(password); - driver.findElement(loginButton).click(); - } - ``` -6. Add the locators as private variables, for example: - ``` - private By userField = By.cssSelector( - "[placeholder = 'Username']"); - - private By passwordField = By.cssSelector( - "[placeholder = 'Password']"); - - private By loginButton = By.className( - "login-button"); ``` +6. Open **`LoginFeatureTest`** and import the **`LoginPage`** changes to replace **`driver.navigate().to("https://www.saucedemo.com")`** For Example: + * Before + ``` + driver.navigate().to("https://www.saucedemo.com"); + ``` + * After + ``` + LoginPage LoginPage = new LoginPage(driver); + LoginPage.visit(); + ``` + +7. Run a `mvn test` command to see if the test executes successfully: + ``` + mvn test -Dtest=LoginFeatureTest + ```
-## Part Two: Create a `LogInTest` Class -1. Open **`LogInTest`** and add the following `@BeforeMethod` and `@AfterMethod` classes: - In **`LogInTest`** add the following methods: - ``` - public class LogInTest { - protected WebDriver driver; - @BeforeMethod - public void setup(Method method) throws MalformedURLException { - String username = System.getenv("SAUCE_USERNAME"); - String accessKey = System.getenv("SAUCE_ACCESS_KEY"); - String methodName = method.getName(); - - ChromeOptions chromeOpts = new ChromeOptions(); - chromeOpts.setExperimentalOption("w3c", true); - - MutableCapabilities sauceOpts = new MutableCapabilities(); - sauceOpts.setCapability("username", username); - sauceOpts.setCapability("accessKey", accessKey); - sauceOpts.setCapability("name", methodName); - sauceOpts.setCapability("seleniumVersion", "3.141.59"); - sauceOpts.setCapability("name", methodName); - sauceOpts.setCapability("build", "saucecon19-best-practices"); - sauceOpts.setCapability("tags", "['best-practices', 'saucecon19']"); - - MutableCapabilities caps = new MutableCapabilities(); - caps.setCapability(ChromeOptions.CAPABILITY, chromeOpts); - caps.setCapability("sauce:options", sauceOpts); - caps.setCapability("browserName", "googlechrome"); - caps.setCapability("browserVersion", "71.0"); - caps.setCapability("platformName", "windows 10"); - - String sauceUrl = "https://ondemand.saucelabs.com:443/wd/hub"; - URL url = new URL(sauceUrl); - driver = new RemoteWebDriver(url, caps); - } - ... +## Part Two: Create `login()` Class Method +1. Open **`LoginPage`** and create a new class method called `login()`. This method will return a new page object that represents the next page in the journey (i.e. `InventoryPage`) +2. Add the **`LoginPage.visit()`** action in place of **`driver.navigate().to("https://www.saucedemo.com")`** The method will also expect some String data for the credentials (`username` and `password`) input. For Example: + ``` + public InventoryPage login(String username, String password) + { - @AfterMethod - public void teardown(ITestResult result) { - ((JavascriptExecutor)driver).executeScript("sauce:job-result=" + (result.isSuccess() ? "passed" : "failed")); - driver.quit(); - } } ``` -2. Import the **`LogInPage`** and reference the `@Test` class method. For example: - ``` - import pages.LogInPage; - ... - public class LogInTest { - - @Tag(name = "logInSuccessfully()") - @Test - /** Tests for a successful login **/ - public void logInSuccessfully(Method method) { - LogInPage logInPage = LogInPage.visit(driver); - logInPage.signInSuccessfully(); - Assert.assertEquals(driver.getCurrentUrl(), "https://www.saucedemo.com/inventory.html"); - } - ... +3. In `LoginFeatureTest` copy line 55-73 and paste it into `LoginPage`. The `login()` method should now look like the following: ``` -
+ public InventoryPage login(String username, String password) + { + String userField = "[data-test='username']"; + String passField = "[data-test='password']"; + String loginBtn = "[value='LOGIN']"; -## Part Three: Test the Results -1. Save all and run **`mvn test`** in your terminal. We now have duplication in not only our tests, but potentially our future page objects. Next we will create a `BasePage` object class and a `BaseTest` object class. \ No newline at end of file + // send username keystrokes + driver.findElement(By.cssSelector(userField)) + .sendKeys(username); + + // send password keystrokes + driver.findElement(By.cssSelector(passField)) + .sendKeys(password); + + // click login button to submit keystrokes + driver.findElement(By.cssSelector(loginBtn)).click(); + return new InventoryPage(driver); + } + ``` + +4. Add the following to the `LoginFeatureTest`, add the following to the `ShouldBeAbleToLogin` method: + ``` + String username = "standard_user"; + String password = "secret_sauce"; + loginPage.login(username, password); + ``` +5. After the change, save and run `mvn test` to ensure the test still runs. + ``` + mvn test -Dtest=LoginFeatureTest + ``` +6. Use `git stash` or `git commit` to discard or save your changes. Checkout the next branch to proceed to the next exercise + ``` + git checkout 03_remove_duplication + ``` \ No newline at end of file diff --git a/exercise-guides/exercise3.md b/exercise-guides/exercise3.md index 70223b3..333e768 100644 --- a/exercise-guides/exercise3.md +++ b/exercise-guides/exercise3.md @@ -1,76 +1,48 @@ # Exercise 3: Remove Duplication -## Part One: Identify Duplicate Test Code 1. Checkout branch `03_remove_duplication`. -2. Open the new page object called **`InventoryPage`** in `src > test > java > pages`. -3. Note the four class methods: - * `addOneItem`: adds one item to the shopping cart - * `addTwoItem`: adds two items to the shopping cart - * `itemCount`: counts items in the shopping cart - * `checkout`: proceeds to the checkout page. -4. Open **`InventoryTest`** in `src > test > java > exercises`. -5. There is now duplicate code that also exists in **`LogInTest`**, specifically the `setup()` and `teardown()` methods. Next, we will create a `BaseTest` class that executes these prerequisite and postrequisite test actions. - -
- -## Part Two: Create a `BaseTest` Class -1. Create a class called **`BaseTest`** in `src > test > java > exercises`. -2. Move the **`setup()`** and **`teardown()`** methods that exist in **`LogInTest`** and **`InventoryTest`** into **`BaseTest`**. -3. Back in **`LogInTest`** (and **`InventoryTest`**) add the following: +2. Open the new test class called **`CheckoutFeatureTest`** in `src > test > java > exercises`. +3. There is duplicate code that exists in **`LoginFeatureTest`**, specifically the `setup()` and `teardown()` methods. +4. Create a `BaseTest` class that executes these prerequisite and postrequisite test actions. +5. Remove the `setup()` and `teardown()` methods from `LoginFeatureTest` and `CheckoutFeatureTest` and place them into `BaseTest` +6. Back in `LoginFeatureTest` and `CheckoutFeatureTest`, extend `BaseTest` like so: ``` - public class LogInTest extends BaseTest { + public class LoginFeatureTest extends BaseTest { ... } ``` -4. Remove the **`setup()`** and **`teardown()`** methods in **`LogInTest`** and **`InventoryTest`**. - -
+7. Delete `FullJourneyTest` and test the changes: + ``` + mvn test + ``` +8. If both tests run fine, use `git commit` or `git stash`, then checkout the next branch to proceed to the next exercise: -## Part Three: Create a `BasePage` Object -1. Identify duplication in any method that uses common Selenium **`driver`** commands such as the following in **`LogInPage`** and **`InventoryPage`**: - * `.findElement()` - * `.click()` - * `.sendKeys()` -2. Create a class called **`BasePage`** in `src > test > java > pages`. -3. Create new class methods in `BasePage` that represent common Selenium **`driver`** commands, for example: - - * Find an element on a page - ``` - WebElement findBy(By locator) { - return driver.findElement(locator); - } - ``` - * Click on an element - ``` - void click(By locator) { - driver.manage().timeouts().implicitlyWait(5, - TimeUnit.SECONDS) ; - findBy(locator).click(); - } - ``` - * Submit keystrokes - ``` - void sendKeys(By locator, String text) { - driver.manage().timeouts().implicitlyWait(5, - TimeUnit.SECONDS) ; - findBy(locator).sendKeys(text); - } - ``` -4. Use the class methods in **`BasePage`** to refactor duplicate Selenium **`driver`** commands in both **`LogInPage`** and **`InventoryPage`**, for example: - * Before - ``` - void logIn(By locator){ - driver.findElement(userField).sendKeys(username); - driver.findElement(passField).sendKeys(password); - driver.findElement(logInButton).click(); - } - ``` - * After - ``` - void logIn(By locator){ - sendKeys(userField, username); - sendKeys(passField, password); - click(logInButton); - } - ``` -5. Checkout branch `04_explicit_waits` to see the complete examples \ No newline at end of file +## Part Two: Cross Browser Testing +Next we will update our `capabilities` in `BaseTest` to test using the latest version of Google Chrome, which means we have to modify our code a bit to comply with the new `W3C` protocol. +1. Add a second `MutableCapabilities` object in the `setup()` method with the following details: + ``` + MutableCapabilities sauceOpts = new MutableCapabilities(); + sauceOpts.setCapability("username", sauceUsername); + sauceOpts.setCapability("accessKey", sauceAccessKey); + sauceOpts.setCapability("name", method.getName()); + sauceOpts.setCapability("seleniumVersion", "3.141.59"); + sauceOpts.setCapability("build", "saucecon19-best-practices"); + sauceOpts.setCapability("tags", "['best-practices', 'saucecon19']"); + ``` +2. Next, modify the existing `MutableCapabilities` object called `capabilities with the following: + ``` + MutableCapabilities capabilities = new MutableCapabilities(); + capabilities.setCapability(ChromeOptions.CAPABILITY, chromeOpts); + capabilities.setCapability("sauce:options", sauceOpts); + capabilities.setCapability("browserName", "googlechrome"); + capabilities.setCapability("browserVersion", "71.0"); + capabilities.setCapability("platformName", "windows 10"); + ``` +3. Run a test to ensure the build passes + ``` + mvn test + ``` +4. Use `git stash` or `git commit` to discard or save your changes. Checkout the next branch to proceed to the next exercise + ``` + git checkout 04_configure_atomic_tests + ``` \ No newline at end of file diff --git a/exercise-guides/exercise4.md b/exercise-guides/exercise4.md index 7804679..bb11de2 100644 --- a/exercise-guides/exercise4.md +++ b/exercise-guides/exercise4.md @@ -1,43 +1,70 @@ -# Exercise 4: Implement Explicit Waits -## Part One: Identify Implicit Waits -1. Checkout branch `04_explicit_waits `. -2. Open **`LogInPage`** and navigate to the **`logIn`** method. -3. As it stands, our wait strategy is inefficient because of: **`implicitlyWait`**. This means our session hangs for a broad 5 seconds after the preceding command and adds unecessary overhead to our test execution. A better strategy is to wait until a specific element renders on the page, then proceed to the next command. In other words, our wait strategy should transform as follows: - * Before +# Exercise 4: Configure Atomic Tests + +## Part One: Modify `ConfirmationPage` +1. Checkout the branch `04_configure_atomic_tests`. +2. Open `ConfirmationPage` in `src > test > java > pages`. +3. Add the following class methods: ``` - driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); + public void visit() { + driver.get("https://www.saucedemo.com/checkout-step-two.html"); + } ``` - - * After ``` - WebDriverWait wait = new WebDriverWait(driver, 5); - wait.until(ExpectedConditions - .presenceOfElementLocated(locator)); - + public Boolean hasItems() { + String cartBadge = "shopping_cart_badge"; + return Integer.parseInt(driver.findElement(By.className(cartBadge)).getText()) > 0; + } ``` - However this still adds a bit of duplication. The best strategy is to add the **`WebDriverWait`** command to the **`BasePage`** class to avoid further duplication. -4. In the **`BasePage`** object, create a method that waits for specific elements to render based on a relevant `locator`. Below is an example of an explicit wait method: ``` - private void waitForElement(By locator) { - WebDriverWait wait = new WebDriverWait(driver, 5); - wait.until(ExpectedConditions. - presenceOfElementLocated(locator)); + public CheckoutCompletePage finishCheckout() { + String checkoutLink = "cart_checkout_link"; + driver.findElement(By.className(checkoutLink)).click(); + return new CheckoutCompletePage; } ``` - > Note: the `ExpectedConditions` method must be imported using: `import org.openqa.selenium.support.ui.ExpectedConditions` +4. Open **`CheckoutFeatureTest`** located in `src > test > java > exercises`. +5. You'll notice that the **`ShouldBeAbleToCheckout()`** class method steps through many pages to get to the checkout function. The existing test flow works like this: + * User logs in + * Adds some items to the cart + * Clicks the cart icon to proceed to checkout + +This approach is under-optimized because our tests shouldn't rely on the assertions of other tests and it's unecessary to travel through each individual page to reach that feature. Therefore if we're only testing features on a specific page, we can modify the page state using the **`JavaScriptExecutor`**. + +
-2. In **`BasePage`**, check each method and refactor the following wherever it exists: - * Before +## Part Two: Implement the `JavascriptExecutor` to Bypass Pages +1. Go back to **`ConfirmationPage`** and add the following class method: + ``` + public void setCartState() { + driver.navigate().refresh(); + } ``` - driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); +2. In **`setCartState()`** add the following **`JavaScriptExecutor`** command to bypass logging in through the **`LoginPage`** object: ``` - * After + ((JavascriptExecutor)driver).executeScript("window.sessionStorage.setItem('standard-username', 'standard-user')"); ``` - waitForElement(locator); - ``` - -3. Ensure each command replacement has a relevant `locator` -4. Save and run `mvn test` -5. Checkout branch `05_configure_atomic_tests` to see the answers. - -
\ No newline at end of file +3. Then add another **`JavaScriptExecutor`** command to bypass adding items to the cart through the **`InventoryPage`** object: + ``` + ((JavascriptExecutor)driver).executeScript("window.sessionStorage.setItem('cart-contents', '[4,1]')"); + ``` +4. In **`CheckoutFeatureTest`**, delete the existing commands and add the following: + ``` + @Test + public void ShouldBeAbleToCheckoutWithItems() { + // wait 5 seconds + driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS) ; + ConfirmationPage confirmationPage = new ConfirmationPage(driver); + confirmationPage.visit(); + confirmationPage.setCartState(); + confirmationPage.checkout(); + Assert.assertTrue(confirmationPage.hasItems()); + } + ``` +5. Save all and run the following command to ensure the build passes: + ``` + mvn test + ``` +6. Use `git stash` or `git commit` to discard or save your changes. Checkout the next branch to proceed to the next exercise + ``` + git checkout 05_create_base_page + ``` \ No newline at end of file diff --git a/exercise-guides/exercise5.md b/exercise-guides/exercise5.md index acc7638..aa6af25 100644 --- a/exercise-guides/exercise5.md +++ b/exercise-guides/exercise5.md @@ -1,89 +1,41 @@ -# Exercise 5: Configure Atomic Tests -## Part One: Identify Inefficient Tests -1. Checkout branch `05_configure_atomic_tests`. -2. Open **`CartTest`** located in `src > test > java > exercises`. -3. You'll notice that the **`confirmCheckout()`** method instantiates several page objects and depends on several assertions before reaching the actual page under test (**`CartPage`**): - ``` - @Tag(name = "confirmCheckout()") - @Test - //** Tests checkout process **//* - public void confirmCheckout(Method method) { - LogInPage logInPage = LogInPage.visit(driver); - logInPage.signIn(User.validUser()); - InventoryPage inventoryPage = InventoryPage.visit(driver); - Assert.assertTrue(inventoryPage.isSignedIn()); - inventoryPage.addAllItems(); - Assert.assertFalse(inventoryPage.emptyCart()); - CartPage cartPage = CartPage.visit(driver); - cartPage.checkout(); - Assert.assertTrue(cartPage.hasItems()); - } - ``` - This approach is under-optimized. In general, our tests shouldn't rely on the assertions of other tests and it's unecessary to travel through the entire customer experience unless you're conducting end-to-end functionality testing. Therefore if we're only testing features on a specific page, we should modify the front-end state of the page using the **`JavaScriptExecutor`**. - -
- -## Part Two: Leverage the `JavascriptExecutor` to Bypass Pages -1. Open the page object called **`CartPage`** in `src > test > java > pages` and add the following method: - ``` - public void setCartState() { - driver.navigate().refresh(); - } - ``` -2. In **`setCartState()`** add the following **`JavaScriptExecutor`** command to bypass the **`LogInPage`** status needed to checkout: - ``` - ((JavascriptExecutor)driver) - .executeScript("window - .sessionStorage - .setItem('standard-username', 'standard-user')"); - ``` -3. Then add the **`JavaScriptExecutor`** to bypass adding items to the cart in the **`InventoryPage`**: - ``` - ((JavascriptExecutor)driver) - .executeScript("window - .sessionStorage - .setItem('cart-contents', '[4,1]')"); - ``` -4. In **`CartTest`**, refactor the **`confirmCheckout()`** method to the following: - ``` - @Tag(name = "checkoutTest()") - @Test - public void checkoutTest() { - CartPage cartPage = CartPage.visit(driver); - cartPage.setCartState(); - cartPage.checkout(); - Assert.assertTrue(cartPage.hasItems()); +# Exercise 5: Create a Base Page Object + +1. Checkout the branch `05_create_base_page`. +2. In `src > test > java > pages`, create a new class called `BasePage`. +3. Remove the following duplication: + ``` + //TODO notice the duplication + private final WebDriver driver; + ... + //TODO notice the duplication + this.driver = driver; + ... + driver.navigate().to("some-url"); + ... + ``` +In the following classes: + * `CheckoutCompletePage` + * `ConfirmationPage` + * `InventoryPage` + * `LoginPage` +4. Add the following in `BasePage`: + ``` + public class BasePage { + public final WebDriver driver; + public BasePage(WebDriver driver) { + this.driver = driver; + } } ``` -5. Save all and run the following command in your terminal +5. Extend the `BasePage` object in the other page objects, for example: ``` - mvn test -Dtest=CartTest + public class CheckoutCompletePage extends BasePage { ``` -6. Final Results - * Before: +6. Save all and run the following command to ensure the build passes: ``` - @Tag(name = "confirmCheckout()") - @Test - public void confirmCheckout(Method method) { - LogInPage logInPage = LogInPage.visit(driver); - logInPage.signIn(User.validUser()); - InventoryPage inventoryPage = InventoryPage.visit(driver); - Assert.assertTrue(inventoryPage.isSignedIn()); - inventoryPage.addAllItems(); - Assert.assertFalse(inventoryPage.emptyCart()); - CartPage cartPage = CartPage.visit(driver); - cartPage.checkout(); - Assert.assertTrue(cartPage.hasItems()); - } + mvn test ``` - * After: +7. Use `git stash` or `git commit` to discard or save your changes. Checkout the next branch to proceed to the next exercise ``` - @Tag(name = "checkoutTest()") - @Test - public void checkoutTest() { - CartPage cartPage = CartPage.visit(driver); - cartPage.setCartState(); - cartPage.checkout(); - Assert.assertTrue(cartPage.hasItems()); - } + git checkout 06_test_parallelization ``` \ No newline at end of file diff --git a/exercise-guides/exercise6.md b/exercise-guides/exercise6.md index fc7ea16..5193c7c 100644 --- a/exercise-guides/exercise6.md +++ b/exercise-guides/exercise6.md @@ -1,79 +1,75 @@ # Exercise 6: Test Code Parallelization -## Part One: Configure `DataProvider` +## Part One: Configure Parallelization in `pom.xml`: 1. Checkout the branch `06_test_parallelization` -2. In `BaseTest`, add the following `ThreadLocal` declarations: +2. Open up the `pom.xml` and modify the following `plugin` setting: + * Before: ``` - private ThreadLocal webDriver = new ThreadLocal(); - private ThreadLocal sessionId = new ThreadLocal(); + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + classes + 1 + false + ``` - -3. Add the following object array: - ``` - @DataProvider(name = "hardCodedBrowsers", parallel = true) - public static Object[][] sauceBrowserDataProvider(Method method) { - return new Object[][]{ - new Object[]{"MicrosoftEdge", "14.14393", "Windows 10"}, - new Object[]{"firefox", "49.0", "Windows 10"}, - new Object[]{"internet explorer", "11.0", "Windows 7"}, - new Object[]{"safari", "10.0", "OS X 10.11"}, - new Object[]{"chrome", "54.0", "OS X 10.10"}, - new Object[]{"firefox", "latest-1", "Windows 7"}, - }; - } - ``` -4. Create a **`WebDriver`** public method to return the current WebDriver in the thread: + * After: ``` - public WebDriver getWebDriver() { return webDriver.get(); + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + methods + 10 + false + ``` -5. Create a **`String`** public method to return the current SauceLabs session ID for the current thread: +3. In `BaseTest` change the `"build"` tag so that SauceLabs logs the tests as a different build, for example: + * Before: ``` - public String getSessionId() { return sessionId.get(); + sauceOpts.setCapability("build", "saucecon19-best-practices"); ``` -6. Create a new public method that constructs a `RemoteWebDriver` instance that uses the capabilities defined by the browser, version and os parameters defined in the current thread. + * After: ``` - protected void createDriver(String browser, String version, String os, String methodName) - throws MalformedURLException { + sauceOpts.setCapability("build", "saucecon19-best-practices-v0.0.1"); ``` - * `String browser` Represents the browser type. - * `String version` Represents the browser version. - * `String os` Represents the operating system. - * `methodName` Represents the name of the test case that we can use to identify the test on Sauce Labs. - * `MalformedURLException` throws if an error occurs parsing the url +4. Navigate to saucelabs.com and open the Analytics tab. Go to the **Trends** tab: -7. Set the `MutableCapabilities caps = new MutableCapabilities();` as follows: - ``` - caps.setCapability("sauce:options", sauceOpts); - caps.setCapability(CapabilityType.BROWSER_NAME, browser); - caps.setCapability(CapabilityType.VERSION, version); - caps.setCapability(CapabilityType.PLATFORM, os); - caps.setCapability("name", methodName); - ``` + ![Trends Tab](images/test-trends.png) -8. Launch the remote web browser and set it as the current thread, then set the current session ID. - ``` - webDriver.set(new RemoteWebDriver(new URL( - "https://ondemand.saucelabs.com/wd/hub"),caps)); - ``` +6. Change the **Time** parameter to **Last 15 mins** + + ![Last 15 Minutes](images/last-15-mins.png) + +7. Scroll down to the bottom to **Build and Test Statistics**: + ![Test and Build Statistics](images/test-and-build-stats.png) + +8. Your build efficiency should read somewhere around **semi-parallel (44%)**,. Go back to `pom.xml` and change the parallel execution to **`classes`** level, e.g.: ``` - String id = ((RemoteWebDriver) getWebDriver()).getSessionId().toString(); - sessionId.set(id); + org.apache.maven.plugins + maven-surefire-plugin + 2.22.1 + + classes + 10 + false + ``` -## Part Two: Enable Each `@Test` Method to Accept `DataProvider` -1. In both `LogInTest` and `InventoryTest` refactor each `@Test` method with the following parameters outlined in the `BaseTest` **`@DataProvider`** annotation. For example: +9. In `BaseTest` change the `"build"` tag again to compare the differences (make sure you also **"import Maven changes"** whenever changing the `pom.xml`: + * Before ``` - @Test(dataProvider = "hardCodedBrowsers") - public void logInSuccessfully(String browser, String version, String os, Method method) - throws MalformedURLException, InvalidElementStateException, UnexpectedException { + sauceOpts.setCapability("build", "saucecon19-best-practices-v0.0.1"); ``` -2. In each test method add the following code to instantiate a webdriver session from the current driver in the thread: + * After ``` - this.createDriver(browser, version, os, method.getName()); - WebDriver driver = this.getWebDriver(); + sauceOpts.setCapability("build", "saucecon19-best-practices-v0.0.2"); ``` -3. Save all and re-run your test: +10. Save all and re-run your tests: ``` mvn test ``` -4. Check your SauceLabs dashboard. You'll notice in the **Automated Tests** tab that each test now runs in parallel. \ No newline at end of file + the **Build and Test Statistics** tab should now show the current build runs as **parallelized (100%)** in the **Efficiency** tab: + + ![100% Parallel](images/100-parallel.png) \ No newline at end of file diff --git a/exercise-guides/images/100-parallel.png b/exercise-guides/images/100-parallel.png new file mode 100644 index 0000000000000000000000000000000000000000..dbef2749a41347b86917026672f10bddc7087008 GIT binary patch literal 13334 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 T?Irf#A4H0>YBJSQra}J$Z=OI# literal 0 HcmV?d00001 diff --git a/exercise-guides/images/test-and-build-stats.png b/exercise-guides/images/test-and-build-stats.png new file mode 100644 index 0000000000000000000000000000000000000000..2aafbad1585a95fdea5d2e55065ad096df81d5f5 GIT binary patch literal 27454 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 G^gjS;Sv?*A literal 0 HcmV?d00001 diff --git a/exercise-guides/images/test-trends.png b/exercise-guides/images/test-trends.png new file mode 100644 index 0000000000000000000000000000000000000000..b82021b602f993484e592974aad22ebb29bcfa08 GIT binary patch literal 28056 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 literal 0 HcmV?d00001 diff --git a/exercise-guides/optional/optional-cross-browser-parallelization.md b/exercise-guides/optional/optional-cross-browser-parallelization.md new file mode 100644 index 0000000..6e7f206 --- /dev/null +++ b/exercise-guides/optional/optional-cross-browser-parallelization.md @@ -0,0 +1,86 @@ +# Optional Exercise: Cross-Browser Parallelization + +## Part One: Configure `DataProvider` +2. In `BaseTest`, add the following `ThreadLocal` declarations: + ``` + private ThreadLocal webDriver = new ThreadLocal(); + private ThreadLocal sessionId = new ThreadLocal(); + ``` + +3. Add the following object array: + ``` + @DataProvider(name = "hardCodedBrowsers", parallel = true) + public static Object[][] sauceBrowserDataProvider(Method method) { + return new Object[][]{ + new Object[]{"MicrosoftEdge", "14.14393", "Windows 10"}, + new Object[]{"firefox", "49.0", "Windows 10"}, + new Object[]{"internet explorer", "11.0", "Windows 7"}, + new Object[]{"safari", "10.0", "OS X 10.11"}, + new Object[]{"chrome", "54.0", "OS X 10.10"}, + new Object[]{"firefox", "latest-1", "Windows 7"}, + }; + } + ``` +4. Create a **`WebDriver`** public method to return the current WebDriver in the thread: + ``` + public WebDriver getWebDriver() { return webDriver.get();} + ``` +5. Create a **`String`** public method to return the current SauceLabs session ID for the current thread: + ``` + public String getSessionId() { return sessionId.get();} + ``` +6. Create a new public method that constructs a `RemoteWebDriver` instance that uses the capabilities defined by the browser, version and os parameters defined in the current thread. + ``` + protected void createDriver(String browser, String version, String os, String methodName) + throws MalformedURLException { + ``` + * `String browser` Represents the browser type. + * `String version` Represents the browser version. + * `String os` Represents the operating system. + * `methodName` Represents the name of the test case that we can use to identify the test on Sauce Labs. + * `MalformedURLException` throws if an error occurs parsing the url + +7. Set the `MutableCapabilities caps = new MutableCapabilities();` as follows: + ``` + caps.setCapability("sauce:options", sauceOpts); + caps.setCapability(CapabilityType.BROWSER_NAME, browser); + caps.setCapability(CapabilityType.VERSION, version); + caps.setCapability(CapabilityType.PLATFORM, os); + caps.setCapability("name", methodName); + ``` + +8. Launch the remote web browser and set it as the current thread, then set the current session ID. + ``` + webDriver.set(new RemoteWebDriver(new URL( + "https://ondemand.saucelabs.com/wd/hub"),caps)); + ``` + + ``` + String id = ((RemoteWebDriver) getWebDriver()).getSessionId().toString(); + sessionId.set(id); + ``` +## Part Three: Enable Each `@Test` Method to Accept `DataProvider` +1. In both `LogInFeatureTest` and `CheckoutFeatureTest` refactor each `@Test` method with the following parameters outlined in the `BaseTest` **`@DataProvider`** annotation. For example: + ``` + @Test(dataProvider = "hardCodedBrowsers") + public void SHouldBeAbleToLogin(String browser, String version, String os, Method method) + throws MalformedURLException, InvalidElementStateException, UnexpectedException { + ``` +2. In each test method add the following code to instantiate a webdriver session from the current driver in the thread: + ``` + this.createDriver(browser, version, os, method.getName()); + WebDriver driver = this.getWebDriver(); + ``` +3. In `BaseTest` change the `"build"` tag again log in a different build: + * Before + ``` + sauceOpts.setCapability("build", "saucecon19-best-practices-v0.0.2"); + ``` + * After + ``` + sauceOpts.setCapability("build", "saucecon19-best-practices-v0.0.3"); + ``` +3. Save all and re-run your tests: + ``` + mvn test + ``` \ No newline at end of file diff --git a/exercise-guides/optional/optional-explicit-waits.md b/exercise-guides/optional/optional-explicit-waits.md new file mode 100644 index 0000000..1c02bca --- /dev/null +++ b/exercise-guides/optional/optional-explicit-waits.md @@ -0,0 +1,43 @@ +# Exercise 6: Implement Explicit Waits +## Part One: Identify Implicit Waits +1. Checkout branch `06_explicit_waits `. +2. Open **`LogInPage`** and navigate to the **`logIn`** method. +3. As it stands, our wait strategy is inefficient because of: **`implicitlyWait`**. This means our session hangs for a broad 5 seconds after the preceding command and adds unecessary overhead to our test execution. A better strategy is to wait until a specific element renders on the page, then proceed to the next command. In other words, our wait strategy should transform as follows: + * Before + ``` + driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); + ``` + + * After + ``` + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions + .presenceOfElementLocated(locator)); + + ``` + However this still adds a bit of duplication. The best strategy is to add the **`WebDriverWait`** command to the **`BasePage`** class to avoid further duplication. +4. In the **`BasePage`** object, create a method that waits for specific elements to render based on a relevant `locator`. Below is an example of an explicit wait method: + ``` + private void waitForElement(By locator) { + WebDriverWait wait = new WebDriverWait(driver, 5); + wait.until(ExpectedConditions. + presenceOfElementLocated(locator)); + } + ``` + > Note: the `ExpectedConditions` method must be imported using: `import org.openqa.selenium.support.ui.ExpectedConditions` + +2. In **`BasePage`**, check each method and refactor the following wherever it exists: + * Before + ``` + driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS); + ``` + * After + ``` + waitForElement(locator); + ``` + +3. Ensure each command replacement has a relevant `locator` +4. Save and run `mvn test` +5. Checkout branch `05_configure_atomic_tests` to see the answers. + +
\ No newline at end of file diff --git a/pom.xml b/pom.xml index b1a1fd3..858889b 100644 --- a/pom.xml +++ b/pom.xml @@ -71,7 +71,7 @@ 2.22.1 classes - 1 + 10 false