Creating AbstractTest classes to reuse our startup code
So where were we?
Oh yes, last week I was investigating JUnit 5 asserts and deciding that generally I don’t like them. It is safe to assume then that I will mainly be using Assertj for my assertions. We also got our first test passing in a way that won’t fail if I decide to change my WordPress theme again. One test is all very well, but what if I want to write many tests. As I demonstrated last week with the assertion tests, you can include many tests within a single class, by annotating each with the appropriate @Test annotation.
If you recall back to the first week, I amended the class with annotated setup() and teardown() methods to ensure that everything was cleaned up in the event of a test fail. Looking at that code, it is pretty clear that I would want to run it at the start and end of every WebDriver test I am likely to use. Adding extra test methods in this class are already covered, however if I wish to write a lot of tests covering an entire website, I am almost certainly not going to want to keep them all in a single class.
Rather than duplicating these methods in every test class, I want to create an AbstractTest class that all of my test classes can inherit from. In fact I want to investigate two alternatives. If you have tried the code from week one you will realise that probably the longest part of the test is in starting up the WebDriver and browser. Whilst I could dump a heap of soft assertions into a single method, this is not terribly clear in trying to identify problems. In general I will want each test to verify a single task.
In a typical static website, I can simply reload the homepage and I am back where I started, so another possibility is to reuse the same WebDriver, reload the Homepage and start my new test. There are limitations and on some sites, and especially Web-apps that maintain state, this can be dangerous, but I want to give it a try.
Step 1. Extracting an AbstractTest class.
The good news here is that with my current test class I have already isolated the setup and teardown code that I want to pull up to my Abstract test. I am going to create it in an infrastructure package so we get:
package tech.alexontest.poftutor.infrastructure; import org.junit.After; import org.junit.Before; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; /** * Abstract Test class that creates a new WebDriver for Each Test. * Supports JUnit 4 and JUnit 5 tests. */ abstract public class AbstractTest { private WebDriver driver; @Before @BeforeEach public void setup() { System.out.println("Preparing Driver"); //find the chromedriver and set its location in a system property ClassLoader classLoader = getClass().getClassLoader(); String path = classLoader.getResource("chromedriver.exe").getPath(); System.setProperty("webdriver.chrome.driver", path); ChromeOptions chromeOptions = new ChromeOptions(); chromeOptions.addArguments("--start-maximized"); driver = new ChromeDriver(chromeOptions); System.out.println("Driver Started"); } @After @AfterEach public void teardown() { driver.quit(); } protected WebDriver getDriver() { return driver; } }
Again this retains the Annotations so that I can run with JUnit 4 or JUnit 5 by altering the annotation in the inheriting test class. Note the addition of the getDriver() method here to pass down the created driver. That also cuts out most of the code from my Test class, and as the idea is to run multiple tests I will add a second test to check that the reported url is as requested.
package tech.alexontest.poftutor; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import tech.alexontest.poftutor.infrastructure.AbstractTest; import static org.assertj.core.api.Assertions.assertThat; public class DriverLaunchTest extends AbstractTest { private WebDriver driver; private String homePageURL = "https://alexanderontesting.com/"; @Test public void titleIsCorrect() { setupHomepage(); //confirm the title text is correct assertThat(driver.findElement(By.cssSelector(".site-title")).getText()) .isEqualToIgnoringCase("Alexander on Testing"); } @Test public void urlIsCorrect() { setupHomepage(); assertThat(driver.getCurrentUrl()) .isEqualToIgnoringCase(homePageURL); } private void setupHomepage() { driver = getDriver(); driver.get(homePageURL); } }
You will notice that I have pulled out the getting the homepage to a private method that I can call whenever I want to load the homepage. Running it gives me a nice pair of green ticks in a total of 20 seconds. Let’s add a couple more tests. One is to check that my site correctly redirects anyone on http to the https URL and also my first use of the WebDriver.findElements(…..) locator to return a List<WebElements>. Looking to the right you should see there are 5 widgets so again we can verify that they are all present and correct.
Again these run just fine, now 4 passing tests, but at 47 seconds it is already a pain to wait for the tests to run, especially as running WebDriver tests locally makes using your computer for anything else just about impossible.
Step 2. A “greener” AbstractTest
This is not something I have tried before: How easy is it to switch my Abstract test to re-use the same WebDriver and Browser for all the tests? So:
abstract public class AbstractSingleDriverTest
and this time we annotate the methods with the @BeforeClass / @AfterClass (JUnit4) and @BeforeAll / @AfterAll annotations and see what happens, still in JUnit 4.
Oops! A big fat fail, 2 red minus signs instead of 4 ticks and 2 exceptions telling me that setup() and teardown() methods should be static. OK lets try that then.
Hmm. Not good. This is starting to look messy. The method call concerned there is the one that picks up the position of the ChromeDriver executable from the resources folder. Now I could just create a static link, but then it wouldn’t work when someone checks out the code from my repo. Still one last thing to try. I recall reading that these methods are not static in JUnit 5. Perhaps this is where the new framework starts to show its worth.
It’s a bit of a pain as we have to re-enable our gradle plugin again and change the import in the test class, but it’s worth a try. Ouch! the support for the results is difficult. 4 tests but not run and only the message
Failures (1):
JUnit Jupiter:DriverLaunchTest
ClassSource [className = ‘tech.alexontest.poftutor.DriverLaunchTest’, filePosition = null]
=> org.junit.platform.commons.JUnitException: @BeforeAll method ‘public void tech.alexontest.poftutor.infrastructure.AbstractSingleDriverTest.setup()’ must be static unless the test class is annotated with @TestInstance(Lifecycle.PER_CLASS).
Test run finished after 123 ms
[ 3 containers found ]
[ 0 containers skipped ]
[ 3 containers started ]
[ 0 containers aborted ]
[ 2 containers successful ]
[ 1 containers failed ]
[ 4 tests found ]
[ 0 tests skipped ]
[ 0 tests started ]
[ 0 tests aborted ]
[ 0 tests successful ]
[ 0 tests failed ]
:junitPlatformTest FAILED
FAILURE: Build failed with an exception.
* What went wrong:
Execution failed for task ‘:junitPlatformTest’.
> Process ‘command ‘C:\Program Files\Java\jdk1.8.0_111\bin\java.exe” finished with non-zero exit value 1
* Try:
Run with –stacktrace option to get the stack trace. Run with –info or –debug option to get more log output.
* Get more help at https://help.gradle.org
BUILD FAILED in 2s
4 actionable tasks: 2 executed, 2 up-to-date
Process ‘command ‘C:\Program Files\Java\jdk1.8.0_111\bin\java.exe” finished with non-zero exit value 1
Hmm, at least there is a clue. I’ve tried the static approach so lets try that annotation. Putting my Google-Fu to the test I find this in the JUnit 5 API.
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
Annotation to tell the runner to reuse the same test instance. How about that then;
My final class then;
package tech.alexontest.poftutor.infrastructure; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.TestInstance; import org.openqa.selenium.WebDriver; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; /** * Abstract test class that reuses the same WebDriver for all tests. * Only supports JUnit 5 tests. */ @TestInstance(TestInstance.Lifecycle.PER_CLASS) abstract public class AbstractSingleDriverTest { private WebDriver driver; @BeforeAll public void setup() { //launch a chromedriver System.out.println("Preparing Driver"); //find the chromedriver and set its location in a system property ClassLoader classLoader = getClass().getClassLoader(); String path = classLoader.getResource("chromedriver.exe").getPath(); System.setProperty("webdriver.chrome.driver", path); //the chromeoptions we can explore later, but this will maximise it on startup ChromeOptions chromeOptions = new ChromeOptions(); chromeOptions.addArguments("--start-maximized"); driver = new ChromeDriver(chromeOptions); System.out.println("Driver Started"); } @AfterAll public void teardown() { //close the webdriver driver.quit(); } protected WebDriver getDriver() { return driver; } }
No fancy green ticks in the IDE, but the browser opened, flickered a few times as the page reloaded and passed all 4 tests. Total time 13 seconds, less than a third of that needed to run them in separate instances! I’ll call that a result.
The first win to JUnit 5, but the lack of stack trace reporting in the IDE and the inability to run individual tests means I will keep things with JUnit 4 for development, but I would certainly consider using this as part of a CI test run to reduce the build time.
Progress made this week:
- My code is checked in on GitHub if you want to save yourself some cutting and pasting
- Produced an AbstractTest class that my test classes can inherit to run tests in JUnit 4 or JUnit 5 as requested, but this makes a new WebDriver for each test and is SLOW
- Produced an AbstractSingleDriverTest class that re-uses a single WebDriver but only works with JUnit 5 tests
Lessons learnt:
- JUnit 5 gets its first win with the @TestInstance(TestInstance.Lifecycle.PER_CLASS) annotation allowing the ability to reuse one test instance across multiple methods
- Working in this way can save a lot of time, as long as you are not changing the cstate of the page during your test scripts
- Debugging JUnit 5 tests is painful without good support in the IDE
- For now develop and test individual tests using JUnit 4 and the AbstractTest class but maybe run the whole class using JUnit 5 and the AbstractSingleDriverTest if the code supports it.
That’s it for now. Next week I will finally start to look at using the Page Object Factory pattern.