In sensible phrases, understanding how not to put in writing exams is perhaps as essential as understanding find out how to write them. There are some very useful chapters on check smells in Gerard Meszaros’s ebook about xUnit patterns — and extra nice stuff across the web; nevertheless, it is at all times useful to have sensible examples for explicit tech stacks.
We have already proven find out how to clear up unit exams; this time, we’ll do JUnit + Selenide end-to-end exams, on the high of the pyramid. We’re assuming you are accustomed to Selenide, however most stuff right here is legitimate for different stacks, too.
All our examples can be found at a GitHub repo. There’s the primary, uncooked model of the code, the amended one that also has some points, and the ultimate model.
Earlier than We Begin: To Disguise or Not To Disguise
There are two approaches to writing automated exams:
- Stuffing as a lot as doable into web page objects and holding the naked minimal in exams;
- Conserving the exams pretty verbose.
Neither strategy is universally higher. The primary strategy is preferable in case you have an advanced UI and quite a lot of code is being reused. Alternatively, if most complexity lies on the back-end facet and also you’re planning to check it by way of API bypassing the UI, then holding the web page objects leaner might be the higher method.
Both method is okay, so long as your group is normally settlement about what you are doing. Right here, we’ll observe the primary strategy to indicate some structural issues.
The First, Awful Model
Now, for our instance. We get handed some exams — and we don’t like them:
import com.codeborne.selenide.Situation;
import com.github.javafaker.Faker;
import io.qameta.attract.Step;
import org.junit.jupiter.api.Take a look at;
import static com.codeborne.selenide.Selenide.$;
import static com.codeborne.selenide.Selenide.open;
public class BadE2E {
// The step has no description, we do not know what it is doing.
// We will not ensure that the web page is loaded
@Step
public void openAuthorizationPage() {
open("");
}
// Parts have not been put inside variables or web page objects,
// which suggests we won't reuse them.
// Knowledge is hard-coded, whereas it may very well be accepted from the surface.
@Step
public void authorize() {
$("#user-name").setValue("standard_user");
$("#password").setValue("secret_sauce");
$("#login-button").click on();
}
// Selectors are primarily based on courses and never IDs
@Step
public void checkUserAuthorized() {
$(".app_logo").shouldBe(Situation.seen);
}
// Unsafe verify: if the web page takes some time to load,
// the verify will cross, as a result of the brand is current
// on the earlier web page
@Step
public void checkUserNotAuthorized() {
$(".login_logo").shouldBe(Situation.seen);
}
// The check would not check something
@Take a look at
public void shouldAuthorizeUser() {
openAuthorizationPage();
authorize();
}
// 1. Opening the primary web page is not put right into a fixture
// 2. The 'authorize()' technique is not reused
// 3. Knowledge is generated in a separate class, which at the moment is overkill
@Take a look at
public void shouldNotAuthorizeUserWithInvalidPassword() {
Faker faker = new Faker();
TestUser person = new TestUser();
openAuthorizationPage();
$("#user-name").setValue(person.username);
$("#password").setValue(faker.web().password());
$("#login-button").click on();
checkUserNotAuthorized();
}
// An occasion of the Faker class is created in every check,
// though only one would have sufficed.
// Exams are just about equivalent; they need to be parameterized.
@Take a look at
public void shouldNotAuthorizeUserWithInvalidUsername() {
Faker faker = new Faker();
TestUser person = new TestUser();
openAuthorizationPage();
$("#user-name").setValue(faker.title().username());
$("#password").setValue(person.password);
$("#login-button").click on();
checkUserNotAuthorized();
}
// A single check has a number of checks. The exams rely on one another,
// which can trigger flakiness.
// The checks are equivalent.
@Take a look at
public void shouldNotAuthorizeUserWithEmptyAndBlankInputs() {
openAuthorizationPage();
$("#login-button").click on();
checkUserNotAuthorized();
$("#user-name").setValue(" ");
$("#password").setValue(" ");
$("#login-button").click on();
checkUserNotAuthorized();
$("#user-name").clear();
$("#password").clear();
$("#login-button").click on();
checkUserNotAuthorized();
}
}
There are many issues right here; we’ll begin with the extra basic ones after which drill all the way down to extra particular stuff.
Refactoring
Readability
Loads of the stuff beneath will associated to readability not directly, however there are not any obvious direct issues right here; for example, the names are all alright.
Nonetheless, we may add descriptions to our steps. We’re already utilizing the Attract @Step
annotation to indicate steps; we’d as nicely use its performance to supply an outline for the step (i. e. @Step("Open the login page")
). Along with being a helpful type of documentation, this additionally lets you create a report with exams that anybody can learn, with out essentially understanding Java. This is an instance:
Construction
There’s quite a lot of pointless repetition within the exams. By eradicating it, we will enhance their construction and make them shorter, thus simpler to learn. So yeah, the construction can be about readability.
As an illustration, each check requires us to open the registration web page, so we’d as nicely transfer this half right into a fixture earlier than every check. Additionally, all exams use the identical URL, so it might make sense to maneuver it to Configuration.baseUrl
.
@BeforeEach
public void setUp() {
Configuration.baseUrl = "https://www.saucedemo.com";
openAuthorizationPage();
}
With this, opening the house web page is simply open("")
.
One other drawback is that we’re making a Faker
occasion for each check. Nonetheless, that is superfluous; we will retailer only one as a category subject.
Additionally, it is a good suggestion to maneuver selectors into separate variables:
SelenideElement inputUsername = $("#user-name");
SelenideElement inputPassword = $("#password");
SelenideElement buttonLogin = $("#login-button");
This fashion, you needn’t memorize or lookup the selector while you’re writing a brand new check. Additionally, code turns into simpler to keep up if it is advisable to change the selector for some purpose.
The authorize()
Technique
Persevering with the subject of reusability, we have to enhance the strategy with which we authorize on the webpage:
@Step
public void authorize() {
$("#user-name").setValue("standard_user");
$("#password").setValue("secret_sauce");
$("#login-button").click on();
}
Presently, knowledge is hard-coded, so we will solely use the strategy for this explicit login/password pair. Which is why we won’t use it within the final two exams, e. g. right here:
$("#user-name").setValue(faker.title().username());
$("#password").setValue(person.password);
$("#login-button").click on();
So, how will we enhance the strategy?
When authorizing, we’re both utilizing a default worth or one generated by a faker. Possibly we may sign our technique to which one to make use of? If we did that, we might get a way like this:
@Step
public void authorize(Boolean username, Boolean password) {
String trueUsername = "standard_user";
String truePassword = "secret_sauce";
if (username && password) {
inputUsername.setValue(trueUsername);
inputPassword.setValue(truePassword);
buttonLogin.click on();
checkUserAuthorized();
} else if (username) {
inputUsername.setValue(trueUsername);
inputPassword.setValue(faker.web().password());
buttonLogin.click on();
checkUserNotAuthorized();
} else if (password) {
inputUsername.setValue(faker.title().username());
inputPassword.setValue(truePassword);
buttonLogin.click on();
checkUserNotAuthorized();
} else {
inputUsername.setValue(faker.title().username());
inputPassword.setValue(faker.web().password());
buttonLogin.click on();
checkUserNotAuthorized();
}
}
Abort, abort! This appears much more horrible. Conditional logic is a serious odor in a check or a step, they develop into harder to learn.
Not solely that, we have not even achieved what we wished: making the step extra versatile. We nonetheless cannot use it for the shouldNotAuthorizeUserWithEmptyAndBlankInputs
check.
And there’s one other drawback. This is how the step appears like when used inside a check: authorize(true, false)
.
What’s “true” and “false” right here? What does this imply? We have now to look contained in the step to seek out out – so readability suffers and time to investigate failure will increase.
The issue is, our step is aware of an excessive amount of. Let’s cross the parameters from the surface as an alternative of setting their values contained in the check:
public void authorize(String username, String password) {
inputUsername.setValue(username);
inputPassword.setValue(password);
buttonLogin.click on();
}
Now, we will use it in every single place we’re attempting to log in, and it appears completely self-evident when referred to as in a check: authorize(username, password)
.
Parameterizing
The shouldNotAuthorizeUserWithInvalidPassword()
check, the shouldNotAuthorizeUserWithInvalidUsername()
check, and one of many circumstances from shouldNotAuthorizeUserWithEmptyAndBlankInputs()
are just about equivalent. So, let’s parameterize them:
@ParameterizedTest(title = "{0}")
@MethodSource("invalidCredentials")
@DisplayName("User can't authorize with ")
public void shouldNotAuthorizeUserWithInvalidCredentials(String username, String password) {
authorize(username, password);
checkUserNotAuthorized();
}
personal static Stream invalidCredentials() {
return Stream.of(
Arguments.of("invalid password", trueUsername, faker.web().password()),
Arguments.of("invalid username", faker.title().username(), truePassword),
Arguments.of("blank fields", " ", " ")
);
}
Parameterization is nice for apparent causes – you write and preserve fewer exams. Additionally, every check will get a correct show title, which is supplied with arguments. Take care, although: it is doable to overdo it with parameterizing.
In testing, the stability between avoiding repetition and readability is considerably in the direction of readability (see DRY vs. DAMP). Forcing many check circumstances right into a single one with conditionals and dozens of parameters hurts readability vastly.
Extra parameters imply your check turns into extra summary and extra faraway from the actual drawback you are testing for. Additionally, when it fails, you will need to ask your self: is the issue in my code or in my check?
We have seen corporations that managed to parameterize the hell out of every part and run exams with a number of thousand parameters. One way or the other, they handle to get every part working, however all we will say is:
Do not do this at house
Splitting
The exams from earlier than needed to be merged, however the final check in our large instance wants the other: splitting into a number of smaller ones. Right here is its first model:
@Take a look at
public void shouldNotAuthorizeUserWithEmptyAndBlankInputs() {
openAuthorizationPage();
$("#login-button").click on();
checkUserNotAuthorized();
$("#user-name").setValue(" ");
$("#password").setValue(" ");
$("#login-button").click on();
checkUserNotAuthorized();
$("#user-name").clear();
$("#password").clear();
$("#login-button").click on();
checkUserNotAuthorized();
}
Our exams must be atomic: when one factor fails, it should not trigger every part else to fail, in order that we will see instantly the place the issue is.
A number of checks enhance the prospect of a flaky check – as a result of each verify will be flaky, and only one is sufficient to destabilize the check. Everytime you ask an exterior system for authorization, one thing would possibly go fallacious. As an illustration, one thing would possibly snatch your enter from you, after which all the following checks will fail no matter what went on within the system below check.
To keep away from that:
- We have now to reduce the footprint of each check,
- And now we have to make the exams impartial of one another.
So, how will we break up our check?
Because it seems, one of many checks in it has already been taken care of by parameterization, described within the earlier part. One other verify is just redundant as a result of doing .clear()
and logging in is similar as simply logging in. So, ultimately, we will scale back our lengthy check to this:
@Take a look at
public void shouldNotAuthorizeUserWithEmptyInputs() {
buttonLogin.click on();
checkUserNotAuthorized();
}
Eradicating a Class
In most earlier examples, we have been including abstractions – parameterizing, hiding stuff in strategies, and so forth. However we have additionally received an abstraction that’s pointless. Two of our strategies use the TestUser
class:
public class TestUser {
Faker faker = new Faker();
String username;
String password;
TestUser() {
this.username = faker.title().username();
this.password = faker.web().password();
}
}
It is fairly and all, however, as mentioned, we’re solely utilizing it twice in our code. Really, each locations are in exams that we wish to parameterize, so ultimately, it is simply as soon as.
Too many abstractions litter the code and make it much less readable. You would possibly argue thatTestUser
is a category we are going to want sooner or later. If that’s true, we’ll create it once we want it.
Overpreparing for the long run isn’t a good suggestion. Having a modular and scalable construction is one factor, however including extra code since you would possibly want it’s one other. Preserve it easy:
Picture: flaviocopes
Issues Speaking to Webpages
Our instance has some issues which are particular to UI exams for webpages.
Finding by IDs and Knowledge Attributes
Let’s check out the selectors in our exams:
@Step
public void checkUserAuthorized() {
$(".app_logo").shouldBe(Situation.seen);
}
Every time doable, we should always base our selectors on IDs or knowledge attributes, and never courses: they’re much extra exact, which suggests your exams are going to be much less buggy. So a greater model could be this:
@Step
public void checkUserAuthorized() {
$("[data-test="secondary-header"]").shouldBe(Situation.seen);
}
Making Checks Definitive
Let’s check out this verify:
@Step
public void checkUserNotAuthorized() {
$(".login_logo").shouldBe(Situation.seen);
}
Positive, if we will see the login emblem, it may imply that authorization has been rejected; nevertheless, it may additionally imply that the following web page merely took too lengthy to load.
Let’s attempt to repair this:
@Step
public void checkUserNotAuthorized() {
$(".login_logo").shouldBe(Situation.seen);
$("#login_button_container").shouldBe(Situation.seen);
inputUsername.shouldBe(Situation.seen);
inputPassword.shouldBe(Situation.seen);
}
Nice, now we have one ID-based selector now. However this step remains to be dangerous. We have tried to compensate for an unreliable verify by including three extra unreliable checks. Which suggests:
- If the check fails, we nonetheless aren’t sure it is a “real” failure, so we’ll must waste time checking;
- Extra traces imply extra stuff can go fallacious;
- The check turns into unclear.
We actually solely want two checks:
- That the URL has modified;
- That the web page on the new URL is the one we want.
Placing every part collectively, we will rewrite each this step and the one from the earlier part:
@Step("Check the product page is opened")
public void checkUserAuthorized() {
webdriver().shouldHave(url(Configuration.baseUrl + "/inventory.html"));
$("[data-test="secondary-header"]").shouldBe(Situation.seen);
}
@Step("Check the user wasn't redirected to the products page")
public void checkUserNotAuthorized() {
webdriver().shouldHave(url(Configuration.baseUrl + "https://dzone.com/"));
$("#login_button_container").shouldBe(Situation.seen);
}
Load Instances, Once more
There’s one other place in our code the place load time is a matter:
@Step
public void openAuthorizationPage() {
// The URL of the web page is already in Configuration.baseUrl
open("");
}
We will not ensure that the web page has been loaded, so an additional verify is required on the finish:
@Step("Open the login page")
public void openAuthorizationPage() {
open("");
inputUsername.shouldBe(Situation.seen);
}
Exams Should Have Checks
Lastly, we have a check the place we’re simply performing an motion with out checking for something:
@Take a look at
public void shouldAuthorizeUser() {
openAuthorizationPage();
authorize();
}
That is pointless —we’re simply “making sure it runs.” The check would not do what’s acknowledged within the title – we do not know if the person has really been approved, so the check would not even verify the blissful path. So now we have so as to add a checkUserAuthorized()
name on the finish. With all the opposite adjustments we have lined above, the brand new check goes to seem like this:
@Take a look at
public void shouldAuthorizeUserWithValidCredentials() {
authorize(trueUsername, truePassword);
checkUserAuthorized();
}
The Remaining Model
return Stream.of(
Arguments.of(“invalid password”, trueUsername, faker.internet().password()),
Arguments.of(“invalid username”, faker.name().username(), truePassword),
Arguments.of(“blank fields”, ” “, ” “)
);
}
}” data-lang=”text/x-java”>
import com.codeborne.selenide.Condition;
import com.codeborne.selenide.Configuration;
import com.codeborne.selenide.SelenideElement;
import com.github.javafaker.Faker;
import io.qameta.allure.Step;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static com.codeborne.selenide.Selenide.*;
import static com.codeborne.selenide.WebDriverConditions.url;
public class GoodE2E {
static Faker faker = new Faker();
SelenideElement inputUsername = $("#user-name");
SelenideElement inputPassword = $("#password");
SelenideElement buttonLogin = $("#login-button");
static String trueUsername = "standard_user";
static String truePassword = "secret_sauce";
@BeforeEach
public void setUp() {
Configuration.baseUrl = "https://www.saucedemo.com";
openAuthorizationPage();
}
@Step("Open the login page")
public void openAuthorizationPage() {
open("");
inputUsername.shouldBe(Condition.visible);
}
@Step("Authorize with credentials: {0}/{1}")
public void authorize(String username, String password) {
inputUsername.setValue(username);
inputPassword.setValue(password);
buttonLogin.click();
}
@Step("Check the product page is opened")
public void checkUserAuthorized() {
webdriver().shouldHave(url(Configuration.baseUrl + "/inventory.html"));
$("[data-test="secondary-header"]").shouldBe(Situation.seen);
}
@Step("Check the user wasn't redirected to the products page")
public void checkUserNotAuthorized() {
webdriver().shouldHave(url(Configuration.baseUrl + "https://dzone.com/"));
$("#login_button_container").shouldBe(Situation.seen);
}
@Take a look at
public void shouldAuthorizeUserWithValidCredentials() {
authorize(trueUsername, truePassword);
checkUserAuthorized();
}
@ParameterizedTest(title = "{0}")
@MethodSource("invalidCredentials")
@DisplayName("User can't authorize with ")
public void shouldNotAuthorizeUserWithInvalidCredentials(String username, String password) {
authorize(username, password);
checkUserNotAuthorized();
}
@Take a look at
public void shouldNotAuthorizeUserWithEmptyInputs() {
buttonLogin.click on();
checkUserNotAuthorized();
}
personal static Stream invalidCredentials() {
return Stream.of(
Arguments.of("invalid password", trueUsername, faker.web().password()),
Arguments.of("invalid username", faker.title().username(), truePassword),
Arguments.of("blank fields", " ", " ")
);
}
}
Conclusion
On the floor, a few of our suggestions may appear contradictory. We have been speaking about avoiding pointless abstractions, the hazards of an excessive amount of DRY, and the significance of holding issues easy — and but most of our refactoring was about eradicating duplication. We have added extra construction than we have eliminated, and our import record has grown fairly a bit.
It is perhaps tempting to say, “There is no silver bullet” — however there sort of is. Take your check remoted from the remainder of the code, and see in case you can learn it with out opening something it calls. See how shortly you may learn it. See if one other individual can learn it. Think about that you have simply needed to learn 10 or 100 such exams. And do every part to cut back the work wanted to determine what’s being examined.
Although, in fact —there is no silver bullet, and instinct that comes with expertise is paramount. So get on testing!