Skip to content
Go back

Non-Functional Testing

When we talk about software testing, we often start with a simple question: does the system work correctly?

That is the territory of functional testing. We check whether a function returns the expected result, an API produces the right response, or a user can complete a particular workflow.

But a system can be functionally correct and still fail badly in production.

It may work perfectly with a small amount of data but become painfully slow as the database grows. A service may behave normally for a few minutes but gradually consume more memory over several hours. A system may handle normal traffic well but collapse when the load suddenly increases. An external dependency may become unavailable and bring down a service that was otherwise working correctly.

This is where non-functional testing becomes important.

If functional testing asks, “Does the system do the right thing?”, non-functional testing asks a broader question: “Does the system continue to behave well under the conditions it is expected to face?”

Some of the most important areas include performance, data volume, stability, reliability, stress and scalability, and security testing.

Performance Testing

Performance testing is about measuring how fast a system performs and making sure that performance does not silently degrade over time.

At the simplest level, we can measure a performance-critical function or piece of code. Imagine a data-processing service with a busy loop that repeatedly decodes, transforms, and encodes messages. Instead of only checking whether the output is correct, we can run that loop tens of thousands of times and measure how long it takes.

That measurement can become a baseline.

Later, if someone changes the implementation and the execution time becomes significantly worse, the performance test can detect the regression. The same approach can also be used when optimizing the code. We measure the original implementation, introduce an optimization, and measure it again to see whether the change actually improves performance.

This is useful because performance is easy to underestimate when we rely only on intuition.

A change can look harmless during code review while adding a significant amount of work to a hot path. Without a baseline, the regression may go unnoticed until it becomes a production problem.

Performance testing can also be performed at a higher level. Instead of measuring a single function, we can measure an entire component or service, including the performance of its external dependencies. For example, we might measure how long a data-ingestion service takes to process one million messages. At an even higher level, we can measure the complete path from data ingestion to data export.

One important detail is that performance measurements need a reasonably controlled environment. A threshold that makes sense on one developer’s machine may not make sense on another. The same applies to CI. If the machines running the tests are not consistent, the measurements may vary for reasons unrelated to the code itself.

Data Volume Testing

Performance does not depend only on the code. It can also depend heavily on the amount of data the system has to deal with.

A query that runs quickly against a nearly empty database may behave very differently when the database contains millions of records. As tables grow, indexes become larger, and queries have to process more data, performance can change significantly.

This is what data volume testing is intended to expose.

Instead of testing only with the small dataset typically found in development, we test the system with a more realistic amount of data and observe how its performance changes.

This matters because a system that performs well today may not perform equally well after several years of production data has accumulated.

Stability Testing

Some problems do not appear immediately.

A service may run perfectly for ten minutes or even a few hours, but eventually start consuming more and more memory, accumulating resources, or producing failures that are difficult to reproduce.

This is where stability testing becomes useful.

The idea is simple: keep the system running under load for an extended period of time and observe what happens.

This type of testing is also known as endurance, load, or soak testing. The test may need to run for many hours, or even several days, depending on the system.

The goal is to discover problems that only become visible over time, such as memory leaks, resource exhaustion, or sporadic bugs that happen only under particular conditions.

The load should also resemble real production behavior as much as possible. Production traffic is rarely constant. There are usually peaks and quiet periods. A test that follows a more natural load pattern can therefore reveal problems that a short test with constant load might miss.

Reliability Testing

If stability testing asks whether the system can keep running for a long time, reliability testing asks a different question:

What happens when something fails?

In a distributed system, failures are inevitable. Networks can become unavailable. Servers can crash. Nodes can disappear. Databases and external services can go down.

A reliable system is not necessarily a system that never fails. Instead, it is a system that can remain available or recover automatically when failures occur.

This is where reliability testing often overlaps with chaos engineering.

For example, imagine a service running on Kubernetes with multiple pods. If one node goes down and one of the pods disappears, the service should remain available through the other pods. Kubernetes should eventually detect the missing pod and create a replacement.

Rather than simply trusting that the configuration will work when a real failure happens, we can deliberately introduce the failure and observe what the system actually does.

The same approach can be applied to external dependencies.

Suppose a service depends on Kafka. We can temporarily make Kafka unavailable and observe the system:

  • Do the services detect the failure?
  • Are the appropriate alerts triggered?
  • When Kafka comes back, do the alerts clear automatically?
  • Do the services resume normal operation?

These are questions that are difficult to answer with functional testing alone.

Stress and Scalability Testing

A system may perform well under normal traffic, but what happens when the load becomes much higher than normal?

That is the focus of stress testing.

In stress testing, we deliberately expose the system to a workload beyond its usual operating conditions. The goal is not simply to see whether the system crashes. We also want to understand how it behaves as demand increases and whether it can scale when necessary.

For a horizontally scalable system, this means testing both scale-out and scale-in behavior.

When the load increases, does the system create more instances? When the load returns to normal, does it remove unnecessary instances?

The metric used for scaling does not always have to be CPU or memory. For a Kafka consumer, for example, consumer lag may be a more meaningful signal. If the lag becomes too high, the system can scale out to provide more processing capacity. Once the lag decreases, it can scale back in again.

This highlights an important point: scalability is not simply about having more powerful servers. A scalable system needs to respond appropriately as its workload changes.

Security Testing

Finally, there is security testing.

A system can be functionally correct, fast, stable, and scalable while still being vulnerable to attack.

Security testing therefore focuses on finding weaknesses that could compromise the system or its data.

One part of this is automated vulnerability scanning. For example, container images can be scanned for known vulnerabilities. Another important part is penetration testing, where the system is tested in ways that simulate malicious activity.

These tests may look for vulnerabilities such as SQL injection, cross-site scripting, information disclosure, denial of service, code execution, CSRF, and other security issues.

Security is also an area where automation can be particularly valuable. Some vulnerabilities can be detected early in the development or CI/CD process instead of waiting until the application is already running in production.

And There Is More

Non-functional testing does not stop at performance, reliability, scalability, and security. There are also areas closer to the user experience, such as accessibility, usability, localization, internationalization, and visual testing.

Visual testing is a good example.

A change to HTML or CSS may not break any business logic. Functional tests may still pass. APIs may still return exactly the expected responses. But the UI could have an unintended visual regression.

Snapshot-based testing can help detect these changes by capturing the rendered UI and comparing it with a known reference. The goal is not to test business logic again, but to make sure that changes to the UI do not accidentally alter something that users can see.

Testing Beyond Correctness

The broader lesson of non-functional testing is that “working” is more than producing the correct result.

A production system exists in a much larger environment. It has users, data, traffic, infrastructure, dependencies, failures, and security threats.

Functional testing helps us ask “does the system do the right thing?” Non-functional testing makes us ask additional questions:

  • How fast is it?
  • What happens when the data grows?
  • What happens when it runs for days?
  • What happens when a dependency fails?
  • What happens when traffic suddenly increases?
  • What happens when someone tries to attack it?

These are not edge cases that can simply be ignored.

They are part of the environment in which real software operates.

And that is why testing is not only about proving that a system works.

It is also about understanding how well it continues to work when the conditions around it change.

Previous Post
Next Post