Introduction to Web Test Automation – Java Edition

The internet is an incredible market place hosting millions and millions of software products to make our lives indefinitely easier. To keep up with each other, software grew more and more elaborate to provide us customers with lots of different features and services to finally make us purchase a thing or two. Now due to the resulting increase of complexity we must take special care not to break existing features while developing the next big version brimming full of features. Testing that the next code increment does not break existing stuff is what we testers call „regression testing“, and there is one especially economical way of doing that: test automation.

That’s what we’re going to do today. We gonna test and we gonna automate and we do all that in Java this time. Because everyone knows about Java, right? So let’s write once and run everywhere. Given we have a browser there.

Shopping list

To start our test automation journey – again -, we will work with the following tools. The versions are the ones I used at the time of writing. I might update them in the future.

After downloading and installing these tools from their respective web sites, we’re ready to start our system under test.

Deploying the system under test

To achieve this, we simply run the following command:

docker run --name spree --rm -d -p 3000:3000 spreecommerce/spree:3.6.4

If it went well, a quick glance at http://localhost:3000/ should show a catalog page full of cute Rails merch. Now we will install and prepare Selenium.

Geckodriver installation and management

Contrary to the Python version, we use WebDriverManager to free us from the installation hassle of Geckodriver, hence we are done with this step. The tip was sent to me by my good friend Sho. Thank you, mate. I owe you one! 🙂

Initializing our test automation project

Given a successful Gradle installation as linked in the shopping list we can now create our test project. Please open a terminal in a root directory of your choice and do:

mkdir jata_tutorial && cd jata_tutorial
gradle init

When prompted answer with 1 for a basic project and then 2 for Kotlin as our Gradle DSL.

Of course we will use the new APIs as well, since we love to live on the edge here. Please choose „yes“ here.

Finally, when asked, give your project the default name that is the project root directory’s name. Now your boilerplate project is set up and ready to go.

Now it’s time to define our dependencies. In build.gradle.kts please introduce the following lines:

plugins {
    java
}

group = "your.groupid"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.slf4j:slf4j-api")
    implementation("ch.qos.logback:logback-classic:1.4.5")
    implementation("org.apache.commons:commons-lang3:3.12.0")

    testImplementation("org.seleniumhq.selenium:selenium-java:4.7.2")
    testImplementation("io.github.bonigarcia:webdrivermanager:5.3.1")
    testImplementation(platform("org.junit:junit-bom:5.9.1"))
    testImplementation("org.junit.jupiter:junit-jupiter:5.9.1")
}

tasks.getByName<Test>("test") {
    useJUnitPlatform()
}

This sets up our project with some basic metadata like our groupId and a (somewhat) meaningful version. We defined to use Java as our JVM language and JUnit as our test runner. Additionally we will have access to Maven’s central repository, where Gradle can download our project dependencies that we defined in the section of that exact same name.

With these dependencies, we are able to:

  • automagically download and install webdriver executables for Selenium (in our case: Geckodriver)
  • perform Selenium actions including opening and closing browser windows, enter URLs and clicking stuffs
  • generate random test input strings
  • log things to the console in a nice timestamped format (without being scared of Log4Shell)
  • and do all that in a modern JUnit5 – runner

Everything that we need to do very soon.

The boilerplate

Next up, we create the skeleton of our first test class. In src/test/java, please create a file named CrossTests.java containing the following code. Please note that .java source files should be named after the contained class. Here: CrossTests.

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.WebDriver;

class CrossTests {

    WebDriver driver;

    @BeforeAll
    static void setupClass() {
        
    }

    @BeforeEach
    void setup() {

    }

    @AfterEach
    void teardown() {

    }

    @Test
    void isOpen() {

    }
}

Great. That looks almost like a real test already. Finally we are going to open a browser window.

Starting and closing the browser

In setupClass we call the WebDriverManager and let it work its Geckodriver-initializing magic.

@BeforeAll
static void setupClass() {
    WebDriverManager.firefoxdriver().setup();
}

Then we ensure that a browser is opened before each of the individual tests marked with @Test:

@BeforeEach
void setup() {
    this.driver = new FirefoxDriver();
    this.driver.manage().timeouts().implicitlyWait(Duration.of(5, ChronoUnit.SECONDS));
}

This opens a Firefox window whenever we execute one of the class‘ test methods. This is a very pure and uncustomised instance; we could configure it further if we wanted. But for our purpose at the moment, it is perfectly fine.

Now before we perform any actions on the page, we must make sure that the browser window is closed appropriately after each test run. In teardown, we do:

    @AfterEach
    void teardown() {
        this.driver.quit();
    }

Caveat: confusion potential

Selenium’s WebDriver class has a close()-method, but please always use quit() instead. It performs a clean browser termination with regards to Selenium’s execution flow, whereas close() will just terminate the browser causing you lots of warnings in the log.

Alright! We have taken care of the browser handling. Time to do stuffs with it.

Opening the landing page

The most basic action ist to open a web page. Thankfully this task is handled by Selenium with a simple one-liner. Given that our sample web shop is up and running, we do:

this.driver.get("http://localhost:3000/");

We introduce the line to the test case method isOpen, which we introduced earlier in our boiler plate. But before we head into executing the test case, we have to be aware of the fact: We as the user can see the page being open, but the computer cannot. We have to programmatically verify that our expectation „landing page is open“ is met.

Verifying the outcome

To do that we verify these 3 conditions:

  • the window title contains the landing page’s HTML title
  • the most prominent logo exists on the web page and
  • it’s actually visible (i.e. it does not have its display property set to none).

We add this code directly below the line that opens the website:

assertTrue(this.driver.getTitle().contains("Spree Demo"),
           "'Spree Demo' is not in the browser's title.");

WebElement logoElement = this.driver.findElement(By.id("logo"));
assertTrue(logoElement.isDisplayed());

The first line peeks into the page title, which is another staple of Selenium, and then checks if it contains „Spree Demo“. Using JUnit’s assertTrue, we verify that this is the case, and if not, we print a custom error message. Condition 1 done.

The second line depicts the method you will probably use the most whilst moving forward in your test automation career: Webdriver#findElement goes through the page’s DOM and grabs the element that meets the given criteria. Here we search for an element with the HTML id „logo“. If it fails to find the element, it drops a NoSuchElementError. Therefore, we just implicitly verified condition 2.

The return value is a WebElement object that we can do clicks, inputs and various other actions on. We will use the object’s method isDisplayed() to check if the logo is visible therefore verifying our 3rd condition.

Straight-forward so far. At this point your code should look like that:

import io.github.bonigarcia.wdm.WebDriverManager;
import org.junit.jupiter.api.*;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.firefox.FirefoxDriver;

import java.time.Duration;
import java.time.temporal.ChronoUnit;

import static org.junit.jupiter.api.Assertions.*;

class CrossTests {

    WebDriver driver;

    @BeforeAll
    static void setupClass() {
        WebDriverManager.firefoxdriver().setup();
    }

    @BeforeEach
    void setup() {
        this.driver = new FirefoxDriver();
        this.driver.manage().timeouts().implicitlyWait(Duration.of(5, ChronoUnit.SECONDS));
    }

    @AfterEach
    void teardown() {
        this.driver.quit();
    }

    @Test
    void isOpen() {
        this.driver.get("http://localhost:3000/");
        assertTrue(this.driver.getTitle().contains("Spree Demo"),
                "'Spree Demo' is not in the browser's title.");

        WebElement logoElement = this.driver.findElement(By.id("logo"));
        assertTrue(logoElement.isDisplayed());
    }

}

Let’s execute it. In a terminal window, please switch to the project root and do:

./gradlew test

If everything went well, your console should display a big OK:

BUILD SUCCESSFUL in 7s
4 actionable tasks: 2 executed, 2 up-to-date

Congratulations, you just executed your first Java-Selenium-test:
Our shop’s landing page can be opened and it is displayed correctly.

What is this driver thingy by the way?

Simply put, the driver – in our case Geckodriver – is the glue between our code and Firefox. The same would be valid for Chrome with Chromedriver respectively. This means it is the interface to our browser handling all the requests that we perform :

  • findElement
  • click
  • „are you displayed?“
  • perform key strokes
  • „grab that text of an element“
  • etc. pp.

Under the hood, drivers are http server applications that receive requests sent by Selenium everytime we perform an action similar to these mentioned above. The driver then interacts with the browser in the way we tasked it to and sends a JSON-based response that we can base our future work on. Everything is encapsulated by Selenium so that we can work with simple objects during automating our tests without having to deal with HTTP or JSON.

Performing and validating browser actions

Now that we know the basics of how to write automated tests with Java and Selenium, let’s develop a more complex test.

The best candidates for automation are cross tests. These are tests that cover representative cross sections of the product. One example could be: Put items into the cart, view the cart page and perform the checkout. It is a great practise to start test automation for new projects by coding these cross tests first.

For the sake of brevity here, we will write up the test until reaching the checkout page. The rest is homework. Don’t worry, I got you covered with the full source code here.

Ok, without further ado, here’s the code. Please add the following method to the test class.

@Test
void findSpreeBagAndCheckout()throws InterruptedException{
    this.driver.get("http://localhost:3000/");

    // Landing and catalog page
    this.driver.findElement(By.cssSelector("[href='/t/spree']")).click();
    this.driver.findElement(By.cssSelector("a[href*='spree-bag']")).click();

    // Product page

    this.driver.findElement(By.cssSelector("input#quantity")).clear();
    this.driver.findElement(By.cssSelector("input#quantity")).sendKeys("3");
    this.driver.findElement(By.id("add-to-cart-button")).click();

    // Cart page
    assertTrue(this.driver.getTitle().contains("Shopping Cart"),"'Shopping Cart' is not in the browser's title.");
    this.driver.findElement(By.cssSelector("img[src$='/spree_bag.jpeg']")).isDisplayed();
    String qtyValue=this.driver.findElement(By.cssSelector(".line_item_quantity")).getAttribute("value");
    assertEquals(qtyValue,"3","The item quantity is not the same we typed in!");

    String cartTotal=this.driver.findElement(By.cssSelector(".cart-total > .lead")).getText();
    assertEquals(cartTotal,"$68.97","The total cart value does not match our expectations. That's a blocker!");
}

What’s new here?

If we look back at our first landing page test, you will notice that we use a lot of new things. We will cover them one by one in the next section.

CSS selectors

One thing that catches the eye is that we looked for elements by using CSS expressions. You may have heard about them from your frontend peers, or maybe you have even worked with them yourself already. If not, don’t worry. We will cover them in a followup post. For now, I will give you a few translations:

[href='/t/spree']

„Give me a page element, whose attribute href is equal to ‚/t/spree'“. Quick reminder: href is used in link elements for defining the target URL that is opened, when the user clicks the link.

a[href*='spree-bag']

„Give me any link („anchor“) element, whose attribute href contains ’spree-bag‘.“

img[src$='/spree_bag.jpeg']

„Give me an image element, whose attribute src ends with ‚/spree_bag.jpg‘.“

input#quantity

„Give me an input element, whose HTML id is equal to ‚quantity‘.“ Yes, we could have used By.ID with „quantity“ here, but in this case I wanted to be explicit about the element type input.

.line_item_quantity

„Give me an element that has line_item_quantity in its class attribute.“ The leading dot indicates that we are looking for elements that has the specified class in its class list. It is roughly equivalent to [class*='line_item_quantity'].

.cart-total > .lead

„Give me any element that has the lead class and that’s preceded by any element that has the cart-total class.“

By using CSS selectors you can be very specific about what element you want to use in your test case. This makes CSS selectors incredibly powerful and versatile while maintaining a solid degree of simplicity, especially compared to XPath (ugh).

New properties and methods used in our test case

Aside from CSS selectors, we used several new methods and properties in our automated test.

click() issues a click on the target element. Straight-forward.

.getText() returns the element’s written text. In <p>Some text</p> for example, we would get access to „Some text“.

getAttribute("someHtmlAttribute") gives us the content of an element’s HTML attribute. In our test we fetch an element’s value attribute that is often used to set the content of an input element. Another example might be the link element we talked about earlier. Given we want to have its href attribute for some further verifications; on our page:

<a id="mylink" href="https://www.example.org">an example link</a>

Then in our test we could do:

targetHref = this.driver.findElement(By.id("mylink"))
              .getAttribute("href");
this.doSomeStuffsOn(targetHref);

.sendKeys("My desired input") imitates key strokes on an element. We use it to type any string we want into our target text input element. In our test for example that would be the „quantity“ input box.

And finally, with clear() we have a little helper in our toolbox that removes any content from an input element. In our test we use that to remove the preset value. Otherwise we would accidentally order 31 bags: „3“ from us and „1“ from the value preset by the page.

Continuing your test automation journey from here

Alright, let’s sum up what we accomplished:

We have seen how to open and close a browser window, do various actions on the page and verify their outcomes. Finally, we used that to write a fairly large cross test, all of that on a full fledged ecommerce web app. Great job! Now where should we go next? For the next post, as promised, I’d like to go back one step and give you a deeper introduction to CSS expressions, because they will accompany you for a long time during your test automation journey. Afterwards we have a big problem to solve that you probably already noticed:

Copious amounts of repetition.

To tackle that we will leverage cucumber-jvm, a Behavior Driven Development framework that makes it possible to write test cases in human-readable text form on top of a DRY Java code base. Afterwards we will apply the Page Object Pattern, a test automation – specific design pattern that reduces repetition even more.

Okay! To keep you busy while I am busy, how about finishing the checkout? As a reminder, you can find the full source code in this repository at any time. Alternatively you can spoil yourself a little about Behavior Driven Development frameworks here or try Python instead of Java.

Stay curious and see you in the next post!

Share it with:

Introduction to Web Test Automation in Python

The internet is an incredible market place hosting millions and millions of software products to make our lives indefinitely easier. To keep up with each other, software grew more and more elaborate in order to provide us customers with lots of different features and services to make us purchase a thing or two. Now due to the resulting increase of complexity we must take special care not to break existing features while developing the next big version. Testing that the next code increment does not break existing stuff is what we testers call „regression testing“, and there is one especially economical way of doing that: test automation.

That’s what we’re going to do today. We gonna test and we gonna automate and we do all that in Python, because with its well-designed concepts Python is suitable for beginners and experts alike. So let’s start!

Shopping list

To start our test automation journey, we will work with the following tools. The versions are the ones I used at the time of writing. I might update them in the future.

  • Docker, preferably in its latest version
  • Python 3 version 3.10.8
  • Firefox Browser 106.0.1
  • geckodriver for Firefox 0.32.0
  • a Spree Commerce docker image version 3.6.4 as our system under test
    (will be pulled automagically in the next step)
  • Selenium for Python 4.5.0
  • unittest as our first test runner (comes with Python 3)

After downloading and installing Docker and Python from their respective web sites, we’re ready to start our system under test.

Deploying the system under test

To achieve this, we simply run the following command:

docker run --name spree --rm -d -p 3000:3000 spreecommerce/spree:3.6.4

If it went well, a quick glance at http://localhost:3000/ should show a shopping catalog page full of cute Rails merch. Now we will prepare and install Selenium.

Installing Geckodriver and Selenium

First we need to download geckodriver that will be responsible for sending our actions to the browser. Please see our shopping list for a download page. Download and extract the appropriate archive and put the extracted executable on your PATH. Please do that in a way that fits your OS and your preferences. I’m a mac user, therefore I move it to /usr/local/bin:

$ mv ~/Downloads/geckodriver /usr/local/bin/ 

If everything went well,

$ geckodriver -h 

should print the version and an instruction page. If not, feel free to drop me a Q in the comments below. Otherwise we move on to installing Selenium.

We will leverage pip3 to quickly go through that step:

pip3 install selenium==4.5.0

Now we’re all set. Let’s go headlong into the real trouble.

Test runner boilerplate

First of all, we create the skeleton of our test class. We write a simple Python unittest class that we will fill in with Selenium web test automation goodness as we go further. Our goal is to see the basic structure of a unittest-based automated test case that we will leverage to execute our browser magic. In your favorite project directory, please create a file named cross_tests.py and fill it with the following code. We will talk about the file name a bit later. Don’t worry, it was not a technical decision.

import unittest

# Start with a base class and derive it from 
# unittest.TestCase
class TestSpreeShop(unittest.TestCase):

    # setUp is executed at the start...
    def setUp(self):
        pass

    # ... and tearDown at the end of each test case.
    def tearDown(self):
        pass

    # This will be our first test:
    # We will open the shop's homepage
    # and verify that it worked.
    # For now, we will just let it pass.
    def test_isOpen(self):
        pass


# The main entry point of our unittest-based execution 
# script. Think of it as an actual main method similar 
# to Java's or C's.
if __name__ == '__main__':
    unittest.main()

Great, that almost looks like a real test already. Now finally we are going to open a browser window.

Starting and closing the browser

We write a small support function:

def open_browser(self):
    return webdriver.Firefox()

And we apply it in setUp:

def setUp(self):
    self.driver = self.open_browser()

This opens a Firefox window it its most pure form whenever we execute one of the class‘ test methods. We could configure it further in open_browser, but for our purpose at the moment, this is perfectly fine.

Now before we perform any actions on the page, we must make sure that the browser window is closed appropriately after each test run. In tearDown, we do:

def tearDown(self):
    self.driver.quit()

Caveat: confusion potential

Selenium’s webdriver has a close()-method, but please always use quit() instead. It takes care of clean browser termination with regards to Selenium’s execution, whereas close() will just terminate the browser causing warnings.

Alright! We have taken care of the browser handling. Time to do stuffs with it.

Opening the landing page

The most basic action ist to open a web page. Thankfully this task is handled by Selenium with a simple one-liner. Given our sample web shop is up and running, we do:

self.driver.get("http://localhost:3000/")

We introduce the line to the test case method test_isOpen, which we introduced in our boiler plate. But before we head into executing the test case, we have to be aware of the fact that we as the user can see the page being open, but the computer can’t. We have to programmatically verify that our expectation „landing page is open“ is met.

Verifying the outcome

To do that we verify these 3 conditions:

  • the window title contains the landing page’s HTML title
  • the most prominent logo exists on the web page and
  • it’s actually visible (i.e. it does not have its display property set to none).

We add this code directly below the get – line from before:

self.assertTrue("Spree Demo" in self.driver.title, 
                "'Spree Demo' is not in the browser's title.")

logo_element = self.driver.find_element(By.ID, "logo")
self.assertTrue(logo_element.is_displayed())

The first line peeks into the page title, which is another staple of Selenium, and then checks, if it contains „Spree Demo“ with Python’s in-operator. Using unittest's assertTrue, we verify that this is the case, and if not, we print a custom error message. Condition 1 done.

The second line depicts the method you will probably use the most whilst moving forward in your test automation career: find_element goes through the page’s DOM and grabs the element that meets the given criteria. Here we search for an element with the HTML id „logo“. If it fails to find the element, it drops a NoSuchElementError. Therefore, we just implicitly verified condition 2.

The return value is a WebElement object that we can do clicks, inputs and various other actions on. We will use the object’s method is_displayed() to check, if the logo is visible therefore verifying our 3rd condition.

Straight-forward so far. At this point your code should look like that:

import unittest
from selenium import webdriver
from selenium.webdriver.common.by import By

# Start with a base class and derive it from unittest.TestCase
class TestSpreeShop(unittest.TestCase):

    # setUp is executed at the start...
    def setUp(self):
        self.driver = self.open_browser()

    # ... and tearDown at the end of each test case.
    def tearDown(self):
        self.driver.quit()

    def test_isOpen(self):
        self.driver.get("http://localhost:3000/")
        self.assertTrue("Spree Demo" in self.driver.title, 
                        "'Spree Demo' is not in the browser's title.")

        logo_element = self.driver.find_element(By.ID, "logo")
        self.assertTrue(logo_element.is_displayed())


    # Support method. We will use it to open a browser instance in setUp.
    def open_browser(self):
        return webdriver.Firefox()

# The main entry point of our unittest-based test execution script.
# Think of it as an actual main method similar to Java's or C's.
if __name__ == '__main__':
    unittest.main()

Let’s execute it by doing:

python3 cross_tests.py # Linux or MacOS
python cross_tests.py # Windows

If all went well, your console should display a big OK:

➜  pyta_tutorial git:(main) python3 cross_tests.py
.
----------------------------------------------------------------------
Ran 1 test in 4.719s

OK

Congratulations, you just executed your first Python-Selenium-test:
Our shop’s landing page can be opened and it is displayed correctly.

What is this driver thingy by the way?

Simply put, the driver is the glue between our code and Gecko (or Chromedriver respectively), thus it’s the interface to our browser window. It handles all the requests that we perform like find_element, click, „are you displayed?“, perform key strokes, „grab that text of an element“ etc. pp. Under the hood, Gecko is an http server, which receives requests sent by the driver everytime we perform one of the above actions. The driver then interacts with the browser in the way we tasked it to and sends a JSON-based response object that we can base our future work on. Everything is encapsulated by Selenium so that we can work with simple objects during test automation development without having to deal with HTTP or JSON.

Performing and validating browser actions

Now that we know the basics of how to write automated tests with Python and Selenium, let’s develop a more complex example.

The best candidates for automated tests are cross tests. These are tests that cover representative cross sections of the product. One example could be: Put items into the cart, view the cart page and perform the checkout. This is the reason we named our test file cross_tests.py. It is good practise to start test automation for a new project by coding these cross tests first.

For the sake of brevity here, we will write up the test until the checkout page. The rest is homework. Don’t worry, I got you covered with the full source code here.

Ok, without further ado, here’s the code. Please add the following method to the test class:

def test_findSpreeBagAndCheckout(self):
    self.driver.get("http://localhost:3000/")

    # Landing and catalog page
    self.driver.find_element(By.CSS_SELECTOR, "[href='/t/spree']").click()
    self.driver.find_element(By.CSS_SELECTOR, "a[href*='spree-bag']").click()

    # Product page
    self.driver.find_element(By.CSS_SELECTOR, "input#quantity").clear()
    self.driver.find_element(By.CSS_SELECTOR, "input#quantity").send_keys("3")
    self.driver.find_element(By.ID, "add-to-cart-button").click()
        
    # Cart page
    self.assertTrue("Shopping Cart" in self.driver.title, 
                  "'Shopping Cart' is not in the browser's title.")
    self.driver.find_element(By.CSS_SELECTOR, "img[src$='/spree_bag.jpeg']").is_displayed()
        
    qty_value = self.driver.find_element(By.CSS_SELECTOR, ".line_item_quantity").get_attribute("value")
    self.assertEqual(qty_value, "3", "The item quantity is not the same we typed in!")
        
    cart_total = self.driver.find_element(By.CSS_SELECTOR, ".cart-total > .lead").text
    self.assertEqual(cart_total, "$68.97", "The total cart value does not match our expectations. That's a blocker!")

What’s new here?

If we look back at our first landing page test, you will notice that we use a lot of new things. We will cover them one by one now.

CSS selectors

One thing that catches the eye is that we looked for elements by using CSS expressions. You may have heard about them from your frontend peers, or maybe you have even worked with them yourself already. If not, don’t worry. We will cover them in a followup post. For now, I will give you a few translations:

[href='/t/spree']

„Give me a page element, whose attribute href is equal to ‚/t/spree'“. Quick reminder: href is used in link elements for defining the target URL that is opened, when the user clicks the link.

a[href*='spree-bag']

„Give me any link element, whose attribute href contains ’spree-bag‘.“

img[src$='/spree_bag.jpeg']

„Give me an image element, whose attribute src ends with ‚/spree_bag.jpg‘.“

input#quantity

„Give me an input element, whose HTML id is equal to ‚quantity‘.“ Yes, we could have used By.ID with „quantity“ here, but I wanted to be explicit about the element type input.

.line_item_quantity

„Give me an element that has line_item_quantity in its class attribute.“ The leading dot indicates that we are looking for elements that has the specified class. It is roughly equivalent to [class*='line_item_quantity'].

.cart-total > .lead

„Give me any element that has the lead class and that’s preceded by any element that has the cart-total class.“

By using CSS selectors you can be very specific about what element you want to use in your test case. This makes CSS selectors incredibly powerful and versatile while maintaining a solid degree of simplicity, especially compared to XPath (Ugh).

New properties and methods used in our test case

Aside from CSS selectors, we used several new methods and properties in our new automated test.

click() issues a click on the target element. Straight-forward.

.text contains the element’s written text. In <p>Some text</p> for example, we would get access to „Some text“.

get_attribute("someHtmlAttribute") gives us the content of an element’s HTML attribute. In our test we fetch an element’s value attribute that is often used to set the content of an input element. Another example might be the link element we talked about earlier. Given we want to have its href attribute for some further verifications. On our page:

<a id="mylink" href="https://www.example.org">an example link</a>

Then in our test we could do:

target_href = self.driver.find_element(By.ID,"mylink")
              .get_attribute("href")
self.do_some_shenanigans_on(target_href)

send_keys("My desired input") imitates key strokes on an element. We use it to type any string we want into our target text input element. In our test, that would be the „quantity“ input box.

And finally, with clear() we have a little helper that clears an input element from any content. In our test we use that to remove the preset value. Otherwise we would accidentally order 31 bags: „3“ from us and „1“ from the value preset by the page.

Continuing from here

Alright, let’s sum up what we accomplished:

We have seen how to open and close a browser window, do various actions on the page, verify their outcomes and finally we used that to write a fairly large cross test. And all of that on a full fledged ecommerce web app. Great job! Now where should we go next? For the next post, as promised, I’d like to go back one step and give you a deeper introduction to CSS expressions, because they will accompany you for a long time during your test automation journey. Afterwards we have to tackle a big problem you probably already noticed:

Copious amounts of repetition.

To tackle that we will leverage Behave, a Behavior Driven Development framework that makes it possible to write test cases in human-readable text form on top of a DRY Python code base. Afterwards we will apply the Page Object Pattern, a test automation – specific design pattern to reduce repetition even more.

Okay. To keep you busy while I am busy, how about finishing the checkout? As a reminder, you can find the full source code in this repository. Or you can spoil yourself a little about Behavior Driven Development frameworks here.

See you in the next post!

Share it with:

Google Playstore Releases: Things I Wish I Knew Earlier

So recently I released my first app in the Google Playstore: a simple real estate calculator for the German market that goes by the name Hauschecker. I happily and avidly coded, did some changes here, made some learnings there, and eventually an MVP was finished that looked awful, but did its job. In accordance to Eric Ries‘ awesome book „The Lean Startup“*, I want to release fast and start gathering feedback as quickly as possible. So I went to the Google Play Console ready to upload my created-with-love app bundle…

…and… yeah.

Google Playstore poses quite a challenge

But the good thing with challenges is: You learn and you get better. And Playstore-releases are not that much different from running a marathon, getting a productive appointment in a German Bürgeramt or, well, developing an app. Therefore let’s head into the things I learned the hard way so you don’t have to.

A trivial one for starters: An app icon is required.

And with „required“, I mean required-HTML-multipart-form-field-required. A form field with that nasty little asterisk character ‚*‘ that tells you there will be no going-forward without filling it in. And as if that’s not enough, Google has a very specific opinion about how it’s supposed to look like: 32bit PNG -format with 512×512 px, and God forbid it’s larger than 1meg. And in addition it hast to follow Google’s icon design specs, which we happened to fulfill apparently. In addition, we cannot use the Flutter default app icon, as Google has the copyright. (Oh btw, I use Flutter. 🙂)

Cost: about 20 bucks. Maybe creating a simple dummy here is possible. Memo to myself for the next app.

This one’s a bit nastier: A feature graphic is required.

The next thing I was confronted with is the concept of feature graphics, that is a required form field, too. And again: Google is very specific about that picture: a 1024×500 px JPEG or 24bit PNG without alpha. Okay..

I get that it makes sense: It explains what the app is supposed to do, conveys feelings and shows use cases in an intuitive way, and in addition it plays a major role, if you have a marketing video for your app. But it was certainly a thing to make – or rather have it made.

Cost: about 40 bucks.

25$ for a Google Playstore dev account

This one hit me cold: Google Paystore (pun semi-intended) charges you a one time fee of 25$. At first it was a surprise to me, but on the other hand it’s still cheap compared to what Apple is doing with their 99 bucks a year. I clenched my teeth, paid and moved on.

Cost: well, 25 bucks.

A little bit of text to the mix

Google also wants us to provide a short and a long description. Okay. I would be a lousy internet entrepreneur-in-training, if I couldn’t explain my idea with a few words, so that’s fine by me.

Be prepared to bring screenshots

Another important (and required) Playstore asset is screenshots. Screenshots for phone screens as well as 7“ and 10.5“ tablet screens. To create these screenshots I ran the app in a Chrome instance using the CLI command flutter run first. Then I copied the localhost link to the resulting webapp and pasted it into a Firefox, because I prefer its dev and screenshot tools. As an alternative you could use appropiate phone and tablet emulators with a desktop snipping tool, or you could use Chrome directly, but all these were too much of an unnecessary hassle for me. 🙂 So let’s continue with Firefox. We open the dev tools, click on the little devices icon to the right and choose an appropiate phone or „ipad“ for the respective device sizes. The webapp is shown in the specific dimensions and now we use its screenshot tool by right-clicking into the app and use the „Take screenshot“ function. We change some inputs and take another one. Rinse and repeat until we have the required 2 screenshots for each device type.

Ironically, as diligent as Google usually is regarding their specifications and form field validations, they let me do this:

Misplaced phone screenshot within the device outline provided by and shown in the Google Playstore.

Thanks, Google…

Playstore Queries & Questionaires

Be prepared for a huge interview about various different topics of major and minor significance; Google wants to know eeeverything. I mean, child protection!? I just want to do some basic percetage calculation. But it can’t be helped… we have to push through. Thankfully, most of the questions are basic multiple choices and checkboxes. They won’t impose too much of a hassle. But it was a surprise for me as a firsttimer.

Sometimes you win, sometimes you learn – and then you win

Phew, that was a journey. My first release for Hauschecker 1.0.0 took me a whooping two and a half weeks, and I had to delay a lot of things in life (and spend a significant amount of money) to make it happen. But I pushed through, and now having a full-fledged working Android app in the Google Playstore is such a great feeling I can’t describe. In fact, it was so great I recently pushed version 1.0.1. So for today I hope I could show you my learnings and hopefully you will make it in half the time it took me. If not, feel free to drop me a question in the comments below, and if you are interested in another big accomplishment I achieved in my career, you might like to read on here. This one tells about the ISTQB Test Management Certificate and the perks it brings to the table.

Have a great day & happy coding!

*) Affiliate Link

Share it with:

AccessDenied in psutil

psutil is an interesting Python package that provides us with valuable insights about running processes, their memory and CPU usage and many more key aspects for monitoring and profiling processes across all major platforms. Thus, it is an incredibly useful tool for system admins, developers and testers alike. Now one of my favourite methods is cmdline(). Defined in the package’s Process class, it yields the whole CLI command of a running process in array form. There is one important catch on Windows though that got me kinda by surprise: While iterating casually through all my processes I suddenly got an AccessDenied error.

What did I try?

For an advanced verification in a process management – related test case I wanted to print the executed CLI command in a log file, so basically this:

import psutil

for pc in psutil.process_iter():
    print(pc.cmdline())

But that will just throw a painful AccessDenied error at us. Uff.

Why did that happen?

The reason is that psutil is quite consequent: It really lists every running process. That means even SYSTEM or root processes . That’s okay, it might even be interesting in one case or another, and you can still access selected attributes like the name() of a root process, if you want. But for me, it doesn’t suffice. I want to see the full-fledged cmdline(), but I understand that SYSTEM processes are none of my business. Once I came to term with that fact, the solution was easy: We just skip them.

The solution

What I did was applying a try-except around the loop’s inner statement:

import psutil

for pc in psutil.process_iter():
    try:
        print(pc.cmdline())
    except psutil.AccessDenied:
        continue

The continue statement will make sure that the processes that I’m not supposed to see are happily skipped without hurting the rest of the program flow.

But what if I need to monitor foreign processes?

In that case, we would need to execute the script within the process owner’s user context. That might be a bit fiddly in Windows depending on the use case, but of course that’s still possible. Just remember to keep the try-except block, because there still will be processes you wouldn’t be allowed to see.

Mine, for example. 😄

Conclusion

So far for today. I hope this little Q3A (quick question quick answer) could shed some light upon that surprising AccessDenied error. If you want to learn more about psutil, I strongly recommend the readthedocs page. Otherwise, if you want to see more quick tips, I have one more for Python here. That one covers Python’s environment variable handling. As an alternative, if you are – like me – into containers, here is a handy docker ps trick useful for monitoring tasks as well.

Happy coding!

Share it with:

Python Environment Variables: getenv() vs. environ[ ]

Last week, I was about to execute a test run using one of our functional test suites that requires a certain environment variable – and I forgot to set it, whoops.. The answer was harsh and generic: A good ol‘ Python KeyError. That put me up with two questions:

  1. What options do I have to get environment variables with Python and
  2. which one is the best?

Let’s start checking them out.

Introducing os.environ[key]

The first option is a simple dictionary that is prefilled with all environment variables when starting the Python process. This provides us with a simple-to-use interface that we already know and love.

Let’s try it out:

For python environment variables with os.environ[key], do the following: export MY_VAR="test", python3 -i, import os and os.environ["MY_VAR"]. This should yield 'test'.
Python environment variables with os.environ[key]

But what happens, when we query a variable that is not set? Then we hopefully prepared our try-except Block, because that call is going down the river faster than sound. This happens because dictionaries throw generic KeyErrors, when the queried key could not be found in the dict. That’s what happened in my test run.

os.getenv(key, default=None)

The second option follows a more higher-level approach by providing a function that takes our key in question and an optional default value. The usage is straight forward and it won’t crash as harshly as the first option does.

Let’s try it out:

For python environment variables with os.getenv(key, default), do the following: export MY_VAR="test", python3 -i, import os and os.getenv("MY_VAR"). This should yield 'test'. If you query MY_VAR2 instead, it should yield None. If you do os.getenv("MY_VAR2", "default"), it should yield default.
Python environment variables with os.getenv and default value handling

As we can see, we can now use any key we want; worst case is a result of None. But the increased flexibility comes with a price: We have to take care of whatever the function returns. Even harder, due to the fact that the default defaults to None (pun semi-intended), this solution is prone to hidden bugs. Therefore use it with care and set an appropriate default value if possible.

Conclusion: Which one works best for you?

We saw both options now, but which one works best (for you)? At the end of the day, it comes down to preference regarding two factors:

  1. Do you like your code to be more low-level or high-level?
  2. Do you want your fails fast or more controlled?

This is a decision you have to make, but once you have it, you find everything you need to get your environment variables with Python right here.

So long for the quick journey into my Python life. Usually I talk more about Rust as seen here and here, but since Python is my language on the job, there will definitely be more coming up soon. So if you are a friend of british comedy, feel free to stay tuned. Additionally, since container technologies are getting more and more important in my day job as well, I will write more about these techs, too, starting with this little trick.

Happy holidays and a merry Christmas Eeve! 🎄

Share it with:

docker ps formatting – Cheat Sheet

Checking the status of Docker containers is an important starting point, when it comes to debugging container clusters. docker ps is an ideal tool to get you started. Unfortunately, the output can sometimes be quite verbose and overwhelming.

The --format parameter is supposed to ease the pain here, but Go templates can be quite tedious to write every time, too. To counter that a little, I reveal my favorite ready-to-alias docker ps calls in today’s post. I hope they make your everyday Docker life a little easier. 🙂

# Are my expected containers up?
docker ps --format "table {{.Names}}\t{{.Status}}"

# What ports do my containers expose?
docker ps --format "table {{.Names}}\t{{.Ports}}"

# What networks are my containers assigned to?
docker ps --format "table {{.Names}}\t{{.Networks}}"

All of them can be extended with more docker ps command line parameters, even when aliased later on.

As you may have noticed, my first column is always the container name followed by the property I’d like to analyse. This is my personal way to keep the output compact and focused on the problem at hand, but of course you can query any property you like. Check out the official documentation for a comprehensive list of available properties and pick what you need for the task.

Do you have a favourite formatting? Or do you want to talk about any other question? Feel free to let me know in the comments below. I plan to publish more cheat sheet-style posts in the future. This helps me out to share recent learnings in a focused way and provides you with quick help and valuable insights. You can find another example here, where I demonstrate how to set up Linux users with low maintenance work afterwards. Or if you are into Rust like I am, this one might be a valuable companion, when it comes to structuring your code with Rust Modules.

Until next time!

Share it with:

Create a New User in Linux

User creation is one of the most common task in traditional Linux system administration. It is so popular that there are even two command line utilities: adduser (Debian and its derivates) and useradd (all). And since it’s so popular many different administrators have many different solutions for the same problem.

Here I present mine. Note that I prefer sudo over working as root, but the steps work with root as well. Just omit the sudos.

  1. Create a personalised user group.
    $ sudo groupadd newuser
  2. Create the user with a home directory, a good default shell, its user group and further desired groups assigned.
    $ sudo useradd -m -s /bin/bash -g newuser -G sudo,orga newuser
  3. Set the user’s initial password.
    $ sudo passwd newuser

Step summary for your automation needs

sudo groupadd newuser
sudo useradd -m -s /bin/bash -g newuser -G sudo,orga newuser
sudo passwd newuser

Prepare SSH access for our new Linux user

If required, we can now enable the user to connect to our machine via SSH. We assume that the SSH demon is up and running and the user’s public key is available on our machine.

  1. Change into the user’s home directory.
    $ sudo cd /home/newuser/
  2. Create the .ssh directory.
    $ sudo mkdir .ssh
  3. Set the directory permissions of .ssh to 0700 (read-write-execute only for the user).
    $ sudo chmod 0700 .ssh/
    Note that the execute permission is necessary to access the directory; read does not suffice. This might be a little counterintuitive for new admins. (It surely was for me. 😅)
  4. Change into our meticulously prepared directory.
    $ sudo cd .ssh/
  5. cat the user’s public key into a new file called authorized_keys.
    sudo cat /path/to/pubkey > authorized_keys
  6. Set the authorized_keys file permissions to 0600.
    sudo chmod 0600 authorized_keys
  7. Make sure that everything we created is assigned to the new user.
    cd ..
    sudo chown -R newuser:newuser .ssh/

    Now the user should be able to connect to us with SSH.

Step summary for your automation needs

sudo cd /home/newuser/
sudo mkdir .ssh
sudo chmod 0700 .ssh/
sudo cd .ssh/
sudo cat /path/to/pubkey > authorized_keys
sudo chmod 0600 authorized_keys
  • cd .. sudo chown -R newuser:newuser .ssh/
  • Conclusion

    User creation is a staple task when working with Linux servers, and now you have a solid solution at hand that will lighten up your daily workload. So long. I hope this little sheet was useful to you. Usually I write about Rust, test automation and business topics. If you want to read more from me, this post combines the former two. In my day job, I also do lots of test automation, but here my focus is Java. Hence, if you are rather interested in that, I’m talking about fuzzing with Java in this post.

    Or if you miss a detail, or you want to get your hands a little dirtier than just cheat sheet level, Linuxize has an in-depth post about the nitty gritty parts of user administration. It really helped me out a lot in the past.

    Have a nice day!

    Share it with:

    Cucumber-rust since 0.7 – The Most Important Changes

    cucumber-rust has had a long way, since my last post about the 0.7 release in October 2020. It’s time to come back and see what happened since back then. First of all, starting from the initial 0.8.0 release, I will dig through the changelog and evaluate my favorite changes. Then we will update the Cucumber tests of my encrspyter project to the most recent version. Lots of stuff to do, so let’s go!

    New things first

    Let’s start soft: With 0.8.4, we got a --debug command line flag that leverages the test execution to nicely print stdout and stderr for each executed step. We can activate the debug mode in the runner creation code of our test’s main function:

    fn main() {
        let runner = cucumber::Cucumber::<EncrsypterTestWorld>::new()
            .features(&["./tests/features/"])
            .steps(encrypt_decrypt_steps::steps())
            .debug(true); // This activates the new debug mode 
        ...
    }

    By running cargo test, we can see it in action:

    Cucumber-rust's Debug mode produces sections in the test's cli output called Captured stdout and Captured stderr respectively. Captured stdout contains stdout text in white, Captured stderr contains stderr text in blue.

    Neat, right? 🙂

    t!-Macro extended with a World parameter type

    Tiny but neat addition: We can now add the type of our Cukes World-object to the t!-closure.

    t!(|mut world: MyPersonalCukesWorld, ctx| { [...] }

    Although the generated code is the same as without the explicit type, it adds a bit more Rust-style expressivity. Sweet!

    New callback methods for the Cucumber runner: before and after

    In vanilla Cucumber, I admired its feature to define hooks that intercept the execution of a feature or a scenario. You can write some code and tell Cucumber to execute it before, after or before and after a scenario, feature or even a step. This is useful to for example set up or tear down a test database before or respectively after a test run.

    With the release of 0.9.0, we can do similar things in Rust, too. There is a significant implementation difference to vanilla Cukes though: Our hooks won’t be picked up from wherever they are defined, but are defined as properties of the Cucumber runner instead. To compensate, our before and after hooks come with powerful query options to decide where to execute the defined method.

    The second difference is that they are not officially called „hooks“ but „lifecycle methods“ instead. I might get this wrong due to habits. Please bear with me. 😄

    Lets head into an example. Given 2 features, one of them in English, one of them in German, each in 2 separate files:

    # Feature 1 (English description)
    Feature: Encrypt messages and write them to a file.
    
      Scenario: Encrypt a simple Hello World - message.
        Given I have an encryptor initialized with input "Hello World!"
         When I test print to STDOUT
          And I test print to STDERR
         Then I should see "Hello World!" in the test encryptor's input field
         When I encrypt the encryptor's input
         Then testfile.txt exists
          And testfile.txt is not empty
         When I decrypt testfile.txt
         Then the decrypted result should be "Hello World!"
    
    # language: de
    # Feature 1 (German description)
    Funktionalität: Verschlüssele Nachrichten und schreibe sie in eine Datei.
    
      Beispiel: Encrypt a simple Hello World - message.
        Angenommen I have an encryptor initialized with input "Hello World!"
         Wenn I test print to STDOUT
          Und I test print to STDERR
         Dann I should see "Hello World!" in the test encryptor's input field
         Wenn I encrypt the encryptor's input
         Dann testfile.txt exists
          Und testfile.txt is not empty
         Wenn I decrypt testfile.txt
         Dann the decrypted result should be "Hello World!"
    

    What we want to do now is get greeted and dismissed in the respective language. We will define proper lifecycle methods on our Cucumber runner to do that. In the main method:

        let english_feature_name = "Encrypt messages and write them to a file."; // full string filter for the English...
        let german_feature_pattern = Regex::new("Verschlüssele Nachrichten.*").unwrap(); // and a Regex filter for the German variant.
    
    let runner = cucumber::Cucumber::<world::EncrsypterTestWorld>::new()
    .features(&["./tests/features/"])
            .steps(crate::encrypt_decrypt_steps::steps())
            .language("de") 
            .before(feature(english_feature_name), |_ctx| {
                async { println!("Greetings, encryptor!") }.boxed()
            })
            .after(feature(english_feature_name), |_ctx| {
                async { println!("Goodbye, encryptor!") }.boxed()
            })
            .before(feature(german_feature_pattern.clone()), |_ctx| { // clone is necessary here due to the trait bounds of Inner<Pattern>
                async { println!("Hallo, Verschlüsselnder.") }.boxed()
            })
            .after(feature(german_feature_pattern), |_ctx| {
                async { println!("Tschüss, Verschlüsselnder.") }.boxed()
            });

    feature() expects either the full feature description as a &str or a valid regex::Regex() matching your targets‘ description string. The latter requires the regex module as a dependency in your Cargo.toml, but it will provide you a highly powerful filtering tool, so adding that additional dependency is highly recommended.

    Executing cargo test will show us what we expect. For the English feature file:

    Greetings, encryptor!
    Feature: Encrypt messages and write them to a file.
    
    [...]
    
      ✔ Then the decrypted result should be "Hello World!"                                                                
    Goodbye, encryptor!

    For the German Feature file:

    Hallo, Verschlüsselnder.
    Funktionalität: Verschlüssele Nachrichten und schreibe sie in eine Datei.
    
    [...]
    
      ✔ Dann the decrypted result should be "Hello World!"                                                               
    Tschüss, Verschlüsselnder.

    Great stuff! Last but not least, let me note that this does not only work with Feature, but with Scenario and Rule, too. You can even create more custom filters by combining them with And and Or. Please refer to the cucumber-rust code base for more about that.

    Heads up, a breaking change! 👷🏻‍♂️

    With 0.9.0 we got one significant change in Cukes‘ public API, but don’t worry: Fixing it is quickly done and even quite easily automatable. If you review my guide on cucumber-rust for 0.7, you will see the related step definitions written like this:

    .given_regex_async(
        r#"^I have an encryptor initialized with input "([\w\s!]+)"$"#,
        t!(|mut world, texts_to_encrypt, _step| {
            world.encryptor.input = Cow::Owned(texts_to_encrypt[1].to_owned());
            world
        }),
    )

    This throws a compiler error now stating that the „signature“ of the t! macro has changed: Instead of the regex matches object in parameter #2 and _step in parameter #3, we now have a single StepContext object that contains the properties matches and step.

    Therefore, in the above example we have to do the following:

    1. Remove the _step parameter entirely
    2. Rename our matches parameter texts_to_encrypt to something that reflects the StepContext type: ctx
    3. Replace the occurrences of texts_to_encrypt with ctx.matches[index_used_previously]

    For _step we have no replacements to do, because we didn’t use it in the first place, so that’s basically it. The runnable step definition should now look like this:

    .given_regex_async(r#"^I have an encryptor initialized with input "([\w\s!]+)"$"#, t!(|mut world, ctx| {
                    world.encryptor.input = Cow::Owned(ctx.matches[1].to_owned());
                    world
        }),
    )

    Personally I like this particular change quite a lot, because it keeps the already loaded t! macro clean and organised. What do you think? Feel free to let me know in the comments below.

    Feature: Add before and after lifecycle functions to the Cucumber builder. This function takes a selector for determining when to run 'before' or 'after', and a callback
    
    Feature: add language argument to Cucumber builder to set default language for all feature files (ON HOLD)
    

    Encrsypter’s Cucumber tests in a new look

    I updated the tests in Encrsypter’s project master and in the cukes_0.9.0 branch, so if you want to see the changes in full action, give it a git pull on the master or a git checkout on the mentioned branch and enjoy. 🙂

    Conclusion: great changes and improvements

    Phew, so long. cucumber-rust really does have a long way, and many things have changed for more Cukes excitement. Personally I like the current implementation state really a lot and I’m looking forward to seeing its bright future. But for now, let’s wrap up the wrapup, shall we?

    If you want to read more about Cukes in Rust, here’s my intro to Cucumber in Rust written for 0.7. Or you might say „meh, I prefer the vintage things of life, give me the vanilla stuff“. In that case, you can find the original version of my intro guide here.
    And last but for sure not least, here’s the project’s full changelog with all the goodness listed. Happy cuking!

    Share it with:

    Rust Modules Cheat Sheet

    If you struggle as regularly with Rust’s super explicit modules as I do, then welcome to the club. For us poor souls, I decided to write down a quick cheat sheet about how to structure our modularized code.

    Limitations

    This post focuses on the directory and file structure only. It does not cover use, mod, crate:: et. al. in-depth. If you would like me to cover them, feel free to let me know in the comments. 🙂

    The Module Setup

    Consider a generic cargo new – generated project with one module we want to expose. We will have the following participating files and folders named in a very generic way here. These are listed from deep inside the project tree to higher levels:

    1. file1.rs, file2.rs,... that contain code we want to expose
      1. public functions and structs are written here using the keyword pub at the beginning of their declaration
      2. macros come with their #[macro_export] anyways, so no further changes needed
    2. a directory src/folder that contains the files from 1.
    3. a file src/folder.rs that lists all files within folder that we want to expose by doing:
      1. pub mod file1;
      2. pub mod file2;
    4. our main.rs (or lib.rs, depending of your app design) that references the exposed code by doing:
      1. mod folder;
      2. use crate::folder::file1::pub_function1
      3. use crate::folder::file1::pub_function2
      4. use crate::folder::file2::pub_function1
      5. use crate::folder::file2::pub_function2
      6. ….

    Please check this repo for an in-action-demo. We will use the maths module next. The macros module is a bonus for you. 😄 Here’s a quick mapping of the file and folder names according to the schema above:

    1. src/maths/functions1.rs, src/maths/functions2.rs
    2. src/maths/
    3. src/maths.rs
    4. src/main.rs

    When we review those files, we will see that they do exactly, what the schema expects us to.

    Conclusion

    I hope this little sheet will help you to remember the complex structure of Rust’s modules. If you have an improvement idea, or if you found a defect, please feel free to drop it in the comments – I watch the comment sections regularly – or send me an email.

    Have great day! 🦀

    Share it with:

    One Year (and a few months) of Leadership

    On last year’s November 1st, I earned a very special achievement: 1 full year in a leadership position leading a group of 3 smart & experienced QA Engineers. This is quite a biggie of a milestone for my professional and personal career and I’m incredibly thankful for my team and my boss for putting so much trust in me. It is still early 2021, thus it is time to use the cold wintery days to reflect on what I have learned and why I chose this career path in the first place.

    Why did I pick up the leadership path?

    I’m a natural supporter; the more I can help others to be successful, the happier I am. But as an individual contributor, you are responsible for your own stuff. You have to do your tasks and focus on your work. Of course doing your own tasks helps your team mates, your lead and the company in general, but that’s an indirect effect. If you want to help people directly to succeed, you need to shift your daily work’s focus towards that. This grants you time and space to listen to your peer’s needs, to abstract away the company politics and to make sure that your peers can work in a clean and focused environment. Applying these principles, I noticed strong growth in collaboration and an increase in speaking-up on a high technical QA level.

    This is where I want to draw my own professional satisfaction from.

    Now thing with people is that they want to get recognized for their own personal achievements. I’m no different here, hence changing my priorities was a tough lesson to learn. But that’s OK. It’s part of the challenge, and in the long run it is the right way for me.

    What is leadership NOT about?

    Hot take: Picking up a leadership role is not a promotion, but a completely different career path. Even if you lead the same way as I do doing day-to-day tasks after management, you have too many tasks that are different from your handson-stuffs to do, and thus require a different skill set. Let me give you a personal example.

    My job then…

    For me as a testing professional, the IT business world is red or green. Something is implemented according to the specs or it is not. A test is either green or red (i.e. not yellow). Either the test or the app failed. You get the image. Much more important: Things can be proven right or wrong. We have specs, acceptance criteria and various different models for that.

    … vs. now

    This is history. Now I take on decisions, with their outcome being at least most of the time uncertain in the short term. It can take weeks or even months until I know, if my decision was correct. And if so: to what degree? Even correct decisions are not a 1 on the logical output wire. Rather, the truth lies somewhere in between; an aspect you have to constantly deal with. That’s OK, but it’s a significant difference we must be aware of. The next point is that it comes with a change in your skillset: You must stay confident in times of uncertainty.

    Consequently, you cannot tell other people that your decision X is correct. You have to point out advantages, tradeoffs and set the decision into context to actually convince people that your decision is the best for the job at hand. Sure, as individual contributors we have freedom within a certain frame depending on our respective leads, but your audience will be different and it is certainly going to be larger. Therefore, we have again a shift in our skillset.

    These are my 2 most prominent examples on the take why I think that being an IC and being a lead are two completely different jobs. What do you think about it? Agree or disagree? Let me know in the comments below.

    How do I want to lead?

    They say „Sometimes a picture says more than 1000 words“, and when you type „leader vs boss“ into Google, you eventually stumble upon this masterpiece of a motivational sort-of-a-meme-thingy:

    Peers pulling the boss and the business forward vs. the leader that pulls the business together with his peers, the latter being my leadership style orientation.
    Boss vs. Leader. Shoutouts to whoever made this gem.
    If you know the artist, please drop a link the comments. 🙂

    The bottom part sums up my aspired style of leadership so well that I had to post it here.
    To implement the depicted style, I apply six key principles:

    Write up a lean agile process

    I try keeping our testing process as lean and clutter-free as possible by forming few key principles and write them down in our project – Confluence as a manifesto. These principles have to be lived by day by day, but nothing more and nothing less.

    Write it up together with your peers

    The key principles of our testing process are living documents that are formed by the team (including myself). We discuss things we want to do regularly and reliably together, put them into Confluence, observe them and adapt them if necessary. This also forces us to keep the principles few and short to prevent us from overengineering the process. A valuable feedback loop.

    Give your peers as much freedom as possible

    Once the key principles are in place, I give the team as much freedom in their day to day work as possible. Basically, I set priorities to the tasks and recommend (!) assignments. Anything else is up to the team. The task assignments can change freely between the team members, as the team sees fit.

    Trust, trust & again trust

    Trust is the mortar of a working human relationship, and organizational relationships are no exception. As such, they require mutual trust like any other relationship. Trust is an important part of respect we owe the peers we hired to get the job done. If you don’t trust them, they will notice, and they – justified – won’t be happy about that. So please make sure that you pay your peers as much trust as possible. You as a lead want to be trusted, too, right?

    As a great side effect, trusting your peers will give the whole team a huge amount of freedom. That means lots of freedom for you, too! Valuable free time to focus on all the outside requests that drop into your mail box every day.

    Be part of your team

    By now, I talked a lot about the team members. When I do that, I include myself. I am a part of my team. I work at their side and implement a „we“-perspective into our day-to-day work. This keeps up the motivation and changes the way people inside and outside your team will see your team’s efforts.

    Work together

    Setting up the process chores once and only doing minor modifications from time to time, you should have some time left. I use this time to grab tasks. I am careful what to pick though, because I did a huge mistake in the past: By misunderstanding the block-removing part of the leadership job, I tended to pick the biggest and nastiest fundamental tasks available. As a result, it took me aaaages to finish them, because I just don’t have fulltime hours to work on them. I have meetings, have to answer questions, mails etc. That’s the major part of the job now. To fix that, I decided to give these tasks to the team, trust them and only do support on demand. In the meantime, I pick simple non-blocking tasks, e.g. automating single not-too-critical tests. Even these simple tasks increase your team’s free time and are highly appreciated inside and outside.

    Conclusion

    So long, these were my learnings of one full year of the adventure that is called leadership. And what an adventure it is. I absolutely recommend it to you in any case: Try it out, get a grip on leadership and experience firsthand, how much you grow as a person and as a professional. If you like it, that’s awesome. If not, thats Ok, too. I even got you covered for this case. But you should definitely give it a try. Your future self will thank you for that, I promise.

    Also, if you are especially curious about how my first entrepreneurial challenge turned out, check out my most recent blog post, where I write about my experience publishing an app to the Google Playstore. Stay curious, everybody!

    Share it with: