Skip to content

Commit

Permalink
Allow more elegant waits in the Screenplay module
Browse files Browse the repository at this point in the history
You can now write code like this:

jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(10))))

This will not fail if the matcher cannot be evaluated the first time, but will retry up to a maximum of 'serenity.timouts' seconds (5 by default).
  • Loading branch information
wakaleo committed Feb 14, 2016
1 parent e0c2263 commit 433b732
Show file tree
Hide file tree
Showing 8 changed files with 282 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,29 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Created by john on 14/03/15.
*/
public class Stopwatch {

private static final Logger LOGGER = LoggerFactory.getLogger(Stopwatch.class);

public static Stopwatch SYSTEM = new Stopwatch();
long counter = 0;
long startTime = 0;

public void start() {
counter = System.currentTimeMillis();
startTime = System.currentTimeMillis();
}

public long stop() {
long result = System.currentTimeMillis() - counter;
counter = 0;
long result = System.currentTimeMillis() - startTime;
startTime = 0;
return result;
}

public long lapTime() {
return System.currentTimeMillis() - startTime;
}

public long stop(String message) {
long result = stop();
LOGGER.debug(message + " took {0} ms", +result);
System.out.println(message + " took " + result + " ms");
return result;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ public TestResult resultFor(Class testFailureCause) {
DEFAULT_PENDING_TYPES.addAll(ImmutableList.of(PendingStepException.class, PendingException.class));
}

private final List<Class<?>> DEFAULT_ERROR = Lists.newArrayList();
{
DEFAULT_ERROR.addAll(ImmutableList.of(Error.class));
}
public boolean reportAsFailure(Class<?> testFailureCause) {
if (testFailureCause == null) {
return false;
Expand Down Expand Up @@ -103,6 +107,18 @@ public boolean reportAsPending(Class<?> testFailureCause) {
return false;
}

public boolean reportAsError(Class<?> testFailureCause) {
if (testFailureCause == null) {
return false;
}
for(Class<?> validErrorType: configured.errorTypes()) {
if (isA(validErrorType,testFailureCause)) {
return true;
}
}
return false;
}

private boolean isA(Class<?> expectedClass, Class testFailureCause) {
return expectedClass.isAssignableFrom(testFailureCause);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ public class FailureAnalysisConfiguration {
DEFAULT_PENDING_TYPES.addAll(ImmutableList.of(PendingStepException.class, PendingException.class));
}

private final List<Class<?>> DEFAULT_ERROR_TYPES = Lists.newArrayList();
{
DEFAULT_ERROR_TYPES.addAll(ImmutableList.of(Error.class));
}

public FailureAnalysisConfiguration(EnvironmentVariables environmentVariables) {
this.environmentVariables = environmentVariables;
}
Expand Down Expand Up @@ -75,6 +80,17 @@ public List<Class<?>> pendingTypes() {
return pendingTypes;
}

public List<Class<?>> errorTypes() {
List<Class<?>> errorTypes = Lists.newArrayList(DEFAULT_ERROR_TYPES);
errorTypes.addAll(errorTypesDefinedIn(environmentVariables));

errorTypes.removeAll(pendingTypesDefinedIn(environmentVariables));
errorTypes.removeAll(compromisedTypesDefinedIn(environmentVariables));
errorTypes.removeAll(failureTypesDefinedIn(environmentVariables));

return errorTypes;
}

private List<Class<?>> errorTypesDefinedIn(EnvironmentVariables environmentVariables) {
return typesDefinedIn(ThucydidesSystemProperty.SERENITY_ERROR_ON, environmentVariables);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ private Throwable originalExceptionFrom(Throwable thrownException) {
if (!(thrownException instanceof WebdriverAssertionError) && ((thrownException instanceof SerenityManagedException) || (thrownException instanceof AssertionError))){
return thrownException;
}
if (failureAnalysis.reportAsError(thrownException.getClass())) {
return thrownException;
}
if (failureAnalysis.reportAsCompromised(thrownException.getClass())) {
return thrownException;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package net.serenitybdd.screenplay;

import net.serenitybdd.core.time.Stopwatch;
import net.thucydides.core.guice.Injectors;
import net.thucydides.core.webdriver.Configuration;

public class EventualConsequence<T> implements Consequence<T> {
public static final int A_SHORT_PERIOD_BETWEEN_TRIES = 100;
private final Consequence<T> consequenceThatMightTakeSomeTime;
private final long timeout;

private AssertionError caughtAssertionError = null;
private RuntimeException caughtRuntimeException = null;

public EventualConsequence(Consequence<T> consequenceThatMightTakeSomeTime, long timeout) {
this.consequenceThatMightTakeSomeTime = consequenceThatMightTakeSomeTime;
this.timeout = timeout;
}

public EventualConsequence(Consequence<T> consequenceThatMightTakeSomeTime) {
this(consequenceThatMightTakeSomeTime,
Injectors.getInjector().getInstance(Configuration.class).getElementTimeout() * 1000);
}

public static <T> EventualConsequence<T> eventually(Consequence<T> consequenceThatMightTakeSomeTime) {
return new EventualConsequence(consequenceThatMightTakeSomeTime);
}

public EventualConsequenceBuilder<T> waitingForNoLongerThan(long amount) {
return new EventualConsequenceBuilder(consequenceThatMightTakeSomeTime, amount);
}

@Override
public void evaluateFor(Actor actor) {
Stopwatch stopwatch = new Stopwatch();

stopwatch.start();
do {
try {
consequenceThatMightTakeSomeTime.evaluateFor(actor);
return;
} catch (AssertionError assertionError) {
caughtAssertionError = assertionError;
} catch (RuntimeException runtimeException) {
caughtRuntimeException = runtimeException;
}
pauseBeforeNextAttempt();
} while (stopwatch.lapTime() < timeout);

throwAnyCaughtErrors();
}

private void pauseBeforeNextAttempt() {
try {
Thread.sleep(A_SHORT_PERIOD_BETWEEN_TRIES);
} catch (InterruptedException e) {
e.printStackTrace();
}
}

private void throwAnyCaughtErrors() {
if (caughtAssertionError != null) {
throw caughtAssertionError;
}
if (caughtRuntimeException != null) {
throw caughtRuntimeException;
}
}

@Override
public Consequence<T> orComplainWith(Class<? extends Error> complaintType) {
return new EventualConsequence(consequenceThatMightTakeSomeTime.orComplainWith(complaintType));
}

@Override
public Consequence<T> orComplainWith(Class<? extends Error> complaintType, String complaintDetails) {
return new EventualConsequence(consequenceThatMightTakeSomeTime.orComplainWith(complaintType, complaintDetails));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.serenitybdd.screenplay;

public class EventualConsequenceBuilder<T> {
private final Consequence<T> consequence;
private final long amount;

public <T> EventualConsequenceBuilder(Consequence consequence, long amount) {
this.consequence = consequence;
this.amount = amount;
}

public EventualConsequence<T> milliseconds() {
return new EventualConsequence<T>(consequence, amount);
}

public EventualConsequence<T> seconds() {
return new EventualConsequence<T>(consequence, amount * 1000);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package net.serenitybdd.screenplay

public class SomethingBadHappenedException extends Error {

public SomethingBadHappenedException(String msg) {
super(msg)
}

public SomethingBadHappenedException(String msg, Throwable e) {
super(msg, e)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package net.serenitybdd.screenplay
import net.serenitybdd.core.Serenity
import net.thucydides.core.model.TestResult
import static net.thucydides.core.model.TestResult.*
import net.thucydides.core.steps.StepEventBus
import spock.lang.Specification

import static net.serenitybdd.screenplay.EventualConsequence.eventually
import static net.serenitybdd.screenplay.GivenWhenThen.seeThat
import static org.hamcrest.Matchers.equalTo

class WhenWaitingForDelayedResults extends Specification {

def setup() {
Serenity.initialize(this)
StepEventBus.eventBus.testStarted("some test")
}

def "should get a result immediately by default"() {
given:
Actor jane = Actor.named("Jane")
Clicker clicker = new Clicker();
when:
jane.should(seeThat(TheClickerValue.of(clicker), equalTo(1)))
then:
theTestResult() == SUCCESS
}

def "should be able to wait for a result to become available if it is slow to arrive"() {
given:
Actor jane = Actor.named("Jane")
Clicker clicker = new Clicker();
when:
jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(10))))
then:
theTestResult() == SUCCESS
}

def "should not wait forever if the result never arrives"() {
given:
Actor jane = Actor.named("Jane")
Clicker clicker = new Clicker();
when:
jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(-1))).waitingForNoLongerThan(100).milliseconds())
then:
theTestResult() == FAILURE
}

def "should transmit an error if one happens"() {
given:
Actor jane = Actor.named("Jane")
Clicker clicker = new Clicker();
when:
jane.should(eventually(seeThat(TheClickerValue.ofBroken(clicker), equalTo(1))).waitingForNoLongerThan(250).milliseconds())
then:
theTestResult() == ERROR
}

def "should report custom error if one happens"() {
given:
Actor jane = Actor.named("Jane")
Clicker clicker = new Clicker();
when:
jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(-1)).
orComplainWith(SomethingBadHappenedException)).
waitingForNoLongerThan(100).milliseconds())
then:
theTestResult() == ERROR
}

def "should report custom error if one is declared outside of the eventually scope"() {
given:
Actor jane = Actor.named("Jane")
Clicker clicker = new Clicker();
when:
jane.should(eventually(seeThat(TheClickerValue.of(clicker), equalTo(-1))).
waitingForNoLongerThan(100).milliseconds().orComplainWith(SomethingBadHappenedException))
then:
theTestResult() == ERROR
}

private TestResult theTestResult() {
StepEventBus.eventBus.baseStepListener.testOutcomes[0].result
}
}

class Clicker {
int count = 0

def click() {
count++
}
}


class TheClickerValue implements Question<Integer> {
private final Clicker clicker;

TheClickerValue(Clicker clicker) {
this.clicker = clicker
}

public static TheClickerValue of(Clicker clicker) {
new TheClickerValue(clicker)
}

public static TheClickerValue ofBroken(Clicker clicker) {
new BrokenClickerValue(clicker)
}

@Override
Integer answeredBy(Actor actor) {
clicker.click();
return clicker.count;
}
}

class BrokenClickerValue extends TheClickerValue {
private final Clicker clicker;

BrokenClickerValue(Clicker clicker) {
super(clicker);
}

@Override
Integer answeredBy(Actor actor) {
throw new SomethingBadHappenedException("Oh crap");
}
}

0 comments on commit 433b732

Please sign in to comment.