Getting The Project Set Up
In your IDE of choice, go ahead and create a gradle project. I’ll assume you have some directory called learn-serenity that is going to be your project root. If you don’t have an IDE or are not creating a Gradle project via the IDE, just create the directory and then enter this at the command line in that directory:$ gradle initEdit the build.gradle file and put the following in place:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
apply plugin: 'java' apply plugin: 'net.serenity-bdd.aggregator' repositories { mavenLocal() jcenter() } buildscript { project.ext { serenityVersion = '1.1+' } repositories { mavenLocal() jcenter() } dependencies { classpath('net.serenity-bdd:serenity-gradle-plugin:' + serenityVersion) } } dependencies { testCompile 'junit:junit:4.12' testCompile 'net.serenity-bdd:serenity-core:' + serenityVersion testCompile 'net.serenity-bdd:serenity-junit:' + serenityVersion testCompile 'org.assertj:assertj-core:3.4+' testCompile 'org.slf4j:slf4j-simple:1.7+' } gradle.startParameter.continueOnFailure = true |
buildscript
section. This is what lets Gradle find and apply the plugin to your project. In the dependencies
section, you’ll see that I’ve added the Serenity BDD dependencies. You’ll pretty much always have the serenity-core and then you’ll choose some other dependency that corresponds to the testing library you are using. Here I’m using JUnit, but there are also specific dependencies for Cucumber and jBehave.
The serenity-gradle-plugin adds some tasks to your project. I won’t go into too much detail about those here except to say that, when learning, your most likely path is to run some tests and produce what’s called an aggregate report, no matter what the test results are. This means that you can run your tests from the command like this:
$ gradle test aggregateIncidentally, the continueOnFailure setting does just what it sounds like. It allows tests to keep running even if one of them fails. All reports will be stored in a
target/site/serenity
directory by default.
The official documentation covers using Maven for Serenity projects, should that be your choice.
As one final note, in the logic that follows I’m going to use my own Decohere test site as my running example. You should feel free to use whatever you want. A lot of people like to use The Internet Examples archive.
Creating the Test
If you’ve created a Gradle project, you’re going to have the standard path ofsrc/test/java
. Within that directory, create a package that will be the root package of your project. I’ve called mine com.testerstories.tutorial.serenity
but, of course, you can do whatever you like.
Now, within that package, create another package: features.authentication
.
Incidentally, if you work with Java, you know that packages are just a structuring element. You don’t have to create exactly these packages in terms of what Serenity wants. I’m doing that because it will make some of this easier to describe and because it’s likely going to be what you see in Serenity documentation or other tutorials.
Within the features.authentication package, create a class called WhenAuthenticating. If you’re not using an IDE which tends to abstract away the filenames, this class corresponds to a file called WhenAuthenticating.java. Here’s what things will look like conceptually:
This will be a test class, using jUnit. So start with this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.testerstories.tutorial.serenity.features.authentication; import org.junit.Test; public class WhenAuthenticating { @Test public void shouldBeAbleToLoginAsAdmin() { user.isOnTheHomePage(); user.logsInAsAdmin(); user.shouldBeOnLandingPage(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.testerstories.tutorial.serenity.features.authentication; import org.junit.Test; public class WhenAuthenticating { private DecohereUser user; @Test public void shouldBeAbleToLoginAsAdmin() { user.isOnTheHomePage(); user.logsInAsAdmin(); user.shouldBeOnLandingPage(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
package com.testerstories.tutorial.serenity.features.authentication; import net.thucydides.core.annotations.Steps; import org.junit.Test; public class WhenAuthenticating { @Steps private DecohereUser user; @Test public void shouldBeAbleToLoginAsAdmin() { user.isOnTheHomePage(); user.logsInAsAdmin(); user.shouldBeOnLandingPage(); } } |
user
instance is a Step Library. Step Libraries are used to add a layer of abstraction between the “what” and the “how” of the tests. From a code perspective, what this will do is inject an object into the tests that you can use to perform the individual steps or actions that carry out the test. You’ll note that you never directly instantiate this instance. Anything with the @Steps annotation is automatically instantiated for you.
At this point, let’s create a steps
package, on the same level as features
. Just to be clear, here’s what this will look like:
In that package, add the class DecohereUser. Again, this corresponds to a file called DecohereUser.java. And then make sure that your test references this new class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.testerstories.tutorial.serenity.features.authentication; import com.testerstories.tutorial.serenity.steps.DecohereUser; import net.thucydides.core.annotations.Steps; import org.junit.Test; public class WhenAuthenticating { @Steps private DecohereUser user; @Test public void shouldBeAbleToLoginAsAdmin() { user.isOnTheHomePage(); user.logsInAsAdmin(); user.shouldBeOnLandingPage(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.testerstories.tutorial.serenity.steps; public class DecohereUser { public void isOnTheHomePage() { } public void logsInAsAdmin() { } public void shouldBeOnLandingPage() { } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
package com.testerstories.tutorial.serenity.steps; import net.thucydides.core.annotations.Step; public class DecohereUser { @Step public void isOnTheHomePage() { } @Step public void logsInAsAdmin() { } @Step public void shouldBeOnLandingPage() { } } |
1 |
@Step("authenticates as an administrator user") |
Creating the Test Runner
Now let’s go back to the WhenAuthenticating class. You have to tell jUnit that this class is a Serenity test. You can use the JUnit @RunWith annotation and pass it the SerenityRunner class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.testerstories.tutorial.serenity.features.authentication; import com.testerstories.tutorial.serenity.steps.DecohereUser; import net.serenitybdd.junit.runners.SerenityRunner; import net.thucydides.core.annotations.Steps; import org.junit.Test; import org.junit.runner.RunWith; @RunWith(SerenityRunner.class) public class WhenAuthenticating { @Steps private DecohereUser user; @Test public void shouldBeAbleToLoginAsAdmin() { user.isOnTheHomePage(); user.logsInAsAdmin(); user.shouldBeOnLandingPage(); } } |
Add WebDriver To The Mix
This is a web test that we’re going to execute against a browser. This means you’re going to need to give Serenity a WebDriver instance. To do that, create an instance of the WebDriver class. Then you can annotate it with the @Managed.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
package com.testerstories.tutorial.serenity.features.authentication; import com.testerstories.tutorial.serenity.steps.DecohereUser; import net.serenitybdd.junit.runners.SerenityRunner; import net.thucydides.core.annotations.Managed; import net.thucydides.core.annotations.Steps; import org.junit.Test; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; @RunWith(SerenityRunner.class) public class WhenAuthenticating { @Steps private DecohereUser user; @Managed WebDriver browser; @Test public void shouldBeAbleToLoginAsAdmin() { user.isOnTheHomePage(); user.logsInAsAdmin(); user.shouldBeOnLandingPage(); } } |
1 |
@Managed(driver = "chrome") |
Implement the Test Logic
Now the test logic itself has to be put in place. This means filling in those @Step methods. As I did with the test method in the WhenAuthenticating class, I’ll start to define things as I want them to be and add in the appropriate classes as I go.
1 2 3 4 |
@Step public void isOnTheHomePage() { decohereHomePage.open(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.testerstories.tutorial.serenity.steps; import net.thucydides.core.annotations.Step; public class DecohereUser { private DecohereHomePage decohereHomePage; @Step public void isOnTheHomePage() { decohereHomePage.open(); } @Step public void logsInAsAdmin() { } @Step public void shouldBeOnLandingPage() { } } |
1 2 3 4 5 6 |
package com.testerstories.tutorial.serenity.ui; import net.serenitybdd.core.pages.PageObject; public class DecohereHomePage extends PageObject { } |
1 2 3 4 5 6 7 8 |
package com.testerstories.tutorial.serenity.steps; import com.testerstories.tutorial.serenity.ui.DecohereHomePage; import net.thucydides.core.annotations.Step; public class DecohereUser { private DecohereHomePage decohereHomePage; ... |
open()
method is automatically recognized. That’s happening because when a class is declared as a page object, Serenity automatically makes certain methods on that class available, one of them being the open() method. Going back to the DecohereHomePage class, since we’re using this class to open the home page, we should declare a default URL. This is the URL that will be used when that open() method is called. This can be overridden via command line properties.
1 2 3 4 5 6 7 8 |
package com.testerstories.tutorial.serenity.ui; import net.serenitybdd.core.pages.PageObject; import net.thucydides.core.annotations.DefaultUrl; @DefaultUrl("https://decohere.herokuapp.com/") public class DecohereHomePage extends PageObject { } |
1 2 3 4 |
@Step public void logsInAsAdmin() { decohereHomePage.loginAsAdmin(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.testerstories.tutorial.serenity.ui; import net.serenitybdd.core.pages.PageObject; import net.thucydides.core.annotations.DefaultUrl; @DefaultUrl("https://decohere.herokuapp.com/") public class DecohereHomePage extends PageObject { public void loginAsAdmin() { $("#openLogin").click(); $("#username").sendKeys("admin@decohere.com"); $("#password").sendKeys("admin"); $("#login").click(); } } |
$()
method with an XPath or CSS expression. When you do this, Serenity, behind the scenes, uses a WebDriver “find by” to get the element. You could also do the traditional @FindBy annotation and the PageFactory if you wanted to go that route.
Let’s fill in our last step:
1 2 3 4 5 6 7 8 9 |
import static org.assertj.core.api.Assertions.assertThat; public class DecohereUser { ... @Step public void shouldBeOnLandingPage() { assertThat(landingPage.noticeMessage()).isEqualTo("You are now logged in as admin@decohere.com."); } ... |
1 2 3 4 5 6 7 8 9 |
package com.testerstories.tutorial.serenity.ui; import net.serenitybdd.core.pages.PageObject; public class LandingPage extends PageObject { public String noticeMessage() { return $(".notice").getText(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.testerstories.tutorial.serenity.steps; import com.testerstories.tutorial.serenity.ui.DecohereHomePage; import com.testerstories.tutorial.serenity.ui.LandingPage; import net.thucydides.core.annotations.Step; import static org.assertj.core.api.Assertions.assertThat; public class DecohereUser { private DecohereHomePage decohereHomePage; private LandingPage landingPage; .... |
$ gradle test aggregateYou should find this passes. You can check the report that was generated by looking at:
target/site/serenity/index.html
Adding Navigation
Let’s add some more logic just to make sure we’ve got the hang of this. I’ll move a little faster this time. Create a new package called features.navigation. Here’s what this looks like: In that package, create a class called WhenNavigating. Let’s start off with this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
package com.testerstories.tutorial.serenity.features.navigation; import com.testerstories.tutorial.serenity.steps.DecohereUser; import net.serenitybdd.junit.runners.SerenityRunner; import net.thucydides.core.annotations.Managed; import net.thucydides.core.annotations.Steps; import org.junit.Test; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; @RunWith(SerenityRunner.class) public class WhenNavigating { @Steps private DecohereUser user; @Managed(driver = "chrome") WebDriver browser; @Test public void shouldBeAbleToGoToOverlord() { user.isOnTheHomePage(); user.logsInAsAdmin(); user.navigatesToSiteArea(SiteArea.Overlord); user.shouldSeePageTitleContaining("Project Overlord"); } } |
1 2 3 4 5 |
package com.testerstories.tutorial.serenity.models; public enum SiteArea { Overlord } |
1 2 3 4 |
@Step public void navigatesToSiteArea(SiteArea siteArea) { } |
1 2 3 4 |
@Step public void navigatesToSiteArea(SiteArea siteArea) { siteAreaNavigation.selectArea(siteArea); } |
1 2 3 4 5 6 |
public class DecohereUser { private DecohereHomePage decohereHomePage; private LandingPage landingPage; private SiteAreaNavigation siteAreaNavigation; ... |
1 2 3 4 5 6 7 8 9 10 |
package com.testerstories.tutorial.serenity.ui; import com.testerstories.tutorial.serenity.models.SiteArea; import net.serenitybdd.core.pages.PageObject; public class SiteAreaNavigation extends PageObject { public void selectArea(SiteArea siteArea) { } } |
1 2 3 4 |
public void selectArea(SiteArea siteArea) { $("#pages").click(); $("*[role=menu] li").find(By.linkText(siteArea.name())).click(); } |
1 2 3 4 |
@Step public void shouldSeePageTitleContaining(String expectedTitle) { assertThat(currentPage.getTitle()).containsIgnoringCase(expectedTitle); } |
1 2 3 4 5 6 |
public class DecohereUser { private DecohereHomePage decohereHomePage; private LandingPage landingPage; private SiteAreaNavigation siteAreaNavigation; private CurrentPage currentPage; ... |
1 2 3 4 5 6 |
package com.testerstories.tutorial.serenity.ui; import net.serenitybdd.core.pages.PageObject; public class CurrentPage extends PageObject { } |
Very useful example with clear explanations.
Thank you!!!
Good explanation. Could you please do one scenario with serenity screenplay .I am getting compilation errors in “when then” though I have screenplay jar file in maven.
I may get back into Serenity but, to be honest, I’m not sure. It changes quite a bit and those changes are rarely documented well, if at all. I do have the screenplay posts that I started awhile back.
I may try to get back into this but I just don’t want to promise anything. While I appreciate the work that goes into Serenity, I just think it is become too bloated of a tool and I’m more in the habit of crafting solutions around microframeworks rather than the monolith that Serenity is becoming.