Getting Started

Lets start testing an example application...

The Subject System

Online banking sample application.

Lets suppose that we are developing an online banking application. Lets call it Tabby Banking and anyone who does online banking will be very familiar with the workflow. If you want, you can download and run this application and the Cascade tests from the Cascade Sources on Github.

Here is the general workflow.

The Workflow of the Sample Application

As you can see the example application has some extended journeys, which makes a great test subject.

The Control File

Cascade code artefacts comprise the Control File and Step Files. Cascade runs within the JUnit framework. So the first thing you create is the Control File that configures Cascade. And you configure it within the JUnit framework using the @RunWith annotation. To this, you pass CascadeRunner.class. When JUnit runs, it delegates to this runner class to generate tests.

@RunWith(CascadeRunner.class) 
@Scan("com.github.robindevilliers.onlinebankingexample.steps")
public class OnlineBankingTests {  
 
     @Demands 
     String username; 
 
     @Demands 
     String password; 
 
     @Demands 
     String challengePhrase; 
 
     @Setup 
     public void setup() {  
 
         Map user = new HashMap() { {  
             put("username", username); 
             put("password", password); 
             put("challengePhrase", challengePhrase); 
         }}; 
 
         Client client = ClientBuilder.newBuilder().build(); 
         WebTarget target = client.target("http://localhost:8080/database/set-user"); 
         Response response = target.request().post(Entity.entity(user, "application/json")); 
 
 
         if (response.getStatus() != 200) {  
             String content = response.readEntity(String.class); 
             System.err.print(content); 
         } 
 
         assertEquals(200, response.getStatus()); 
     } 
 
} 
An Example Cascade Control File

The Control File also tells cascade where to find Step Files. The @Scan file contains a package to search.

There are currently only two other annotations/concepts to grasp when writing the initial control file. The first, is that there are a number of member variables within the class with @Demands annotations. These variables have their values set prior to the setup method being called by the Cascade engine.

The second annotation is @Setup. This method is executed prior to the test executing. This is the moment when it is appropriate to setup data to support the test. The data in this example is really just the three strings that it demands. For the test application, I have created a test endpoint that accepts, in one go, all the state necessary for the subject system to support the test. In reality this is likely to be many different database calls and REST calls to fake services. The number of variables that comprise the data set is likely to be quite large as well.

Opening The Landing Page

So lets create our first Step File. As you can see, Step Files are annotated with the @Step annotation. And for our first Step File, the annotation takes no arguments.

@Step 
public class OpenLandingPage {  
 
     @Supplies 
     private WebDriver webDriver; 
 
     @Given 
     public void given() {  
         webDriver = new FirefoxDriver(); 
     } 
 
     @When 
     public void when() {  
         webDriver.get("http://localhost:8080"); 
         waitForPage(webDriver); 
     } 
 
     @Then     
     public void then(Throwable f) {  
         assertNull(f); 
         assertEquals("Tabby Banking", webDriver.getTitle()); 
         assertElementPresent(webDriver, "[test-form-login]"); 
     } 
 
     @Clear 
     public void clear(){  
         webDriver.close(); 
     } 
 
}       
The Open Landing Page Step File

Usually, the @Step annotation takes as its argument, the preceeding step class. You will see an example in a moment. But when no argument is supplied, the Step File is identified as a starting step for tests.

Next, you will notice the @Supplies annotation. This annotation denotes a member variable that is expected to be shared with other steps and with the Control File. In this way, when a group of steps make up a test, they can independently set state by supplying values using this annotation. When other steps are dependent on these values they can use the @Demands annotation as you have already seen with the Control File.

Then we have the @Given annotation. This annotation is a setup method used to initialise any member variables that this step supplies. All the Given methods are executed before the Setup method in the control file. This may seem counter intuitive. Really the only thing you would expect to see here is the initialisation of data. You could make REST calls, but I feel that you will run into trouble with that approach eventually. This method is optional.

Many of the readers of this guide, will have come across the Behaviour Driver Development (BDD) methodology. My choice in annotation names here is a tip-of-the-hat to those ideas. The BDD concept may not align to our Journey Tests, as our Journey Tests comprise many steps. Each Acceptance Criteria in the BDD sense is a precondition, followed by an action and then concluded with an assertion. This framework does apply within the scope of our steps however. So from this you can conclude that the next annotation, @When, denotes the action of this step. If this were a unit test, then the contents of the When method should be the subject of the test. In our case, the contents are the subject of our step. This method is optional. (That will surprise you... bear with me.) In terms of the State Machine, the When method enacts a Transition within the State Machine.

And then we have what every test should have, the assertions of our test. Except that we don't have a test, we have a step. The method annotated with the @Then annotation should contain all assertions necessary to validate the action of the When method. In terms of the State Machine, the Then method asserts that we have arrived at a state. Again... this is optional.

And finally we have clean-up. The @Clear annotation denotes the Clear method which runs after the entire Journey Test.

Why are all the methods optional? Cascade permutes steps. Some developers may find it better to have each State and Transition in the State Machine defined by a independent Step File. Cascade will then permute over them. In my experience it is highly likely that Transitions and States go in pairs. The real question is why allow a step file to contain both a Transition and State and the answer to this is an optimisation on my part to make it easier to develop tests.

The Homepage for Tabby Banking.

So what have we done in our first step file? As you can see from the Given method, we have initialised the Selenium WebDriver. That is because I'm going to drive my test via a browser by using that library. This makes sense to do here, because we have configured this step to be a entry step. As you will soon see, all other steps will demand the WebDriver variable. Next, we have a When method that uses Selenium to open the browser at the home page. This is followed by asserting that we have in fact arrived on the home page.

In fact you can see the homepage to the left here. Our test will open a browser and go to this page.

Logging In

And now for the second step in our journey tests. We will now log into the Online Banking Application.

@Step(OpenLandingPage.class) 
public interface Login {  
     public class SuccessfulLogin implements Login {  
 
         @Supplies 
         private String username = "anne";  
 
         @Supplies 
         private String password = "other"; 
 
         @Demands 
         private WebDriver webDriver; 
 
         @When 
         public void when() {  
             enterText(webDriver, "[test-field-username]", username);  
             enterText(webDriver, "[test-field-password]", password);  
             click(webDriver, "[test-cta-signin]"); 
             waitForPage(webDriver); 
         } 
 
         @Then 
         public void then(Throwable f) {  
             assertNull(f); 
             assertElementPresent(webDriver, "[test-form-challenge]"); 
         } 
     } 
 
     @Terminator 
     public class FailedLogin implements Login {  
 
         @Supplies 
         private String username = "anne"; 
 
         @Supplies 
         private String password = "other"; 
 
         @Demands 
         private WebDriver webDriver; 
 
         @When 
         public void when() {  
             enterText(webDriver, "[test-field-username]", username);  
             enterText(webDriver, "[test-field-password]", "invalidpassword"); 
             click(webDriver, "[test-cta-signin]"); 
             waitForPage(webDriver); 
         } 
 
         @Then 
         public void then() {
             assertElementPresent(webDriver, "[test-form-login]"); 
             assertElementDisplayed(webDriver, "[test-dialog-authentication-failure]"); 
         } 
     } 
} 
The Login Step File

First, take a look at the @Step annotation. You will see that is has as its argument, the OpenLandingPage.class. This is how Cascade can tell how to link steps together to form a journey.

The next interesting thing you will notice, is that the @Step annotation has been applied to a Login interface. The interface has two static inner classes defined which both implement the Login interface. The classes are SuccessfulLogin and FailedLogin. Part of the advantage of Cascade is that the code that defines journeys is stored in a logical structure. Having the scenarios implemented as static inner classes of step interfaces goes a long way to keeping the logic related to particular states organised.

Cascade naturally follows the rules defined by the Java language, so you can restructure this if you want.

The rest of the structure if very much a repeat of what you have seen before in the OpenLandingPage step.

There are some significant points however. If you take a close look at the two scenarios, you will see that they both provide a username and password. This is very common for scenarios. The point of scenarios, after all, is to provide alternative data that drives a particular state in the state machine. So, the data they provide will be over the same part or partition of the expected data model. In this case however the values are the same, but the value of the password entered on the web page is different.

If you take a look at the control file now, you will see that it demands the username and password. It sends the username and password to the subject system so that it knows about the user. After this point you could log into the subject system using the username and password.

There is only one final item of interest in this file and that is the @Terminator annotation. This annotation tells Cascade that this scenario will terminate a journey. Cascade will not append any additional scenarios to this journey after this point. This terminator is known as an explicit terminator.

Here you can see screenshots of the two outcomes of the two scenarios.

The Challenge Page.

Firstly, the successful scenario, we are presented with the challenge page which should be familiar with anyone who uses internet banking. The notion is that there is some secret pass phrase and you are challenged to enter a random selection of characters from the pass phrase. This method of authentication is intended to defeat key loggers and such.

The Failed Login Dialog.

Second, we have the failed scenario. The user's browser stays on the landing page, and a modal dialog is overlayed over the contents of the page that expresses to the user that their login attempt has failed.

The visual journey we have here is really down to Selenium, but this is better because I can show you visually how Cascade's engine permutes steps into journeys.

So what have we defined here? We have defined a Login state that has two Scenarios. Cascade can now generate two journeys from this. I've tabulated them here.

Step 1 Step 2
OpenLandingPage SuccessfulLogin
OpenLandingPage FailedLogin
Tabulating the Journeys

Passing The Challenge

And now to complete the Getting Started example. We move on to the Challenge page.

@Step(Login.class) 
public interface Challenge {  
     public class PassChallenge implements Challenge {  
 
         @Supplies 
         private String challengePhrase = "onceuponamidnightdreary"; 
 
         @Demands 
         private WebDriver webDriver; 
 
         @When 
         public void when() {  
             //enter challenge correctly
         }
     } 
 
     @Terminator 
     public class FailChallenge implements Challenge {  
 
         @Supplies 
         private String challengePhrase = "onceuponamidnightdreary"; 
 
         @Demands 
         private WebDriver webDriver; 
 
         @When 
         public void when() {
            //enter challenge incorrectly
         } 
 
         @Then 
         public void then() {
             assertElementPresent(webDriver,"[test-form-login]"); 
             assertElementDisplayed(webDriver, "[test-dialog-authentication-failure]"); 
         } 
     } 
}       
The Challenge Step File

This will be very similar to the Login page. We have the same interface with a @Step annotation and two scenarios embedded within. The first scenario, again, is a successful scenario, and the last, is a failure scenario. Again the failure scenario is a terminating step.

There is one point of interest here and that is that the successful scenario does not have a Then method. Why? Because the validation that was made here has been pulled out into its own step. This step is called the Portfolio step. This has been done because we can arrive on the portfolio web page through many different transitions, and I didn't want to repeat the validation in each step that contains those transitions.

The Portfolio Page.

On the other hand, the Failed Challenge page has a Then method which is a repeat of the Failed Login Then method. The reason for this is that they end up on the same failed authentication modal dialog. Here I've opted to repeat the validation. I've done this mostly because the code is trivial.

Anyway, I'm not going to go into too much detail around the test modeling side here as this is a getting started guide.

So now I have two more web pages to show you. The first is the Portfolio page which is actually validated by the Portfolio step. The second screenshot is the Failed Challenge page.

The Failed Challenge Dialog.

So lets tabulate the journeys again. We now have three journeys. One journey is the Failed Login journey that terminates after only two steps. The final two journeys make it to the challenge page where one is a terminator and the other will continue to be used by Cascade when generating the journeys. I've laid out the journeys in the table that follows.

Step 1 Step 2 Step 3
OpenLandingPage SuccessfulLogin PassChallenge
OpenLandingPage SuccessfulLogin FailChallenge
OpenLandingPage FailedLogin
Tabulating the Journeys

In the actual example included in the sources, the Tabby Banking application has something like 122 acceptance tests. These tests are defined in 17 step files that contain in total 26 scenarios. The largest scenario class is about 40 lines long. You can generate a terrific number of tests using very few test artefacts.

Running The Failed Login Test

So now you are probably wondering: If I don't have a particular artefact that represents a particular journey how do I run the journey in isolation?

Of course if I provided Cascade without the means to do this, then many developers would choose not to use it. The reason is that for the most part we are interested only in running a particular journey as we develop, since our development efforts tend to focus on particular aspects of the application. We would be forced to run the entire test set when we are interested in only a small part.

So of course you can. You include a filter in the control file. Here is an example.

 @FilterTests
 Predicate filter = or(
     withStep(uk.co.malbec.onlinebankingexample.steps.Login.FailedLogin.class),
     withStep(uk.co.malbec.onlinebankingexample.steps.Challenge.FailChallenge.class)
 );
The Filter For Failed Authentication Journeys

Here we are running all failed authentication journeys. This filter isn't focused on a particular functional point but rather the display of the failed authentication modal. So the filtering functionality is pretty powerful. You can include other kinds of predicates including the usual logical operators; AND, OR and NOT.

Okay, but what a test fails in CI, and I want to reproduce that test?

Well, you can see the failed journeys in the test reports. If you drill down into the test, you can see the Filter tab and if you click on that, you will be presented with the filter code necessary to run that particular test. Its quite long, so I won't include it here.

One final thing to add on this point. What if I want only one test to run, through the Successful Login step?

You can do this by limiting the tests that run, using the @Limit annotation.

Happy testing...