Switching from TestNG to JUnit
First of all, we need to make some changes to our POM.xml
to use JUnit instead of TestNG; we will start with the properties
block:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF- 8</project.reporting.outputEncoding> <java.version>1.8</java.version> <!-- Dependency versions --> <selenium.version>3.12.0</selenium.version> <junit.version>4.12</junit.version> <assertj-core.version>3.10.0</assertj-core.version> <query.version>1.2.0</query.version> <commons-io.version>2.6</commons-io.version> <httpclient.version>4.5.5</httpclient.version> <!-- Plugin versions --> <driver-binary-downloader-maven-plugin.version>1.0.17</driver- binary-downloader-maven-plugin.version> <maven-compiler-plugin.version>3.7.0</maven-compiler- plugin.version> <maven-failsafe-plugin.version>2.21.0</maven-failsafe- plugin.version> <!-- Configurable variables --> <threads>1</threads> <browser>firefox</browser> <overwrite.binaries>false</overwrite.binaries> <headless>true</headless> <remote>false</remote> <seleniumGridURL/> <platform/> <browserVersion/> <screenshotDirectory>${project.build.directory} /screenshots</screenshotDirectory> <proxyEnabled>false</proxyEnabled> <proxyHost/> <proxyPort/> </properties>
Then we need to modify our dependencies. We are going to remove the testNG dependency and instead add in a Unit dependency:
<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency>
For the final changes to our POM.xml
, we need to modify our plugin
configuration:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-failsafe-plugin</artifactId> <version>${maven-failsafe-plugin.version}</version> <configuration> <parallel>methods</parallel> <threadCount>${threads}</threadCount> <perCoreThreadCount>false</perCoreThreadCount> <properties> <property> <name>listener</name> <value>com.masteringselenium. listeners.ScreenshotListener</value> </property> </properties> <systemPropertyVariables> <browser>${browser}</browser> <headless>${headless}</headless> <remoteDriver>${remote}</remoteDriver> <gridURL>${seleniumGridURL}</gridURL> <desiredPlatform>${platform}</desiredPlatform> <desiredBrowserVersion>${browserVersion} </desiredBrowserVersion> <screenshotDirectory>${screenshotDirectory} </screenshotDirectory> <proxyEnabled>${proxyEnabled}</proxyEnabled> <proxyHost>${proxyHost}</proxyHost> <proxyPort>${proxyPort}</proxyPort> <!--Set properties passed in by the driver binary downloader--> <webdriver.chrome.driver>${webdriver.chrome.driver} </webdriver.chrome.driver> <webdriver.ie.driver>${webdriver.ie.driver} </webdriver.ie.driver> <webdriver.opera.driver>${webdriver.opera.driver} </webdriver.opera.driver> <webdriver.gecko.driver>${webdriver.gecko.driver} </webdriver.gecko.driver> <webdriver.edge.driver>${webdriver.edge.driver} </webdriver.edge.driver> </systemPropertyVariables> </configuration> <executions> <execution> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> </execution> </executions> </plugin>
The first change is to add the <perCoreThreadCount>false</perCoreThreadCount>
configuration setting. When using the surefire plugin with JUnit, the plugin applies the thread count to each CPU core you have in your machine. We want to make sure that we are supplying a total number of browsers, not defaulting to eight browsers if you have an eight-core machine.
The second change is to specify the location of our ScreenshotListener
class; we can't apply it using an annotation in code, like we did with TestNG. As a result, we are using the Maven Failsafe plugin configuration block to apply it instead.
Now we need to make some changes to our DriverBase
class:
package com.masteringselenium; import com.masteringselenium.config.DriverFactory; import net.lightbody.bmp.BrowserMobProxy; import org.junit.After; import org.junit.AfterClass; import org.junit.BeforeClass; import org.openqa.selenium.remote.RemoteWebDriver; import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class DriverBase { private static List<DriverFactory> webDriverThreadPool = Collections.synchronizedList(new ArrayList<DriverFactory>()); private static ThreadLocal<DriverFactory> driverThread; @BeforeClass public static void instantiateDriverObject() { driverThread = new ThreadLocal<DriverFactory>() { @Override protected DriverFactory initialValue() { DriverFactory webDriverThread = new DriverFactory(); webDriverThreadPool.add(webDriverThread); return webDriverThread; } }; } public static RemoteWebDriver getBrowserMobProxyEnabledDriver() throws MalformedURLException { return driverThread.get().getDriver(true); } public static RemoteWebDriver getDriver() throws MalformedURLException { return driverThread.get().getDriver(); } public static BrowserMobProxy getBrowserMobProxy() { return driverThread.get().getBrowserMobProxy(); } @After public void clearCookies() { try { getDriver().manage().deleteAllCookies(); } catch (Exception ex) { System.err.println("Unable to delete cookies: " + ex); } } @AfterClass public static void closeDriverObjects() { for (DriverFactory webDriverThread : webDriverThreadPool) { webDriverThread.quitDriver(); } } }
There aren't many changes here. We have switched out the TestNG annotations for JUnit ones, and we have removed the @Listener
annotation because JUnit doesn't have an equivalent. This does mean that whilst running our tests through an IDE, we will no longer get screenshots on failure. Don't worry though, it will still work when running the build on the command line using Maven. You will still be able to collect screenshots on your CI server if things go wrong.
Finally, we need to modify our ScreenshotListener
class to work with JUnit instead of TestNG:
package com.masteringselenium.listeners; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunListener; import org.openqa.selenium.OutputType; import org.openqa.selenium.TakesScreenshot; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.Augmenter; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import static com.masteringselenium.DriverBase.getDriver; public class ScreenshotListener extends RunListener { private boolean createFile(File screenshot) { boolean fileCreated = false; if (screenshot.exists()) { fileCreated = true; } else { File parentDirectory = new File(screenshot.getParent()); if (parentDirectory.exists() || parentDirectory.mkdirs()) { try { fileCreated = screenshot.createNewFile(); } catch (IOException errorCreatingScreenshot) { errorCreatingScreenshot.printStackTrace(); } } } return fileCreated; } private void writeScreenshotToFile(WebDriver driver, File screenshot) { try { FileOutputStream screenshotStream = new FileOutputStream(screenshot); screenshotStream.write(((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES)); screenshotStream.close(); } catch (IOException unableToWriteScreenshot) { System.err.println("Unable to write " + screenshot.getAbsolutePath()); unableToWriteScreenshot.printStackTrace(); } } @Override public void testFailure(Failure failure) { try { WebDriver driver = getDriver(); String screenshotDirectory = System.getProperty("screenshotDirectory", "target/screenshots"); String screenshotAbsolutePath = screenshotDirectory + File.separator + System.currentTimeMillis() + "_" + failure.getDescription().getMethodName() + ".png"; File screenshot = new File(screenshotAbsolutePath); if (createFile(screenshot)) { try { writeScreenshotToFile(driver, screenshot); } catch (ClassCastException weNeedToAugmentOurDriverObject) { writeScreenshotToFile(new Augmenter().augment(driver), screenshot); } System.out.println("Written screenshot to " + screenshotAbsolutePath); } else { System.err.println("Unable to create " + screenshotAbsolutePath); } } catch (Exception ex) { System.err.println("Unable to capture screenshot: " + ex.getCause()); } } }
Again, the changes are minimal. We now extend RunListener
, which is part of JUnit, instead of TestListenerAdaptor
provided by TestNG. The name of the class we need to override has changed and it passes in a different variable. This means that we need to slightly change the code that gets the name of the failing test.
The only thing that is left to do is to modify the imports on our tests; you use the JUnit @Test
annotation instead of the TestNG @Test
annotation. To do this, we need to find all instances of the following:
import org.testng.annotations.Test;
We replace them with the following:
import org.junit.Test;
You are now ready to try running your tests again; let's perform the following in the Terminal to check that everything still works:
mvn clean verify
Let's check that threading still works as well:
mvn clean verify -Dthreads=2
Excellent. Everything seems to be working correctly; we now have a working JUnit implementation.
I did, however, mention that there would be some caveats, and unfortunately they are not instantly obvious. JUnit does not have a concept of @BeforeSuite
like TestNG, so we have used the @BeforeClass
annotation instead. When you have a single test class, it would appear to work in the same way as @BeforeSuite
, however, that's not true. When we were using before suite, we could configure our thread pool before any tests were run and then clean it up after all tests were run. With the JUnit implementation, we configure our thread pool before each class is run and then clean it up after each class is run. It's a small difference, but it does result in more browser startup/shutdown time.
The easiest way to show this is by adding another test class. Copy the existing one and give it a slightly different name. Once you have done that, tweak the tests to search with a slightly different criteria. Now try running the following code again:
mvn clean verify -Dthreads=2
You will notice that, this time, two browser windows opened, the tests in the first class were run, and then the browser windows shut again. They then opened up again and ran the tests for the second class. You can add more tests to each of your test classes to reassure yourself that the browsers are being reused inside the class.