• Skip to main content
EuroSTAR 2027 - Sign up for early access

EuroSTAR Conference

Europe's Largest Software Testing Conference.

  • Programme
    • Call for Speakers
    • 2026 Programme
    • Community Hub
    • Awards
  • Attend
    • Why Attend
    • Bring your Team
    • Testimonials
  • Sponsor
    • Sponsor Opportunities
    • Sponsor Testimonials
  • About
    • About Us
    • Our Timeline
    • FAQ
    • Blog
    • Organisations
    • Contact Us
  • Book Now

DevOps

Embracing Crowdtesting for Quality Assurance: A Strategic Imperative for Software Development

May 22, 2024 by Lauren Payne

In an era marked by rapid digital transformation, the quality of software products has emerged as a linchpin of success for companies across the globe. Digital natives, who represent future generations, are shaping market trends more than ever before, leading to a high demand for flawless, easy-to-use, and feature-packed products. In today’s evolving landscape, organizations need to rethink their approach to do quality assurance (QA) and product testing, recognizing the necessity of integrating native quality management with crowdtesting methodologies. This holistic integration ensures comprehensive coverage and adaptability to meet the demands of today’s dynamic market.

The Imperative for a Quality-Centric Culture

The cost of neglecting quality in software development can be staggering. Companies that fail to cultivate a culture deeply rooted in quality management face not only financial losses from rectifying errors but also damage to their brand reputation and customer trust. A quality-centric culture is not merely about detecting and fixing bugs; it’s about embedding quality into every phase of the development lifecycle, from initial design to final release and further iterations. Adopting a native quality management approach involves seamlessly integrating QA processes with development workflows, ensuring that QA and development teams collaborate closely.

Crowdtesting: Leveraging the Power of Real-World Feedback

As the digital landscape becomes increasingly user-driven, understanding and meeting the diverse needs of various user segments is critical. Crowdtesting emerges as a powerful solution to this challenge, enabling organizations to test their products in real-world scenarios across the big number of devices, operating systems, and user environments. This approach not only validates the functionality and usability of products but also uncovers nuanced insights into user preferences and behaviors, facilitating a deeper connection with the target audience. Crowdtesting bridges the gap between theoretical QA and practical, user-centric validation. By engaging a targeted group of users from the intended market segment, companies can gather actionable feedback on their products’ performance, usability, and appeal. This method provides a more nuanced understanding of subjective user experiences, enabling developers to refine their products in ways that resonate with their audience’s expectations and preferences.

Integrating Quality Management and Crowdtesting

The integration of native quality management and crowdtesting represents a comprehensive strategy for achieving excellence in software development. This dual approach ensures that quality is not only baked into the development process but also validated through extensive, real-world testing. By measuring quality maturity and incorporating crowdtesting feedback early and throughout the product lifecycle, companies can anticipate and mitigate potential issues, streamline their development processes, and enhance product quality. Such an integrated approach also fosters a culture of continuous improvement and innovation. As teams become more aligned on quality objectives and gain insights from direct user feedback, they are better equipped to make informed decisions, prioritize features, and deliver products that truly meet, if not exceed, user expectations.

Conclusion: The Future of Software Development is User-Driven

The digital age demands a new paradigm in software development—one that places quality and user experience at the forefront. By embracing a quality-centric culture and integrating crowdtesting into the product development lifecycle, companies can navigate the complexities of modern software development more effectively. This strategic imperative not only enhances product quality and user satisfaction but also positions companies for sustained success in a competitive digital marketplace. As we look to the future, crowdtesting will undoubtedly become a cornerstone of successful software development. It promises not just better products but also a deeper understanding of the ever-evolving digital consumer, ensuring that companies can continue to innovate and thrive in the digital age.

Figure. 1: The next Level of digital excellence: Embrace Crowdtesting

Author


Stephan Ingerberg, Head of Sales, msg Test & Quality Management

Stephan Ingerberg is a seasoned professional with over a decade of experience in the realm of software quality and digital assurance. He is a dedicated desciple of quality and testing since 2004.

Currently serving as a pivotal figure in the Test & Quality Management division of msg, responsible  for sales, customer relations and commercial aspects within central Europe. His unwavering dedication to excellence and adept navigation of software quality make him indispensable in the pursuit of digital perfection.

https://www.linkedin.com/in/stephan-ingerberg-digital-transformation/

msg Test & Quality Management is an Exhibitor at EuroSTAR 2024, join us in Stockholm.

Filed Under: DevOps Tagged With: 2024, EuroSTAR Conference, Expo

What our tests don’t like about our code

May 3, 2024 by Lauren Payne

When you start writing tests for your code, you’ll likely have the feeling – bloody hell, how do I drag this thing into a test? There is code that tests clearly like, and code they don’t. Apart from checking the correctness of our code, tests also give us hints about how to write it. And it’s a good idea to listen.

A test executes your code in the simplest possible setting, independent of the larger system it’s part of. But if the simplest possible setting is how it’s run in our app, and it’s impossible to tease out the individual pieces – that’s a bad sign. If we’re saying – nah, we don’t need tests, all the code is already executed in the app – that’s a sign that we’ve created a large slab that is hard to change and maintain. As Uncle Bob put it:

“another word for testable is decoupled.”

It’s been said plenty of times that good architecture gets us good testability. Let’s come at this idea from another angle: what hints do our tests give about our architecture? We’ve already talked about how tests help prevent creeping code rot – now, we’ll explore this idea in a particular example.

As a side note, we’ll talk mostly about tests that developers write themselves – unit tests, the first line of defense.

Our example is going to be a primitive Python script that checks the user’s IP, determines their region, and tells the current weather in the region (the complete example is available here). We’ll write tests for that code and see how it gets improved in the process. Each major step is in a separate branch.

Step 1: a quick and dirty version

Our first version is bad and untestable.

def local_weather():
    # First, get the IP
    url = "https://api64.ipify.org?format=json"
    response = requests.get(url).json()
    ip_address = response["ip"]

    # Using the IP, determine the city
    url = f"https://ipinfo.io/{ip_address}/json"
    response = requests.get(url).json()
    city = response["city"]

    with open("secrets.json", "r", encoding="utf-8") as file:
        owm_api_key = json.load(file)["openweathermap.org"]

    # Hit up a weather service for weather in that city
    url = (
        "https://api.openweathermap.org/data/2.5/weather?q={0}&"
        "units=metric&lang=ru&appid={1}"
    ).format(city, owm_api_key)
    weather_data = requests.get(url).json()
    temperature = weather_data["main"]["temp"]
    temperature_feels = weather_data["main"]["feels_like"]

    # If past measurements have already been taken, compare them to current results
    has_previous = False
    history = {}
    history_path = Path("history.json")
    if history_path.exists():
        with open(history_path, "r", encoding="utf-8") as file:
            history = json.load(file)
        record = history.get(city)
        if record is not None:
            has_previous = True
            last_date = datetime.fromisoformat(record["when"])
            last_temp = record["temp"]
            last_feels = record["feels"]
            diff = temperature - last_temp
            diff_feels = temperature_feels - last_feels

    # Write down the current result if enough time has passed
    now = datetime.now()
    if not has_previous or (now - last_date) > timedelta(hours=6):
        record = {
            "when": datetime.now().isoformat(),
            "temp": temperature,
            "feels": temperature_feels
        }
        history[city] = record
        with open(history_path, "w", encoding="utf-8") as file:
            json.dump(history, file)

    # Print the result
    msg = (
        f"Temperature in {city}: {temperature:.0f} °C\n"
        f"Feels like {temperature_feels:.0f} °C"
    )
    if has_previous:
        formatted_date = last_date.strftime("%c")
        msg += (
            f"\nLast measurement taken on {formatted_date}\n"
            f"Difference since then: {diff:.0f} (feels {diff_feels:.0f})"
        )
    print(msg)


if __name__ == "__main__":
    local_weather()

[source]

Let’s not get into why this is bad code; instead, let’s ask ourselves: how would we test it? Well, right now, we can only write an E2E test:

def test_local_weather(capsys: pytest.CaptureFixture):  
    local_weather()  
  
    assert re.match(  
        (            
            r"^Temperature in .*: -?\d+ °C\n"  
            r"Feels like -?\d+ °C\n"  
            r"Last measurement taken on .*\n"  
            r"Difference since then: -?\d+ \(feels -?\d+\)$"  
        ),  
        capsys.readouterr().out  
    )

[source]

This executes most of our code once – so far, so good. But testing is not just about achieving good line coverage. Instead of thinking about lines, it’s better to think about behavior – what systems the code manipulates and what [the use cases are].

So here’s what our code does:

– it calls some external services for data;

– it does some read/write operations to store that data and retrieve previous measurements;

– it generates a message based on the data;

– it shows the message to the user.

But right now, we can’t test any of those things separately because they are all stuffed into one function.

In other words, it will be tough to test the different execution paths of our code. For instance, we might want to know what happens if the city provider returns nothing. Even if we’ve dealt with this case in our code (which we haven’t), we’d need to test what happens when the value of city is None. Currently, doing that isn’t easy.

– You could physically travel to a place that the service we use doesn’t recognize – and, while fun, this is not a viable long-term testing strategy.

– You could use a mock. Python’s requests-mock library lets you make it so that requests doesn’t make an actual request but returns whatever you told it to return.

While the second solution is less cumbersome than moving to a different city, it’s still problematic because it messes with global states. For instance, we wouldn’t be able to execute our tests in parallel (since each changes the behavior of the same requests module).

If we want to make code more testable, we first need to break it down into separate functions according to area of responsibility (I/O, app logic, etc.).

Step 2: Creating separate functions

Our main job at this stage is to determine areas of responsibility. Does a piece of code implement the application logic, or some form of IO – web, file, or console? Here’s how we break it down:

# IO logic: save history of measurements
class DatetimeJSONEncoder(json.JSONEncoder):
    def default(self, o: Any) -> Any:
        if isinstance(o, datetime):
            return o.isoformat()
        elif is_dataclass(o):
            return asdict(o)
        return super().default(o)


def get_my_ip() -> str:
    # IO: load IP from HTTP service
    url = "https://api64.ipify.org?format=json"
    response = requests.get(url).json()
    return response["ip"]


def get_city_by_ip(ip_address: str) -> str:
    # IO: load city by IP from HTTP service
    url = f"https://ipinfo.io/{ip_address}/json"
    response = requests.get(url).json()
    return response["city"]


def measure_temperature(city: str) -> Measurement:
    # IO: Load API key from file
    with open("secrets.json", "r", encoding="utf-8") as file:
        owm_api_key = json.load(file)["openweathermap.org"]

    # IO: load measurement from weather service
    url = (
        "https://api.openweathermap.org/data/2.5/weather?q={0}&"
        "units=metric&lang=ru&appid={1}"
    ).format(city, owm_api_key)
    weather_data = requests.get(url).json()
temperature = weather_data["main"]["temp"]
    temperature_feels = weather_data["main"]["feels_like"]
    return Measurement(
        city=city,
        when=datetime.now(),
        temp=temperature,
        feels=temperature_feels
    )


def load_history() -> History:
    # IO: load history from file
    history_path = Path("history.json")
    if history_path.exists():
        with open(history_path, "r", encoding="utf-8") as file:
            history_by_city = json.load(file)
            return {
                city: HistoryCityEntry(
                    when=datetime.fromisoformat(record["when"]),
                    temp=record["temp"],
                    feels=record["feels"]
                ) for city, record in history_by_city.items()
            }
    return {}


def get_temp_diff(history: History, measurement: Measurement) -> TemperatureDiff|None:
    # App logic: calculate temperature difference
    entry = history.get(measurement.city)
    if entry is not None:
        return TemperatureDiff(
            when=entry.when,
            temp=measurement.temp - entry.temp,
            feels=measurement.feels - entry.feels
        )
def save_measurement(history: History, measurement: Measurement, diff: TemperatureDiff|None):
    # App logic: check if should save the measurement
    if diff is None or (measurement.when - diff.when) > timedelta(hours=6):
        # IO: save new measurement to file
        new_record = HistoryCityEntry(
            when=measurement.when,
            temp=measurement.temp,
            feels=measurement.feels
        )
        history[measurement.city] = new_record
        history_path = Path("history.json")
        with open(history_path, "w", encoding="utf-8") as file:
            json.dump(history, file, cls=DatetimeJSONEncoder)


def print_temperature(measurement: Measurement, diff: TemperatureDiff|None):
    # IO: format and print message to user
    msg = (
        f"Temperature in {measurement.city}: {measurement.temp:.0f} °C\n"
        f"Feels like {measurement.feels:.0f} °C"
    )
    if diff is not None:
        last_measurement_time = diff.when.strftime("%c")
        msg += (
            f"\nLast measurement taken on {last_measurement_time}\n"
            f"Difference since then: {diff.temp:.0f} (feels {diff.feels:.0f})"
        )
    print(msg)


def local_weather():
    # App logic (Use Case)
    ip_address = get_my_ip() # IO
    city = get_city_by_ip(ip_address) # IO
    measurement = measure_temperature(city) # IO
    history = load_history() # IO
    diff = get_temp_diff(history, measurement) # App
    save_measurement(history, measurement, diff) # App, IO
    print_temperature(measurement, diff) # IO

[source]

Notice that we now have a function that represents our use case, the specific scenario in which all the other functions are used: local_weather(). Importantly, this is also part of app logic; it specifies how everything else should work together.

Also note that we’ve introduced dataclasses to make return values of functions less messy: Measurement, HistoryCityEntry, and TemperatureDiff. They can be found in the new [typings module].

As a result of the changes, our code has become more cohesive – all stuff inside one function mostly relates to doing one thing. By the way, the principle we’ve applied here is called the [Single-responsibility principle] (the “S” from [SOLID].

Of course, there’s still room for improvement – e.g., in measure_temperature, we do both file IO (read a secret from disk) and web IO (send a request to a service).

Let’s recap:

– we wanted to have separate tests for things our code does;

– that got us thinking about the responsibilities for different areas of our code;

– by making each piece have just a single responsibility, we’ve made them testable

So, let’s write the tests now.

Tests for step 2

@pytest.mark.slow
def test_city_of_known_ip():
    assert get_city_by_ip("69.193.168.152") == "Astoria"


@pytest.mark.fast
def test_get_temp_diff_unknown_city():
    assert get_temp_diff({}, Measurement(
        city="New York",
        when=datetime.now(),
        temp=10,
        feels=10
    )) is None

A couple of things to note here.

Our app logic and console output execute an order of magnitude faster than IO, and since our functions are somewhat specialized now, we can differentiate between fast and slow tests. Here, we do it with custom pytest marks (pytest.mark.fast) defined in the project’s [config file]. This is useful, but more on it later.

Also, take a look at this test:

@pytest.mark.fast
def test_print_temperature_without_diff(capsys: pytest.CaptureFixture):
    print_temperature(
        Measurement(
            city="My City",
            when=datetime(2023, 1, 1),
            temp=21.4,
            feels=24.5,
        ),
        None
    )

    assert re.match(
        (
            r"^Temperature in .*: -?\d+ °C\n"
            r"Feels like -?\d+ °C$"
        ),
        capsys.readouterr().out
    )

Note that before, we’d have to drag the whole application along if we wanted to check print output, and manipulating output was very cumbersome. Now, we can pass the print_temperature function whatever we like.

Problem: fragility

Our tests for high-level functionality call details of implementations directly. For instance, the E2E test we’ve written in step 1 (test_local_weather) relies on the output being sent to the console. If that detail changes, the test breaks.

This isn’t a problem for a test written specifically for that detail (like test_print_temperature_without_diff [here] – it makes sense we need to change it if the feature has changed.

However, our E2E test wasn’t written to test the print functionality; nor was it written specifically for testing the provider. But if the provider changes, the test breaks.

We might also want to change the implementation of some functions – for instance, break down the  measure_temperature() method into two to improve cohesion. A test calling that function would break.

All in all, our tests are fragile. If we want to change our code, we also have to rewrite tests for that code, which means a higher cost of change.

Problem: dependence

This is related to the previous problems. If our tests call the provider directly, then any problem on their end means our tests will crash.

If the IP service is down for a day, then our tests won’t be able to execute any code inside local_weather() that runs after determining IP – and we won’t be able to do anything about it. And if you’ve got a problem with internet connection, none of the tests will run at all, even though the code might be fine.

Problem: can’t test the use case

On the surface – yes, the tests do call local_weather(), which is our use case. But they don’t test that function specifically, they just execute everything there is in the application. Which means it’s difficult to read the results of such a test, it will take you more time to understand where the failure is localized. Test results should be easy to read.

Problem: excessive coverage

One more problem is that with each test run, the web and persistence functions get called twice: by the E2E test from step 1 and by the more targeted tests from step 2.

Excessive coverage isn’t great – for one, the services we’re using count our calls, so we better not make them willy-nilly. Also, if the project continues to grow, our test base will get too slow.

All these problems are related, and to solve them, we need to write a test for our coordinating functions that doesn’t invoke the rest of the code. To do that, we’d need test doubles that could substitute for real web services or writing to the disk. And we’d need a way to control what specific calls our functions make. 

Step 3: Decoupling dependencies

To achieve those things, we have to write functions that don’t invoke stuff directly but instead call things passed to them from the outside – i.e., we [inject dependencies].

In our case, we’ll pass functions as variables instead of specifying them directly when calling them. An example of how this is done is presented in [step 3]

def save_measurement(
    save_city: SaveCityFunction, # the IO call is now passed from the outside
    measurement: Measurement,
    diff: TemperatureDiff|None
):
    """  
    If enough time has passed since last measurement, save measurement. 
    """
    if diff is None or (measurement.when - diff.when) > timedelta(hours=6):
        new_record = HistoryCityEntry(
            when=measurement.when,
            temp=measurement.temp,
            feels=measurement.feels
        )
        save_city(measurement.city, new_record)

Before, in step 2, the save_measurement function contained both app logic (checking if we should perform the save operation) and IO (actually saving). Now, the IO part is injected. Because of this, we now have more cohesion: the function knows nothing about IO, its sole responsibility is your app logic.

Note that the injected part is an abstraction: we’ve created a separate type for it, SaveCityFunction, which can be implemented in multiple ways. Because of this, the code has less coupling. The function does not depend directly on an external function; instead, it relies on an abstraction that can be implemented in many different ways.

This abstraction that we’ve injected into the function means we have inverted dependencies: the execution of high-level app logic no longer depends on particular low-level functions from other modules. Instead, both now only refer to abstractions.

This approach has plenty of benefits:

– reusability and changeability – we can change e. g. the function that provides the IP, and execution will look the same

– resistance to code rot – because the modules are less dependent on each other, changes are more localized, so growing code complexity doesn’t impact the cost of change as much

– and, of course, testability

 Importantly, we did it all because we wanted to run our app logic in tests without executing the entire application. In fact, why don’t we write these tests right now?

Tests for step 3

So far, we’ve applied the new approach to save_measurement – so let’s test it. Dependency injection allows us to write a test double that we’re going to use instead of executing actual IO:

@dataclass
class __SaveSpy:
    calls: int = 0
    last_city: str | None = None
    last_entry: HistoryCityEntry | None = None


@pytest.fixture
def save_spy():
    spy = __SaveSpy()
    def __save(city, entry):
        spy.calls += 1
        spy.last_city = city
        spy.last_entry = entry
    yield __save, spy

This double is called a spy; it records any calls made to it, and we can check what it wrote afterward. Now, here’s how we’ve tested save_measurement with that spy:

@pytest.fixture
def measurement():
    yield Measurement(
        city="New York",
        when=datetime(2023, 1, 2, 0, 0, 0),
        temp=8,
        feels=12,
    )

@allure.title("save_measurement should save if no previous measurements exist")
def test_measurement_with_no_diff_saved(save_spy, measurement):
    save, spy = save_spy

    save_measurement(save, measurement, None)

    assert spy.calls == 1
    assert spy.last_city == "New York"
    assert spy.last_entry == HistoryCityEntry(
        when=datetime(2023, 1, 2, 0, 0, 0),
        temp=8,
        feels=12,
    )


@allure.title("save_measurement should not save if a recent measurement exists")
def test_measurement_with_recent_diff_not_saved(save_spy, measurement):
    save, spy = save_spy

    # Less than 6 hours have passed
    save_measurement(save, measurement, TemperatureDiff(
        when=datetime(2023, 1, 1, 20, 0, 0),
        temp=10,
        feels=10,
    ))

    assert not spy.calls


@allure.title("save_measurement should save if enough time has passed since last measurement")
def test_measurement_with_old_diff_saved(save_spy, measurement):
    save, spy = save_spy

    # More than 6 hours have passed
    save_measurement(save, measurement, TemperatureDiff(
        when=datetime(2023, 1, 1, 17, 0, 0),
        temp=-2,
        feels=2,
    ))
 assert spy.calls == 1
    assert spy.last_city == "New York"
    assert spy.last_entry == HistoryCityEntry(
        when=datetime(2023, 1, 2, 0, 0, 0),
        temp=8,
        feels=12,
    )

[source]

Note how much control we’ve got over save_measurement. Before, if we wanted to test how it behaves with or without previous measurements, we’d have to manually delete the file with those measurements – yikes. Now, we can simply use a test double.

There are plenty of other advantages to such tests, but to fully appreciate them, let’s first achieve dependency inversion in our entire code base.

Step 4: A plugin architecture

At this point, our code is completely reborn. Here’s our central module, app_logic:

def get_temp_diff(
    last_measurement: HistoryCityEntry | None,
    new_measurement: Measurement
) -> TemperatureDiff|None:
    if last_measurement is not None:
        return TemperatureDiff(
            when=last_measurement.when,
            temp=new_measurement.temp - last_measurement.temp,
            feels=new_measurement.feels - last_measurement.feels
        )


def save_measurement(
    save_city: SaveCityFunction,
    measurement: Measurement,
    diff: TemperatureDiff|None
):
    if diff is None or (measurement.when - diff.when) > timedelta(hours=6):
        new_record = HistoryCityEntry(
            when=measurement.when,
            temp=measurement.temp,
            feels=measurement.feels
        )
        save_city(measurement.city, new_record) # injected IO


def local_weather(
    get_my_ip: GetIPFunction,
    get_city_by_ip: GetCityFunction,
    measure_temperature: MeasureTemperatureFunction,
    load_last_measurement: LoadCityFunction,
    save_city_measurement: SaveCityFunction,
    show_temperature: ShowTemperatureFunction,
):
    # App logic (Use Case)
    # Low-level dependencies are injected at runtime
    # Initialization logic is in __init__.py now
    # Can be tested with dummies, stubs and spies!

    ip_address = get_my_ip() # injected IO
    city = get_city_by_ip(ip_address) # injected IO
    if city is None:
raise ValueError("Cannot determine the city")
    measurement = measure_temperature(city) # injected IO
    last_measurement = load_last_measurement(city) # injected IO
    diff = get_temp_diff(last_measurement, measurement) # App
    save_measurement(save_city_measurement, measurement, diff) # App (with injected IO)
    show_temperature(measurement, diff) # injected IO

[source]

Our code is like a Lego now. The functions are assembled when the app is initialized (in __init__.py), and the central module just executes them. As a result, none of the low-level code is referenced in the main module, it’s all hidden away in sub-modules [console_io], [file_io], and [web_io]. This is what dependency inversion looks like: the central module only works with abstractions.

The specific functions are passed from elsewhere – in our case, the __init__.py module:

def local_weather(
    get_my_ip=None,
    get_city_by_ip=None,
    measure_temperature=None,
    load_last_measurement=None,
    save_city_measurement=None,
    show_temperature=None,
):
    # Initialization logic
    default_load_last_measurement, default_save_city_measurement =\
        file_io.initialize_history_io()
    return app_logic.local_weather(
        get_my_ip=get_my_ip or web_io.get_my_ip,
        get_city_by_ip=get_city_by_ip or web_io.get_city_by_ip,
        measure_temperature=measure_temperature or web_io.init_temperature_service(
            file_io.load_secret
        ),
        load_last_measurement=load_last_measurement or default_load_last_measurement,
        save_city_measurement=save_city_measurement or default_save_city_measurement,
        show_temperature=show_temperature or console_io.print_temperature,
    )

As a side note, initialization is here done with functions (file_io.initialize_history_io() and web_io.init_temperature_service()). We could just as easily have done the same with, say, a WeatherClient class and created an object of that class. It’s just that the rest of the code was written in a more functional style, so we decided to keep to functions for consistency.

To conclude, we’ve repeatedly applied dependency inversion through dependency injection on every level until the highest, where the functions are assembled. With this architecture, we’ve finally fully decoupled all the different areas of responsibility from each other. Now, every function truly does just one thing, and we can write granular tests for them all.

Tests for step 4

[Here’s the final version] of our test base. There are three separate modules here:

– e2e_test – we only need one E2E test because our use case is really simple. We’ve written that test at step 1.

– plugin_test – those are tests for low-level functions; useful to have, but slow and fragile. We’ve written them at step 2.

– unit_test – that’s where all the hot stuff has been happening.

The last module became possible once we introduced dependency injection. There, we’ve used [all kinds of doubles]:

  • dummies – they are simple placeholders
  • stubs – they return a hard-coded value
  • spies – we’ve talked about them earlier

They allow us a very high level of control over high-level app logic functions. Before, they would only be executed with the rest of our application. Now, we can do something like this:

Exercising our use case

@allure.title("local_weather should use the city that is passed to it")
def test_temperature_of_current_city_is_requested():    
    def get_ip_stub(): return "1.2.3.4"  
    def get_city_stub(*_): return "New York"  
    captured_city = None  
  
    def measure_temperature(city):  
        nonlocal captured_city  
        captured_city = city  
        # Execution of local_weather will stop here  
        raise ValueError()  
    
    def dummy(*_): raise NotImplementedError()  
  
    # We don't care about most of local_weather's execution,  
    # so we can pass dummies that will never be called    
    with pytest.raises(ValueError):  
        local_weather(  
            get_ip_stub,  
            get_city_stub,  
            measure_temperature,   
            dummy,  
            dummy,  
            dummy  
        )  
  
    assert captured_city == "New York"

We’re testing our use case (the local_weather function), but we’re interested in a very particular aspect of its execution – we want to ensure it uses the correct city. Most of the function is untouched.

Here’s another example of how much control we have now. As you might remember, our use case should only save a new measurement if more than 6 hours have passed since the last measurement.

How do we test that particular piece of behaviour? Before, we’d have to manually delete existing measurements – very clumsy. Now, we do this:

@allure.title("Use case should save measurement if no previous entries exist")  
def test_new_measurement_is_saved(measurement, history_city_entry):  
    # We don't care about this value:  
    def get_ip_stub(): return "Not used"  
    # Nor this:  
    def get_city_stub(*_): return "Not used"  
    # This is the thing we'll check for:  
    def measure_temperature(*_): return measurement  
    # With this, local_weather will think there is  
    # no last measurement on disk:    
    def last_measurement_stub(*_): return None  
  
    captured_city = None  
    captured_entry = None  
  
    # This spy will see what local_weather tries to  
    # write to disk:    
    def save_measurement_spy(city, entry):  
        nonlocal captured_city  
        nonlocal captured_entry  
        captured_city = city  
        captured_entry = entry  
  
    def show_temperature_stub(*_): pass  
  
    local_weather(  
        get_ip_stub,  
        get_city_stub,  
        measure_temperature,  
        last_measurement_stub,  
        save_measurement_spy,  
        show_temperature_stub,  
    )  
  
    assert captured_city == "New York"  
    assert captured_entry == history_city_entry

We can control the execution flow for local_weather() to make it think there’s nothing on disk without actually reading anything. Of course, it’s also possible to test for opposite behavior – again, without any IO (this is done in test_recent_measurement_is_not_saved()). These and other tests check all the steps of our use case, and with that, we’ve covered all possible execution paths.

A test base with low coupling

The test base we’ve built has immense benefits.

Execution speed

Because our code has low coupling and our tests are granular, we can separate the fast and slow tests. In pytest, if you’ve created the custom “fast” and “slow” marks as we’ve discussed above, you can run the fast ones with a console command:

pytest tests -m "fast"

Alternatively, you could do a [selective run from Allure Testops] – pytest custom marks are automatically converted into Allure tags, so you can select tests by the “fast” tag.

The fast tests are mainly from the unit_test module – it has the “fast” mark applied globally to the entire module. How can we be sure that everything there is fast?

Because everything in that module is decoupled, you can unplug your computer from the internet, and it will run just fine. Here’s how quick the unit tests are compared to those that have to deal with external resources: 

We can easily run these fast tests every time we make a change, so if there is a bug, we’ll know it’s in the code we’ve written since the recent test run.

Longevity

Another benefit to those quick tests we’ve just made is longevity. Unfortunately, throwing away unit tests is something you’ll inevitably have to do. In our case, thanks to interface segregation and dependency injection, we’re testing a small abstract plug point and not technical implementation details. Such tests are likely to survive longer.

Taking the user’s point of view

Any test, no matter what level, forces you to look at your code from the outside, see how it might be taken out of the context where it was created to be used somewhere else.

With low-level tests, you extract yourself from the local context, but you’re still elbow-deep in code. However, if you’re testing an API, you’re taking the view of a user (even if that user is future you or another programmer). In an ideal world, a test always imitates a user.

A public API that doesn’t depend on implementation details is a contract, a promise to the user – here’s a handle, it won’t change (within reason). If tests are tied to this API (as our unit tests are), writing them makes you view your code through that contract. You get a better idea of how to structure your application. And if the API is clunky and uncomfortable to use, you’ll see that, too.

Looking into the future

The fact that tests force you to consider different usage scenarios also means tests allow you to peek into the future of your code’s evolution.

Let’s compare step 4 (inverted dependencies) with step 2 (where we’ve just hidden stuff into functions). The major improvement was decoupling, with its many benefits, including lower cost of change.

But at first sight, step 2 is simpler, and in Python, simple is better than complex, right? Without tests, the benefits of dependency inversion in our code would only become apparent if we tried to add more stuff to the application.

Why would they become apparent? Because we’d get more use cases and need to add other features. That would both expose us to code rot and make us think. We’d see the cost of change. Well, writing tests forces you to consider different use cases here and now.

This is why the structure we’ve used to benefit our tests turns out to be super convenient when we need to introduce other changes into code. To show that, let’s try to change our city provider and output.

Step 4 (continued): Changing without modifying

New city provider

First, we’ll need to write a new function that will call our new provider:

def get_city_by_ip(ip: str):
    """Get user's city based on their IP"""

    url = f"https://geolocation-db.com/json/{ip}?position=true"
    response = requests.get(url).json()
    return response["city"]

[source]

Then, we’ll have to call local_weather() (our use case) with that new function in __main__.py:

 local_weather(get_city_by_ip=weather_geolocationdb.get_city_by_ip)

Literally just one line. As much as possible, we should add new code, not rewrite what already exists. This is such a great design principle that it got its own name, the open/closed principle.

We were led to that principle because we wanted a test to run our app logic independently of the low-level functions. As a result, our city provider has become a technical detail that can be exchanged at will.

Also, remember that these changes don’t affect anything in our test base. We can add a new test for the new provider, but the existing test base runs unchanged because we’re adding, not modifying.

New output

Now, let’s change how we show users the weather. At first, we did it with a simple print() call. Then, to make app logic testable, we had to replace that with a function passed as a variable. It might seem like an unnecessary complication we’ve introduced just for the sake of testability. But what if we add a simple UI and display the message there?

It’s a primitive Tkinter UI; you can [take a look at the code here], but the implementation doesn’t matter much right now, it’s just a technical detail. The important thing is: what do we have to change in our app logic? Literally nothing at all. Our app is just run from a different place (the Tkinter module) and with a new function for output:

def local_weather():  
    weather.local_weather(  
        show_temperature=show_temperature  
    )

This is triggered by a Tkinter button.

It’s important to understand that how we launch our application’s main logic is also a technical detail; it doesn’t matter that it’s at the top of the stack. From the point of view of architecture, it’s periphery. So, again, we’re extending, not modifying.

Conclusion

Testability and SOLID

Alright, enough fiddling with the code. What have we learned? Throughout this example, we’ve been on a loop:

  • we want to write tests
  • but we can’t because something is messy in the code
  • so we rewrite it
  • and get many more benefits than just testability

We wanted to make our code easier to test, and the SOLID principles helped us here. In particular:

  • Applying the single responsibility principle allowed us to run different behaviors separately, in isolation from the rest of the code.
  • Dependency inversion allowed us to substitute expensive calls with doubles and get a lightning-fast test base.
  • The open/closed principle was an outcome of the previous principles, and it meant that we could add new functionality without changing anything in our existing tests.
  • It’s no big surprise why people writing about SOLID mention testability so much: after all, the SOLID principles [were formulated by the man who was a major figure in Test-Driven Development].

But TDD is a somewhat controversial topic, and we won’t get into it here. We aren’t advocating for writing tests before writing code (though if you do – that’s awesome). As long as you do write tests and listen to testers in your team – your code will be better. Beyond having fewer bugs, it will be better structured.

Devs need to take part in quality assurance; if the QA department just quietly does its own thing, its efficiency is greatly reduced. As a matter of fact, it has been measured that

“having automated tests primarily created and maintained either by QA or an outsourced party is not correlated with IT performance.” 

Tests give hints on how well-structured your code is

Let’s clarify causation here: we’re not saying that testability is at the core of good design. It’s the other way round; following engineering best practices has good testability as one of many benefits.

What we’re saying is writing tests gets you thinking about those practices. Tests make you look at your code from the outside. They make you ponder its changeability and reusability – which you then achieve through best engineering practices. Tests give you hints about improving the coupling and cohesion of your code. If you want to know how well-structured your code is, writing tests is a good, well, test.

Authors

Artem Eroshenko CPO & Co-founder of Qameta Software

Artem Eroshenko, CPO and
Co-Founder Qameta Software

Maksim Stepanov Software Developer

Maksim Stepanov, Software Developer Qameta Software

Mikhail Lankin Content Writer Qameta Software

Mikhail Lankin, Content Writer Qameta Software

Allure Report is an Exhibitor at EuroSTAR 2024, join us in Stockholm

Filed Under: DevOps Tagged With: 2024, EuroSTAR Conference, Expo

World Quality Report 2022-2023: Orchestrating Quality in Agile Organizations

May 10, 2023 by Lauren Payne

Thanks to Sogeti for providing us with this blog post.

With the ongoing evolution of Agile and DevOps addressing the need to release more and release faster, quality assurance plays a vital role at every stage of the release cycle.

In the 14th Edition of the World Quality Report, we see quality in the agile and DevOps environment viewed like an orchestra. Every element of the software development process comes together in harmony to complete the finished piece, like a perfect musical performance where quality is assured.

This is referred to as quality orchestration whereby the teams, skillsets, provisioning, automation practices, service virtualization, and more are viewed – and managed – as a seamless end-to-end whole. Collaboration in this orchestrated agile environment is key. It’s how test and quality engineers ensure their activities deliver the two most important quality objectives for the agile enterprise – excellent customer experience and business outcomes.

Our latest World Quality Report survey found that agile and DevOps were delivering benefit in line with these two objectives at many levels. For example, when asked if they had seen ‘significant’ improvements (i.e., more than 20%) since adopting agile and DevOps, 64% of the survey respondents said they had seen improvement in the area of on-time delivery, 63% in predictability, 62% in reducing the cost of their quality activities, and 61% in customer experience.

Agile Adoption Continues to Grow

This chapter of the WQR also assesses the evolution of agile and DevOps and the tools/approaches used to assure quality. For example, we discover that although the agile implementation of packaged enterprise systems has been slow to take off, with waterfall being the predominant methodology for many years, agile adoption has started to grow. Indeed, 59% of the survey respondents now have a well-implemented agile methodology for quality and testing. Quality is being further assured by a number of different approaches, such as pre-built test case repositories for certifying sprints, which has been adopted by 63% of the survey respondents.

When it comes to enterprise systems, testing isn’t always carried out by quality engineers. Some 62% of surveyed organizations say it is carried out by business SMEs. Nonetheless the skills of quality engineers remain integral to agile teams with 32% of organizations saying quality engineers make up between 26% to 35% of their agile teams, and 28% of organizations saying their agile teams comprise even more quality engineers at between 36% to 45% of the team make-up.

Recommendations for Success

As quality orchestration increases within the software development lifecycle (SDLC), the WQR looks ahead to possible future developments, such as a need for much higher levels of automation and quality as the pace of continuous quality grows. It also makes several recommendations for ensuring agile and DevOps success across the SDLC, such as making quality engineers integral to agile development programs, and blending both technical and business skills within the broader quality engineering skillset.

Get in Touch

If you’d like to hear more about our findings relating to quality orchestration in agile and DevOps, please get in touch with:

Author

Bart Vanparys

Practice Lead, Quality Engineering & Testing, Sogeti

Bart has carried many titles in his 20 year career. He’s been an analyst, tester, quality assurance consultant, test manager, project manager, BI developer, quality manager, change manager, CoE lead, program quality lead.

A constant has been his search for ways to deliver value through IT solutions in a controlled and safe manner. He has gained experience in many domains including testing, quality management, service management, project management and architecture. He believes that a broad background combined with deep specialized expertise in selected domain is essential to be valuable in our industry.

Bart’s specialty is testing and quality engineering. His broad interest is in everything else. Bart graduated as Commercial Engineer (KU Leuven, Belgium) and has worked in IT consultancy since 2000. In 2011, he joined Capgemini Belgium where he took a lead role in building the Sogeti Testing & Quality Management practice. He has performed assignments in public sector (European Commission), finance and retail. He is currently supporting organizations in building testing & quality engineering capabilities.

Sogeti is an EXPO Gold partner at EuroSTAR 2023, join us in Antwerp

Filed Under: Agile, DevOps Tagged With: 2023, EuroSTAR Conference

Why Do Testers Need CI/CD Systems?

April 19, 2023 by Lauren Payne

Thanks to JetBrains for providing us with this blog post.

This post was originally published on the JetBrains Qodana Blog.

Competency in the TestOps field is now just as much an essential requirement for QA engineers as the ability to write automated tests. This is because of the ongoing development of CI/CD tools and the increasing number of QA engineers who work with pipelines (or the sequence of stages in the CI/CD pipeline) and implement their own.

So why is CI/CD such an excellent tool for quality control? Let’s find out!

Running Tests Automatically

Automated tests haven’t been run locally in what feels like ages. These days, CI/CD pipelines run tests automatically as one of their primary functions.

Pipeline configuration can be assigned to DevOps. But then we will be a long way from making use of the CI/CD tool’s second function: quality control, or more precisely, “quality gates”.

Quality Control Using Quality Gates

But what are quality gates? Let’s say the product code is like a castle. Every day, developers write new code – which could weaken the foundations of our castle or even poke holes in it if we are really unlucky. The purpose of a QA engineer is to test each feature and reduce the likelihood of bugs finding their way into product code. Lack of automation in the QA process could cause QA engineers to lose sleep, since there is nobody to watch over all the various metrics – especially at dangerous times, like Friday evenings when everyone wants to leave work and is hurrying to finish everything. An ill-fated merge at that moment can cause plenty of unwanted problems down the line.

This problem can be solved by building-in quality checks.

Each check deals with a different important metric. If the code doesn’t pass a check, the gates close, and the feature is not allowed to enter. A feature will only be merged into the product when it has passed all the checks and potential bugs have been fixed.

What Quality Checks can be Included in the CI/CD Pipeline?

We need to put together a list of checks to ensure that the process is as automated as possible. They can be sequenced in a “fail first” order. A feature must pass all the checks to get through the pipeline successfully. The initial checks ensure the app is capable of working: build, code style check, and static analysis.

“Build” speaks for itself: if the app fails to build, the feature does not progress. It is important to incorporate a code style check into your CI/CD pipeline to ensure the code meets unified requirements, as doing so allows you to avoid wasting time on this kind of bug during code reviews.

Static analysis is an essential tool for judging code quality. It can point out a vast number of critical errors that lead to bugs and decrease the number of routine and repetitive tasks for the QA team. Afterwards, developers should fix the detected issues and hand the code over for the testing stage.

We then continue with stage-two checks: unit tests with coverage analysis and coverage quality control, as well as integration and systems tests. Next, we review detailed reports of the results to make sure nothing was missed. At this stage, we may also perform a range of non-functional tests to check performance, convenience, security, and screenshot tests.

When developing a pipeline, we need to pay attention to 2 competing requirements:

  1. The pipeline must guarantee the best possible feature quality in light of your needs.
  2. Time spent running the pipeline should not slow down your workflow. It should generally take no more than 20 minutes.

Examples of Tools to Incorporate in Quality Checks

Code Style Highlighting

A code style is a set of rules that should be followed in every line of code in a project, from alignment rules to rules like “never use global variables”.

You might be wondering what style has to do with testers. The answer is a lot. A style check provides several benefits for QA experts, not to mention the rest of the team:

  1. A unified style helps developers work with the code and gives them more time to implement new features and fix bugs.
  2. A unified style allows you to dispense with manual code checks and use a CI/CD tool to run the checks instead.

Large companies usually have their own style guides that can be used as examples. For instance, Airbnb has a JavaScript style guide, and Google maintains several guides. You can even write your own, should you wish.

The choice of tools for code checking depends on the language. You can find a suitable tool on GitHub or find out which tools other teams use. Linters use bodies of rules and highlight code that fails to abide by them. Some examples include ktlint for Kotlin or checkstyle for Java.

Static Code Analysis

Static code analysis is a method of debugging by examining source code without executing a program. There are many different static code analyzers on the market.

We’ll now look at a platform we’re developing ourselves – Qodana. The significant advantage of this code analyzer is that it includes a number of inspections that are available in JetBrains development environments when writing code.
Many of you probably use an IDE-driven approach, where the IDE helps you write code and points out bugs such as suboptimal code usage, NullPointerExceptions, and duplicates.

But unfortunately, you can never be sure all the critical problems found by the IDE were fixed before the commit. However, you can ensure that the issues will be addressed by incorporating Qodana into your CI/CD pipeline.

Qodana, the latest addition to the family of products from JetBrains, is a cutting-edge static analysis platform designed to help developers and QA engineers improve their code quality, making it more efficient, maintainable, and bug-free. Its static analysis engine is the only solution on the market that brings native JetBrains IDE code inspections to any CI/CD pipeline. The platform provides an overview of project quality and lets you set quality targets, track progress, and automate routine tasks like code reviews.

Interactive inspection report in the Qodana code quality platform.

If you can’t fix everything at once, you can select critical problems, add them to the baseline, and gradually work your way through the technical debt. This allows you to avoid slowing down the development process while keeping the problems that have been found under control.

The updated baseline in the Qodana code quality platform.

Test Coverage

Test coverage is a metric that helps you understand how well your code has been covered by your tests (generally unit tests).

Here, you need to define the minimum coverage percentage you want to support. The code won’t be able to go live until it has been covered sufficiently by the tests. The minimum percentage is established empirically, but you should remember that even 100% coverage may not completely save your code from bugs. According to this article from Atlassian, 80% is a good figure to aim for.

Different coverage analyzers are available for other languages, such as Jacoco for Java, Istanbul for JavaScript, or Coverage.py for Python. You can build all these analyzers into your CI/CD pipeline and track the metrics with ease.

Shaping the Release Process

In addition to automatically running tests and ensuring particular code quality requirements are satisfied, the CI/CD tool lets testers organize the release process.

The release process can be complex and depend on many different manual actions. It is often a completely manual process: the artifact is created by a developer, then passed to the testers for checks, and finally comes to the person who knows how to roll it out for the go-live. Once again, there are a lot of potential choke points here. For instance, one of those people could fall ill or go on vacation.

An effective release process will look different for each team, but it will generally include the following steps:

  1. Each change in the Git branch triggers a build of the app.
  2. The build undergoes quality checks and does not become part of the main branch until it passes all the checks successfully.
  3. A release candidate is taken from the release branch or the main branch: this fixes the version and guarantees that nothing will go live unless it has been tested and has not been changed afterwards. This helps with tracking releases and all the changes they include. In addition, storing artifacts of the stable version makes it possible to revert to them quickly in the event of an unsuccessful release.
  4. The release candidate is tested and undergoes final checks.
  5. The release candidate goes live. This may be either a manual or automated pipeline launch, if the release candidate passed all the checks at the preceding stage. The choice between an automatic release process and a manual one will depend on how frequent and important the releases are, as well as the preferences among team members and the convenience of the rollout.

Any CI/CD system allows you to set up this type of process, which should be convenient for the whole team, including the testing team.

Given the factors outlined above, we believe following these basic rules will help ensure an easy and efficient release process:

  • Artifacts must be ready for download and testing, ideally stored in one place.
  • As many checks and tests as possible must be automated.
  • All complex operations with builds should be as automated as possible.
  • All builds that will go live should be recorded and remain available for a certain period after release. This will help if you need to investigate errors in the production version, reproduce bugs, or just track the history.

We would also like to remind you that if quality metrics are not controlled automatically and are not actionable, they are useless, as there’s no way to guarantee that these metrics will be adhered to.

Implement pipelines, automate processes, and use static code analysis!

Your Qodana team

Author

Alexandra Psheborovskaya, QA Lead and Product Manager at JetBrains

JetBrains is a global software company that creates professional software development tools and advanced collaboration solutions trusted by more than 12.8 million users from 220 counties and territories. Since 2000, JetBrains has built a catalog of 34 products, including PyCharm, IntelliJ IDEA, ReSharper, PhpStorm, WebStorm, Rider, YouTrack, Kotlin, and Space, a new integrated team environment.

Qodana is the code quality platform from JetBrains. It provides a project overview and lets developers and QA engineers set up quality gates, enforce project-wide and company-wide coding guidelines, better plan refactoring projects, and perform holistic license audits. Qodana’s static analysis engine enriches CI/CD pipelines with all of the smart features of JetBrains IDEs, supports 60+ languages and technologies, and allows analysis of unlimited lines of code.

JetBrains is an EXPO Platinum partner at EuroSTAR 2023, join us in Antwerp

Filed Under: DevOps, Test Automation, Uncategorized Tagged With: 2023, EuroSTAR Conference, Test Automation

Trends Software Testers Should Watch In 2023

April 12, 2023 by Lauren Payne

Thanks to Mabl for providing us with this blog post.

The digital experience has become a core aspect of the enterprise, forever linked to customer experience and business outcomes. Customers are online more than ever before, and enterprises have to offer reliable, high-quality capabilities that facilitate engaging and inclusive digital experiences. To exceed this elevated bar in 2023, software test teams should be aware of the trends in DevOps, cloud migration, and digital transformation that will continue to be enabled by strong quality practices.

DevOps Adoption Continues to Grow

DevOps redefines how teams, processes, and technology are aligned within an enterprise to make software development faster and more collaborative through automation. According to the 2022 Testing in DevOps Report, enterprises of all sizes are pursuing full DevOps adoption with automated workflow pipelines. Embracing DevOps practices successfully requires a continuous view of quality, which requires elevated, automated testing.

DevOps pays off. Google’s 2022 Accelerate State of DevOps Report reveals that high-performing software teams are more likely to embrace DevOps practices like version control (33%), continuous integration (39%), and continuous delivery (46%).

Teams leveraging these practices are quickly maturing in a way that improves overall product velocity, and overall application quality as well. Mature organizations where quality is at the center of their DevOps transformation will achieve these DORA metrics faster while delighting customers with the frequent delivery of new battle-tested features.

Adoption of Cloud Technologies is Accelerating

With the massive shift to cloud-based architecture, software testers must be mindful of controlling quality throughout the migration. Often, the migration to the cloud goes together with DevOps adoption. As development cycles increase, QA teams should investigate cloud testing solutions to quickly scale up test execution – without the limitations of managing on-premise infrastructure.

More Reliance on APIs

Enterprises are becoming more connected and integrated – all possible via APIs. As with cloud computing, this makes the infrastructure more dynamic. It also presents the problem of having to contend with the quality of third-party APIs that may not be properly updated and secure and determining how it will affect the status and quality of the enterprise’s applications. Prioritizing quality with continuous API testing for integrated services allows test teams to detect issues before they affect customers.

Digital Transformation Initiatives are Increasing

Digital transformation remains a top priority for enterprises. In fact, according to one survey, 94% of CFOs recognize the need to maintain or accelerate the already-intense pace of transformation incited by the pandemic. To continue the digital transformation momentum, quality must become a foundation of software development and delivery.

To help enable digital transformation, QA teams can embed quality throughout the software development lifecycle while finetuning processes and QA support. Outside of ensuring the functional correctness of your application, QA teams can start embedding end-to-end, performance, and accessibility tests in the pipeline as well. This way, you confidently deliver better experiences for customers throughout your organization’s transformation.

Ensure Great CX with Low-code Test Automation

Quality is the differentiating factor among competing organizations. Software QA teams have the opportunity to transform technologies, develop new processes, and contribute to excellent customer experiences if they are successful in building a culture of quality in their organization during transformational times.

Once you’ve built a culture of quality and set up the right team and processes, QA leaders need the right tools to execute. These transformations are enabled by intelligent, low-code test automation like mabl. Mabl allows anyone, regardless of coding experience, to create, execute, and maintain tests, integrate those tests into your development pipeline, and share insights into the holistic quality of your application back to the entire software team.

Mabl helps teams scale their testing with 90% less effort, giving QA teams time back to grow test coverage and focus on testing activities that positively impact the customer experience.

Author

Leah Pemberton

Director, Marketing at Mabl

Leah is the Director of Marketing at  Mabl, writing frequently about quality engineering and building a culture of quality.

 Mabl is an EXPO Gold partner at EuroSTAR 2023, join us in Antwerp

Filed Under: DevOps, Software Testing, Test Automation Tagged With: 2023, DevOps, EuroSTAR Conference

How 2023 IT Trends Impact Software Testing

March 17, 2023 by Lauren Payne

Thanks to OpenText / MicroFocus for providing us with this blog post.

[Disclaimer: The future described in this article is the author’s personal view and is by no means any indication of OpenText products’ direction.]

Happy 2023 to the community! The beginning of a year is always the time to think big and look into the future. Last year, I wrote about three 2022 trends that’ll change test management, all of which continue to gain momentum. So far, you’ve heard 2023 predictions from analyst firms, consulting firms, and others. Among those trending IT keywords, I picked a few important ones to share with you. These words may not be new. But they have become even more important in today’s challenging business environment. Putting them together, we can see new ways of developing, testing, and measuring software.

Industry Cloud Booms

Gartner defines industry cloud platforms as modular, composable platforms supported by a catalogue of industry-specific packaged business capabilities. Packaged business suites are not new. The ERP suites from SAP, Oracle, and other vendors are well-known examples. Industry cloud platforms offer a rich set of industry-specific capabilities from many vendors and a place to compose and deliver business applications faster. Enterprise integration PaaS gives you the tools to easily integrate those industry components to form your application with low code or no code.

Cloud is the marketplace of other good stuff too, including AI and data analytics capabilities. Container technologies let you deploy your applications easier and swap out a defective module quickly.

Leveraging the industry cloud shortens the cycle to deliver value and lets you focus on innovation instead of the underlying mundane tasks. Some industry cloud platforms are already full-fledged, including the SAP Industry Cloud and Microsoft Cloud for industries. Companies with deep industry knowledge, like Deloitte, also join the play with their industry solutions.

When your application is built in this way, a focus of testing is verifying the business processes—the “wiring” you composed—and the testers are likely business users. Tools to efficiently verify business processes will find many new places to shine. Testers of all skill levels will appreciate the ability to manage complex interdependencies and save efforts with a component- or model-based approach. Model-based testing directly generates test cases from your business process model and automates them. Expect it to become more common in 2023.

With cloud platforms for building applications fast, you won’t want to lose momentum in the testing part. That’s why cloud-based testing and device farm are your choice.


Observability-Driven Development

The concept of observability has been around for decades. But it recently got popular in the cloud world because it allows people to observe the health of cloud-based systems by analyzing logs and correlating events. Now observability has come to software too, with observability-driven development (ODD). By adding a small amount of code for instrumentation, the development team can observe what’s happening when the application runs in the production environment in the cloud. When there is any issue with the app, observability helps identify the cause quickly and accurately—pinpointing the specific code in question.

ODD is yet another way to strengthen the feedback loop from Ops to Dev. ODD is also said to be a way to test in production, i.e., shift-right testing. The biggest advantage of testing in production is that you have real data and the same environment as customers use. Such observability is also valuable when you test in staging environments. We’ll see more test management or DevOps governance tools incorporate observability data into their data repository for analytics and insights in 2023.

FinOps Meets Value Stream Management

The word FinOps combines “Finance” and “DevOps,” stressing collaboration between business and engineering teams.

FinOps started when cloud service providers and other vendors wanted to provide a way of managing finances for a company’s cloud-based systems. It’s meant to help organizations find the best spending model for achieving their business goals. OpenText HCMX supports FinOps.

I expect to see FinOps and value stream management (VSM) converge sometime in the future. Both are extensions of DevOps. FinOps focuses on managing your costs against business value, while VSM focuses on eliminating bottlenecks and wastes during the process of delivering business value. With both sets of data, you can optimize the cost performance of your application. For example, when you’re unsure whether it’s more efficient to outsource a certain part—such as testing—FinOps and VSM data will likely give you a clear answer.

What’s Next?

You may have noticed that all of the above are related to the cloud. Why? Challenges in the global economy are driving more cloud adoption. The cloud has become the frontier of innovation. Every enterprise that wants to win and lead must embrace the cloud. OpenText software quality solutions went the SaaS model to serve customers who are moving to the cloud in two ways—reducing tooling cost and increasing speed.

Stay tuned on what’s happening with OpenText cloud solutions, especially the ValueEdge platform, in which you will find the most innovative software quality capabilities from OpenText at the earliest.


What is your organization’s cloud initiative? We are here to help. Contact us to discuss details.

Author

Ying Lei

Ying Lei is a senior product marketing manager at OpenText who specializes in test management, application lifecycle management, DevOps and value stream management.

OpenText is an EXPO Gold partner at EuroSTAR 2023, join us in Antwerp

Filed Under: Agile, DevOps, Software Testing Tagged With: 2023 trends, agile, ALM, Cloud Testing, DevOps, SaaS, software testing, Test Management, Value Stream Management

3 Obstacles to Continuous Testing & How to Remove Them

July 6, 2021 by Fiona Nic Dhonnacha

Continuous testing is a process that empowers teams to build quality into software development and accelerate the delivery of high-quality customer experiences. With continuous testing, teams get instant feedback on code health using automated testing.

Continuous testing allows organizations to assess business risk. Recent industry surveys show the top metrics used to track project progress and success:

  • High test coverage
  • Increased defect remediation
  • Decreased defects in production

Building Quality Into the Development Process

Quality is an important topic for senior levels of management. Here are some insightful findings of business leaders polled:

  • 48% said quality improvement was in their top three initiatives.
  • 68% were seeking ways to improve delivery speed and quality.

The new goal? Build quality into the development process to accelerate the delivery of high-quality customer experiences.

How is this combination of speed and quality achieved? Continuous testing. But it does come with its challenges.

Overcoming Continuous Testing Obstacles

With quality at speed as the goal, there are typically three primary obstacles to overcome when implementing continuous testing.

  • Lack of expertise. The team may lack in availability to take on new approaches and skills needed to learn and adopt new tools and techniques.
  • Unstable execution. Test automation as it currently stands in the organization may be unstable and unreliable. As code grows, so does execution time.
  • Unavailable environment. The test environment is often unavailable, uncontrollable, and constrained by system dependencies.

Teams must remove these obstacles for continuous testing to become ingrained in the development culture of a software organization.

Obstacle 1: Lack of Expertise on the Team

Initially, the lack of expertise isn’t only the team’s lack of knowledge and skills. It’s also limitations of tools being used.

Consider user interface (UI) test automation. It’s a common practice and reliable, but reusable automation is difficult. Selenium is the de facto standard. While open source and free, it has its own adoption curve, and it takes experience and time to master.

Selenium tests can be unreliable and what’s recorded one day, can’t be played back the next. Test maintenance becomes a growing issue as more UI tests are automated. Selenium requires further tools support to become easier to use and maintain.

Service level or API testing is a relatively new, but valuable practice. However, it sits in a no man’s land between developers and testers. Developers understand the APIs the best but aren’t motivated nor compelled to test them and testers lack the knowledge needed to do API testing.

The number one challenge when organizations are trying to adopt API testing, is understanding how the APIs are actually used. This isn’t to say APIs aren’t documented or designed well. Rather, there isn’t much information about how the services are being used together in a use case, workflow, or scenario.
lack
In addition, it’s important that API test automation goes beyond record (during operation) and playback (for testing). Modeling the behavior and the interactions between APIs are needed, as is using these interactions to steer test creation and management processes.

Performance testing is often seen as something that’s done by another team in the organization, perhaps performed as a check box item. But when performance issues arise, product development may have moved forward, and a disruptive rollback may be required.

Ideally, performance testing needs to be done earlier in the software development process and leverage the work already being done with automated functional testing. At the same time, the team is adopting API testing, they can leverage that work to enable performance testing to shift left, making it a joint responsibility of developers and testers.

How to Remove: Simplify Test Automation

The lack of expertise and training in the development and test team should not reflect on the team itself, but rather on the complexity when adopting test automation and associated tools. There are solutions available that strive to simplify test automation. These solutions make adoption less disruptive and integrate better into existing processes.

Create reusable, maintainable, and understandable test scripts. Parasoft Selenic solves the main issues with Selenium adoption: test creation and maintenance. By recording UI interactions via the Chrome browser, Selenium test cases are automatically created based on these interactions. In addition, locators are recorded using the page object model to be more resilient to changes in the UI. Selenic uses AI-driven self-healing of tests so that when UI changes break existing tests, the tool makes intelligent assumptions to prevent the test cases from failing.

Model real-world API test scenarios by recording manual and automated UI interactions. API test adoption is hindered by the ability to create tests. Parasoft SOAtest uses existing UI tests (including Selenic-created tests) to record API interaction happening during execution of the application. AI within SOAtest organizes these recorded interactions into recognizable scenarios which then form the basis of an API test repository. These API scenarios can be played back, edited, cloned, and reused to form a comprehensive API test suite. The automation and AI-powered decision making SOAtest does to make API testing easier to adopt, use, and maintain. Plus, it helps bridge the API testing knowledge gap in the development team.

Reuse existing test artifacts to efficiently scale load, performance, and security testing as part of DevOps pipeline. As the development team becomes more proficient at test automation for the UI and API levels, the test repository becomes an important reusable resource. Tests can be reused for load and performance testing and to increase use case and code coverage.

The process flow shows automation for Selenium UI tests, recording and creation of API tests, and reuse of assets for future functional, performance, and security tests.

Obstacle 2: Execution of Tests Is Unstable, Unreliable, & Takes Too Long to Run

Understandably, software organizations expect automated tests to be efficient and not impede development progress. However, as test suites grow, so do the problems with maintaining and executing them. Tests, like code, are impacted by change. New functionality added during a sprint can significantly impact the user interface or the workflows of the application. These changes break existing tests, making them unstable. It’s important to address those as quickly as possible.

When tests fail, you need to understand the context of the failure. Not every test failure is equal. Some use cases are more important than others or, perhaps, some tests are unstable by nature. What is lacking is an understanding of the impact of test failures or test instabilities on the business priorities of the application. Investigating these constant test failures becomes a distraction from the overall test automation strategy. Correlation between business requirements and tests is critical to ensure that the value of automation is realized.

Another obstacle is the actual execution time for test suites. As the test portfolio grows, so does the execution time beyond a reasonable waiting period for feedback. Quick feedback to change is essential for a successful CI/CD pipeline so test efficiency and focus are required.

How to Remove: AI-powered Test Execution

The solution to the test execution obstacle is to test smarter with AI. This means leveraging test automation AI to make tests more resilient to change and to target execution on key tests only.

Smart UI testing with Selenium and Selenic. Using AI, Parasoft Selenic self-heals tests when UI changes are detected. These are automatically used but recommendations are sent to the developer to help fix the tests. These fixes can be automatically applied to the Selenium tests, removing manual debug and code changes.

Using AI to self-heal tests reduces test instability but also provides recommendations and quick fixes.

Plan work item tests based on impacted requirements. To prioritize test activities, correlation from tests to business requirements is required. Keeping track of user stories and requirements provides real-time visibility into the quality of the value stream. User stories and requirements should be reviewed for priority. The traceability capabilities in Parasoft SOAtest are used to plan execution for tests that validate items being worked on in-sprint. However, more is required since it’s unclear how recent changes have impacted code.

Prioritizing tests based on impacted user stories is the first step to optimizing test execution.

Use test impact analysis to validate only what has changed. To fully optimize test execution, it’s necessary to understand the code that each test covers, then determine the code that has changed. Parasoft tools provide this capability through a central repository for test results and analysis. Test impact analysis allows testers to focus only on the tests that validate the changes.

Test impact analysis determines which tests correlate to the code that changed to focus testing only on what should be tested.

Obstacle 3: Test Environment Is Unavailable, Uncontrollable, & Constrained by System Dependencies

The testing environment is the linchpin of the obstacles stopping organizations from turning automated into continuous testing. There are three types of challenges that organizations face when trying to make tests run anytime, anywhere, and dealing with the external dependencies of the application. This is especially true for a microservices architecture. The number of dependencies explodes due to the very nature of the design.

Test Environment Challenges

Waiting for access to a shared system, like a mainframe or an external dependency provided by a third party. Availability might be time limited and costly. It’s also a challenge if the external dependency is heavily loaded with multiple people working on it at the same time, resulting in test instability from data collisions.

Bottlenecks caused by delayed access. This is due to the nature of parallel development and typical of modern processes. For example, multiple teams are collaborating to deliver new features to the value stream such as interdependent microservices. Testing can’t proceed on one microservice because another isn’t available yet.

Uncontrollable test data. Although microservices are relatively easy to deploy and test in isolation, their dependencies on data or performance characteristics limit the ability of them to be tested thoroughly. For example, reliance on data in a shared production database can limit the ability to test services.

How to Remove: Control the Test Environment

Start simulating these dependencies to give the team full control using a service virtualization solution. Parasoft Virtualize simulates services that are out of your control or unavailable. It provides workflows that:

  • Enable users to access complete and realistic test environments.
  • Stabilize their test environment.
  • Get access to otherwise inaccessible dependencies.
  • Manage the complex business logic, test data, and performance characteristics required for virtual services to behave just like real services in the real environment they represent.

Service virtualization removes the bottlenecks. Here’s how.

Record and simulate: Capture, model, and provision simulations of live systems.

Using the recording capability of Parasoft SOAtest, it’s possible to capture the behavior of the application in its environment. Parasoft Virtualize models the behavior of external dependencies making it possible to remove and simulate the behavior of dependencies, dynamically on the fly, switching out real versus virtual. Making these services and dependencies available and stable, virtually, accelerates the testing process and enables continuous testing.

Dependencies in the test environment are obstacles to testing. Virtualizing these dependencies removes their impact on testing and enables continuous testing.

Deliver a prototype first: Model behavior based on contract descriptions or payload examples.

Service virtualization enables prototype development based on the contract descriptions derived from the API interaction recordings and analysis in SOAtest.

Dependent services can be simulated with good fidelity to create prototype versions that satisfy their roles in the system when testing another adjacent service. This removes the schedule limitation inherent in parallel development—even when services aren’t complete, they can be virtualized for testing other services.

Environment Manager provides graphical modeling and control of the test environment. Services are controlled and virtualization parameters are configured using this diagram.

Synthesize private test data.

Another obstacle in testing enterprise applications is test data. Many organizations use real data, but this is fraught with privacy concerns. Purely synthetic data is often not realistic enough to test with so a compromise is needed. Synthesizing real data by removing personally identifiable information (PII) provides realistic and safe-to-use data. Test data management is required in conjunction with service virtualization to provide a realistic, highly available test environment that won’t result in any privacy compromises.

The Benefits of Continuous Testing

Removing the key obstacles to continuous testing enables testing to occur on a regular, predictable schedule. It transforms application testing empowering teams to:

  • Test earlier. Shift left to in-sprint testing where it’s quicker, cheaper, and easier to remediate the problem.
  • Test faster. Automate and execute continuously to get immediate feedback when defects are introduced.
  • Test less. Focus and spend less time creating, maintaining, and executing test scenarios. Reduce the cost of test infrastructure.

Want to learn some more about continuous testing? Join us at EuroSTAR in September, for 3 days of talks, tutorials, and lots more from leading test experts. Check out the full programme.

Parasoft is exhibiting at EuroSTAR Conference this year – take a look at their work here.

Author

Mark Lambert, VP of Strategic Initiatives at Parasoft

Mark focuses on identifying and developing testing solutions and strategic partnerships for targeted industry verticals to enable clients to accelerate the successful delivery of high quality, secure, and compliant software. Since joining Parasoft in 2004, Lambert has held several positions, including VP of Professional Services and VP of Products. Lambert is a public speaker and author. He’s been invited to speak at industry events such as JavaOne, Embedded World, AgileDevDays, and StarEast/StarWest. He has published thought-leadership articles in SDTimes, DZone, QAFinancial, and Software Test & Performance. Lambert earned both his Bachelor’s and Master’s degrees in Computer Science at Manchester University, UK.

Filed Under: DevOps, EuroSTAR Conference Tagged With: DevOps

Understanding the DevTestOps Journey

March 24, 2021 by Suzanne Meade

Understanding the DevTestOps Journey

For the last two years, our team has hosted the Testing in DevOps Landscape Survey to take the pulse on how quality testing is evolving as adoption of DevOps grows. We turn to our dedicated community and the software development industry at large to understand how quality engineering is shaping the adoption of DevOps practices, how they champion the customer experience in a rapid development framework, and what obstacles remain in the way.

Last year, an incredible 1,030 people with experience in QA management, testing, and development shared their insights. Their expertise resulted in an in-depth look into how test automation is elevating the voice of quality in a DevOps world. We hope you will help us again this year to collect the data we need to understand the current state and what’s changed over the past year. We are especially looking forward to learning about how teams are adapting to the global pandemic and the impact it’s had on their digital transformation objectives.

In case you missed it, the 2020 DevTestOps Landscape Survey found that most participants were making good progress in their DevOps adoption journey, with 31% saying they were agile and 35% saying they had made strides towards DevOps. Almost one-fifth described themselves as “so DevOps it hurts.” What’s more, the 2020 survey revealed the close connection between DevOps adoption and customer satisfaction:

DevOps chart

This year, we are increasing the focus on solving the real-world challenges of DevOps implementation, especially in a virtual world. With the customer experience front-and-center post-2020, we want to understand how DevOps practitioners are rethinking long-term quality and testing strategies, where testing takes place in the DevOps pipeline, and what the overall QA impact is on the customer. We also want to dig deeper into what challenges remain and what’s top of mind as we head into 2021.

Your experience is valuable to us! Please take a few minutes to fill out the survey today. With your help, we look forward to learning and sharing these important benchmarks and insights early this summer.

mabl are Gold Sponsors at the 2021 EuroSTAR Software Testing conference, which takes place 28-30 September this year. The programme will be launched soon, so stay tuned!

Izzy Azeri, mabl co-founder

Author: Izzy Azeri

Izzy Azeri is the co-founder of mabl, the leading test automation tool built for DevOps. He co-founded mabl in 2017 with Dan Belcher after realizing that existing software testing tools were unable to support the needs of high-velocity development teams. Mabl is now the leading intelligent test automation tool for quality-centric brands like Ritual, Charles Schwab, and Arch Insurance. The company has raised over $36 million from prominent VC firms such as GV (formerly Google Ventures), Amplify Partners, CRV, and Presidio Ventures.

Filed Under: DevOps Tagged With: DevOps

  • Page 1
  • Page 2
  • Next Page »
  • Code of Conduct
  • Privacy Policy
  • T&C
  • Media Partners
  • Contact Us

part of the