Skip to content

Commit 27d9994

Browse files
committed
Preparing for release 0.1
SeleniumRule is the public API. Provides: - automatic setup and teardown of a WebDriver (WebDriverResource) - retry logic in case of test failure, provided that the test is annotated with Flaky (RetryRule) Leaving out for the moment: - TakeScreenshotOnFailureRule - TestLoggerRule which need a TestReporterRule first.
1 parent a1c5e1e commit 27d9994

27 files changed

+963
-318
lines changed

README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Selenium-JUnit4
2+
3+
A light-weight Selenium testing framework providing a number of features in the form of
4+
[JUnit Rules](https://github.com/junit-team/junit4/wiki/Rules).
5+
6+
7+
## Motivation
8+
When starting a test automation project using Selenium, there are a few things that need to be
9+
implemented.
10+
In fact, there's a gap to fill between Selenium and the testing framework of choice.
11+
A classic example is the setup and tear down of a `WebDriver` for each test.
12+
Or to take a screenshot in case of test failure. Selenium provides a method for taking a screenshot,
13+
but it's up to the tester to call it when a test fails.
14+
All of these things are pretty much a must-have, so we all (testers) end up reinventing the wheel.
15+
16+
The aim of this project is to fill the gap for projects using JUnit as the testing framework.
17+
The way it does is by exploiting [JUnit Rules](https://github.com/junit-team/junit4/wiki/Rules),
18+
which by the way allows for a *clean* design.
19+
20+
21+
## Features
22+
23+
- Automatic setup and tear down of `WebDriver`'s
24+
- Retry flaky tests in case of failure
25+
26+
Soon to be added:
27+
28+
- Screenshot on test failure
29+
- HTML test reports (with screenshots on test failure)
30+
31+
32+
## Example of usage
33+
34+
public class MySeleniumTest {
35+
36+
@Rule
37+
public final SeleniumRule seleniumRule = new SeleniumRule(new ChromeDriverFactory())
38+
.toRetryFlakyTestsOnFailure(2); // retry each test max 2 times (max 3 executions in total)
39+
40+
protected final WebDriver driver() {
41+
return seleniumRule.getDriver();
42+
}
43+
44+
@Test
45+
@Flaky // without this, the test will *not* be re-tried in case of failure, even though SeleniumRule was configured to retry
46+
public void myFlakyTest() {
47+
throw new RuntimeException("flaky test failure");
48+
}
49+
50+
@Test
51+
public void myStableTest() {
52+
driver().get("http://www.google.com");
53+
driver().findElement(By.name("q")).sendKeys("selenium-junit" + Keys.ENTER);
54+
new WebDriverWait(driver(), 5).until(ExpectedConditions.titleContains("selenium-junit"));
55+
}
56+
57+
private static class ChromeDriverFactory implements WebDriverFactory {
58+
@Override
59+
public WebDriver create() {
60+
return new ChromeDriver();
61+
}
62+
}
63+
}
64+
65+
In this example we have a test class with two tests, `myStableTest` and `myFlakyTest`,
66+
and one Rule, a `SeleniumRule`.
67+
68+
In `myStableTest` we are using the driver returned by `seleniumRule.getDriver()`
69+
(which, for convenience, is wrapped in the method `driver()`)
70+
to interact with the web UI of the System Under Test.
71+
Thanks to `SeleniumRule`, the driver has already been initialized
72+
(in this example, it will be a local instance of the browser Chrome),
73+
and it will also be torn down automatically as soon as the test finishes.
74+
75+
`myFlakyTest` is simulating a flaky test by always throwing an exception.
76+
Since the test is annotated with `@Flaky`, and `SeleniumRule` is configured to retry flaky tests
77+
on failure (`.toRetryFlakyTestsOnFailure(2)`), then `myFlakyTest` will be executed 3 times
78+
(1 standard execution + 2 retries, as per configuration).
79+
80+
All of the details regarding the setup/teardown and the retry logic are hidden behind three lines:
81+
82+
@Rule
83+
public final SeleniumRule seleniumRule = new SeleniumRule(new ChromeDriverFactory())
84+
.toRetryFlakyTestsOnFailure(2); // retry each test max 2 times (max 3 executions in total)
85+
86+
`SeleniumRule` is the one and only class being part of the public API of this project.
87+
It can be configured so that users can get only the features they need.
88+
The most minimal configuration includes only the setup and tear down of a WebDriver:
89+
90+
@Rule
91+
public final SeleniumRule seleniumRule = new SeleniumRule(new ChromeDriverFactory());
92+
93+
Please see the javadoc of `SeleniumRule` for an up-to-date example of usage.
94+
95+
96+
## Internals
97+
98+
Internally, `SeleniumRule` is not a monolithic Rule implementing all of the features.
99+
Rather, each feature is implemented by a `TestRule` on its own.
100+
`SeleniumRule` simply acts as a collector (a `RuleChain`) of the rules configured:
101+
102+
- makes sure the sub-rules are run in the correct order
103+
- provides clients with a configuration API to activate only the features, aka sub-rules, needed

pom.xml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>me.alb-i986.selenium</groupId>
88
<artifactId>selenium-junit4</artifactId>
9-
<version>1.0-SNAPSHOT</version>
9+
<version>0.1-SNAPSHOT</version>
1010
<description>A lightweight framework for writing Selenium tests with JUnit, providing a number of TestRule's</description>
1111
<url>https://github.com/alb-i986/selenium-junit4</url>
1212

@@ -125,12 +125,6 @@
125125
<version>1.10.19</version>
126126
<scope>test</scope>
127127
</dependency>
128-
<dependency>
129-
<groupId>org.seleniumhq.selenium</groupId>
130-
<artifactId>selenium-chrome-driver</artifactId>
131-
<version>${selenium.version}</version>
132-
<scope>test</scope>
133-
</dependency>
134128
</dependencies>
135129

136130
<distributionManagement>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package me.alb_i986.selenium;
2+
3+
import org.openqa.selenium.WebDriver;
4+
5+
/**
6+
* Provides initialized {@link WebDriver} instances.
7+
*/
8+
public interface WebDriverProvider {
9+
10+
/**
11+
* @return a non-null driver
12+
* @throws IllegalStateException if the driver to be returned is null
13+
*/
14+
WebDriver getDriver() throws IllegalStateException;
15+
}

src/main/java/me/alb_i986/selenium/junit/rules/DriverServiceResource.java

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,33 @@
44
import org.openqa.selenium.remote.service.DriverService;
55

66
/**
7-
* A {@link org.junit.rules.TestRule} managing {@link DriverService} instances.
7+
* A {@link org.junit.rules.TestRule} managing a {@link DriverService},
8+
* i.e. starting and stopping it.
89
* <p>
9-
* The {@link DriverService} is started on test start, and stopped on test termination.
10+
* This rule should be used as a {@link org.junit.ClassRule}.
1011
* <p>
11-
* This rule is typically used as a {@link org.junit.ClassRule}.
12+
* Example of usage:
13+
* <pre>
14+
* public class MyTest {
15+
*
16+
* protected static final ChromeDriverService CHROME_DRIVER_SERVICE = ChromeDriverService.createDefaultService();
17+
*
18+
* &#064;ClassRule
19+
* public static final DriverServiceResource DRIVER_SERVICE_RESOURCE = new DriverServiceResource(CHROME_DRIVER_SERVICE);
20+
*
21+
* &#064;Test
22+
* public void firstTest() {
23+
* WebDriver driver = new ChromeDriver(CHROME_DRIVER_SERVICE);
24+
* [..]
25+
* }
26+
*
27+
* &#064;Test
28+
* public void secondTest() {
29+
* WebDriver driver = new ChromeDriver(CHROME_DRIVER_SERVICE);
30+
* [..]
31+
* }
32+
* }
33+
* </pre>
1234
*/
1335
public class DriverServiceResource extends ExternalResource {
1436

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package me.alb_i986.selenium.junit.rules;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* A test marked as {@code Flaky} will be retried in case of failure
10+
* by {@link RetryRule}, if configured.
11+
*/
12+
@Retention(RetentionPolicy.RUNTIME)
13+
@Target(ElementType.METHOD)
14+
public @interface Flaky {
15+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package me.alb_i986.selenium.junit.rules;
2+
3+
import org.junit.runner.Description;
4+
5+
import java.io.PrintWriter;
6+
import java.io.StringWriter;
7+
import java.util.Arrays;
8+
import java.util.List;
9+
10+
/**
11+
* Thrown by {@link RetryRule} when a test is retried and never passes.
12+
* <p>
13+
* Its message lists all of the test failures which occurred while retrying,
14+
* with the stacktrace.
15+
*/
16+
public class RetryException extends RuntimeException {
17+
18+
public RetryException(Description description, Throwable... failures) {
19+
this(description, Arrays.asList(failures));
20+
}
21+
22+
public RetryException(Description description, List<Throwable> failures) {
23+
super(createMessage(description, failures));
24+
}
25+
26+
private static String createMessage(Description description, List<Throwable> failures) {
27+
if (failures.isEmpty()) {
28+
throw new IllegalArgumentException("The list of failures must not be empty");
29+
}
30+
StringBuilder sb = new StringBuilder();
31+
sb.append(String.format("Flaky test '%s' failed %d times:\n", description.getDisplayName(), failures.size()));
32+
int i = 1;
33+
for (Throwable failure : failures) {
34+
sb.append(String.format("\n %d. %s", i++, getStackTrace(failure)));
35+
}
36+
return sb.toString();
37+
}
38+
39+
// TODO with JUnit 4.13, we'll be able to use Throwables.getStacktrace() instead of this
40+
private static String getStackTrace(Throwable throwable) {
41+
StringWriter sw = new StringWriter();
42+
throwable.printStackTrace(new PrintWriter(sw, true));
43+
return sw.toString();
44+
}
45+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package me.alb_i986.selenium.junit.rules;
2+
3+
import org.junit.internal.AssumptionViolatedException;
4+
import org.junit.rules.TestRule;
5+
import org.junit.runner.Description;
6+
import org.junit.runners.model.Statement;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
11+
/**
12+
* A rule which, in case of failure, re-runs a test annotated with {@link Flaky} until it passes,
13+
* for max {@code n} times (max {@code n+1} executions in total).
14+
* <p>
15+
* If a test doesn't pass after {@code n+1} executions, a {@link RetryException} is thrown,
16+
* whose message contains all of the failures occurred.
17+
* <p>
18+
* A test is retried <i>unless</i>:
19+
* <ul>
20+
* <li>The test is <i>not</i> annotated with {@link Flaky}</li>
21+
* <li>The test throws {@link AssumptionViolatedException}</li>
22+
* <li>The test throws an {@link Error}, excluding {@link AssertionError},
23+
* so that OOM errors are always propagated ASAP</li>
24+
* <li>The configured retry times is 0 (i.e. {@code new RetryRule(0)}),
25+
* in which case any test will be executed only once,
26+
* as if there was no {@link RetryRule} in place</li>
27+
* </ul>
28+
* <h3>Example</h3>
29+
* <pre>
30+
* &#064;Rule TestRule retryRule = new RetryRule(1);
31+
* </pre>
32+
*
33+
* Given the rule above, this is what happens for each test run:
34+
* <ul>
35+
* <li>If the first execution is successful, that's it</li>
36+
* <li>Otherwise, the test will be run once again</li>
37+
* <li>If the second execution is still not successful,
38+
* then a {@link RetryException} will be thrown,
39+
* with the two test failures occurred attached to the exception message</li>
40+
* </ul>
41+
*/
42+
public class RetryRule implements TestRule {
43+
44+
private final int retryTimes;
45+
46+
/**
47+
* @param retries how many times to retry, <i>after the first attempt</i>.
48+
* For example, {@code new RetryRule(1)} will run a test max 2 times.
49+
*/
50+
public RetryRule(int retries) {
51+
if (retries < 0) {
52+
throw new IllegalArgumentException("The number of retries needs to be an integer >= 0" +
53+
" but was: " + retries);
54+
}
55+
this.retryTimes = retries;
56+
}
57+
58+
@Override
59+
public Statement apply(final Statement base, final Description description) {
60+
return new Statement() {
61+
@Override
62+
public void evaluate() throws Throwable {
63+
// DO NOT retry if the test is not marked as flaky,
64+
// or this rule has been configured to retry for 0 times
65+
if (retryTimes == 0 || !isTestFlaky()) {
66+
base.evaluate();
67+
return;
68+
}
69+
70+
List<Throwable> failures = new ArrayList<>();
71+
int i;
72+
for (i = 0; i < (retryTimes + 1); i++) {
73+
try {
74+
base.evaluate();
75+
break;
76+
} catch (AssertionError e) {
77+
failures.add(e);
78+
} catch (Error error) {
79+
throw error;
80+
} catch (AssumptionViolatedException e) {
81+
throw e;
82+
} catch (Throwable t) {
83+
failures.add(t);
84+
}
85+
86+
// TODO report that it's going to retry
87+
// System.err.println(description.getDisplayName() +
88+
// ": Failed, " + i + "retries remain");
89+
}
90+
91+
if (!failures.isEmpty()) { // the test was retried
92+
if (i == retryTimes + 1) { // the test failed all the times
93+
throw new RetryException(description, failures);
94+
} else { // the test failed a few times but passed in the end
95+
// TODO report the first (retryTimes - i) failures
96+
}
97+
}
98+
}
99+
100+
private boolean isTestFlaky() {
101+
return description.getAnnotation(Flaky.class) != null;
102+
}
103+
};
104+
}
105+
}

src/main/java/me/alb_i986/selenium/junit/rules/RuleChainBuilder.java

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)