Software testing is the disciplined practice of evaluating a software system or component to determine whether it satisfies its specified requirements, to detect defects, and to build confidence in its behavior. While the most visible activity is the execution of a program with selected inputs to observe its outputs, the field encompasses far more: the design of test cases, the creation of test infrastructure, the management of test activities, and the use of testing results to estimate and improve product quality. Its central question is deceptively simple—how can we learn enough about a system's behavior to trust it, given that exhaustive testing of all possible inputs and states is almost always impossible?
The stakes are practical and sometimes severe. Software defects can cause financial loss, safety hazards, privacy breaches, and reputational damage. Testing is the primary, though not the only, means that software teams use to detect these defects before users do. Beyond defect detection, testing also serves as a form of specification checking: tests encode expected behavior and can therefore reveal mismatches between what the developers built and what was actually needed. In modern practice, testing is also a design activity, as developers increasingly write tests before or alongside the code they validate.
The foundational challenge of software testing is that any nontrivial program has an effectively infinite input space, including not just user inputs but also the system's internal state, timing, environmental conditions, and the order in which operations are performed. Testing a small fraction of this space can demonstrate the presence of defects but can never prove their absence. This insight, famously articulated in the 1970s and still central to the field, leads to a central planning question: which tests are worth running among the infinite set of possible tests?
This selection problem gives the field its intellectual structure. Since testing everything is impossible, the discipline develops criteria and strategies for choosing a finite, tractable set of tests that maximizes the chance of finding defects and providing confidence. The primary strategies include partitioning the input space into equivalence classes—groups of inputs that are expected to be processed the same way—and testing representatives from each class; covering boundary values where software often mishandles transitions; and structuring tests to exercise specific code statements, branches, or conditions (coverage-based testing). Each criterion is a heuristic, not a guarantee; passing a suite of tests according to any of these criteria means that certain kinds of faults are less likely, but it does not mean the software is correct.
Modern software testing emerged gradually from the practice of debugging. In early computing (1940s–1950s), testing was largely an ad hoc process of trying programs on sample inputs to see if they ran and produced plausible outputs. There was no distinct discipline; debugging and testing were synonymous. Through the 1960s, as software became larger and more expensive, organizations began to formalize testing as a separate phase in the development lifecycle, often performed by dedicated testers after programming was complete. This era saw the rise of the "execution-oriented" view: testing was the process of running a program with the intent of finding errors.
A significant development came in the 1970s and 1980s, when testing began to be treated as a serious technical subject rather than a clerical activity. Researchers and practitioners articulated the idea that a successful test is one that reveals a defect, not one that passes. This reframing encouraged testers to develop adversarial mindsets and systematic methods for choosing inputs. During this same period, the distinction between two complementary activities became standard: verification, asking whether the product was built correctly (conformance to its specification), and validation, asking whether the right product was built (meeting the user's actual needs). Testing serves both, but the distinction clarifies that a test suite can fully satisfy a poorly written specification and still deliver a useless system.
The 1990s saw a radical expansion of testing's role through the rise of agile software development. Previously, testing was typically a phase in a sequential "waterfall" process, occurring late and often as a gate before release. Agile methods emphasized short iterations, continuous integration, and the idea that testing should be interwoven with development rather than deferred. This shift was symbiotic with the rise of automated testing tools and frameworks. Unit testing—testing the smallest testable parts of a program in isolation—became a standard developer practice. The test-first approach known as test-driven development, in which a developer writes a failing test, then writes the minimum code to pass it, and then refactors, turned testing from a verification activity into a design activity. The test suite became a living specification of the system's behavior and a safety net that permitted confident refactoring.
Across all historical eras, testing practice has been organized around two broad approaches that often coexist in the same project.
Manual testing involves a human tester designing and executing test cases by hand, interacting with the software directly and using judgment to explore behavior. Its strengths lie in its flexibility: a skilled tester can notice unexpected behaviors, respond to subtle usability issues, and apply domain knowledge that an automated script lacks. Exploratory testing, a recognized modern methodology, is a manual approach that emphasizes simultaneous learning, test design, and execution; the tester "explores" the software dynamically, letting the results of each action inform the next. Manual testing remains essential for assessing user experience, visual appearance, accessibility, and other qualities that are difficult to encode algorithmically. Its limits are equally clear: it is time-consuming, difficult to repeat exactly, and its effectiveness depends heavily on individual skill and energy.
Automated testing encodes test cases as executable scripts that drive the software, compare its actual behavior against expected results, and report pass or fail. Automation enables tests to be repeated easily and frequently—an essential precondition for the practice of regression testing, in which previously passing tests are rerun to ensure that new code changes have not broken existing functionality. Automation is also scaled across large systems: a test suite with thousands of cases can run overnight or in a continuous integration pipeline after every code commit. The cost of automation is development effort: the scripts themselves must be written, maintained, and debugged, and an automated test that is poorly designed can provide a false sense of security or fail for reasons unrelated to the software under test. The practical distinction is not that automation replaces manual testing, but that the two focus on different question types: automation verifies known expected outcomes repeatedly, while manual testing seeks the unexpected.
Testing practice is commonly organized into levels distinguished by the scope of the unit under examination. These levels are complementary and all are retained in modern practice, having emerged historically as development architectures grew more complex.
Unit testing targets the smallest isolated piece of behavior—a function, method, or class—in isolation from the rest of the system. It is fast, precise, and typically performed by the developer who wrote the code. Unit tests localize failures, making them easy to trace, but they cannot detect integration errors, since they replace collaborators with controlled substitutes (test doubles like mocks, stubs, or fakes).
Integration testing combines two or more components to test their interaction. Its purpose is to detect failures at the seams: mismatched data formats, misordered calls, broken wiring, or incorrect assumptions about a collaborator's behavior. Integration tests are slower and less localized than unit tests but catch a distinct and common class of defects. There is no universal recipe for integration testing; strategies include testing bottom-up (from leaf components upward) or top-down (from the user interface downward), each with its own tradeoffs in how soon a complete skeleton is runnable and how much scaffolding is needed.
System testing evaluates the complete, integrated application against its specified requirements. It treats the system as a black box, typically through its user interface or external API, and checks end-to-end behavior, performance, security, and reliability. Because the entire system is running, these tests can detect defects that only appear with real configurations, real data volumes, or real concurrency. They are the slowest and most expensive to run, and a failure may require considerable investigation to localize.
Acceptance testing is performed from the perspective of the customer or end user to determine whether the system meets their acceptance criteria and is ready for deployment. In many regulated or contractual settings, acceptance testing is a formal gate: the customer runs their own scenarios, often using realistic data, and formally accepts or rejects the system. In agile practice, acceptance tests are often written collaboratively by developers and stakeholders before development begins, expressing the user-visible behavior that must be implemented.
The most intellectually developed part of the field concerns how to select test cases. Different approaches have evolved to answer the selection question in different situations; they are not mutually exclusive and are frequently combined.
Specification-based testing (also called black-box testing) derives test cases from an external description of behavior—a requirements document, a specification, a user story, or a model of intended behavior—without reference to the internal implementation. Its fundamental techniques are equivalence partitioning and boundary-value analysis. The professional test case design method known as classification trees generalizes these ideas, allowing the tester to decompose the input domain into dimensions and combinations of classes. Specification-based testing has the virtues of being unbiased by implementation quirks and of being applicable even before code exists, but it is only as good as the specification: if the specification is wrong, ambiguous, or incomplete, the tests will inherit those flaws.
Structure-based testing (also called white-box or glass-box testing) uses the internal structure of the program—its statements, branches, and conditions—to guide test selection. The goal is to satisfy a coverage criterion: for example, ensuring that a certain percentage of statements or branches is executed by the test suite. Structural coverage is a useful measure of thoroughness, but it is not a guarantee of correctness; achieving 100% branch coverage means that every decision in the code took both directions, not that all combinations of decisions were exercised or that the expected behavior was correctly specified. The main contribution of structure-based testing is negative: it identifies untested parts of the code, which are places where defects may lurk unnoticed.
Fault-based testing directly targets the kinds of defects that may be present. Mutation testing, the most prominent technique in this tradition, preemptively introduces small changes into the code (for example, changing a + to a - or reversing a condition), each change creating a "mutant." The test suite is then run against each mutant; if the suite fails to detect the mutation (that is, the mutant's outputs are indistinguishable from the original), this reveals a weakness in the test suite. Mutation testing provides a measure of test suite quality that goes beyond code coverage, but because it requires running the full suite once per mutant, it is computationally expensive and is used selectively rather than universally.
Model-based testing uses an abstract formal model of the system's expected behavior to generate test cases automatically and, often, to evaluate the system's responses. The model might be a state machine, a set of preconditions and postconditions, or a more specialized formal notation. From the model, tools can algorithmically generate sequences of inputs that meet chosen coverage criteria. Model-based testing offers a systematic way to explore large state spaces and is particularly valuable for embedded systems, communication protocols, and other domains where behavior is stateful and complex. Its adoption is limited by the cost and skill required to construct the model, which must be accurate if the tests it generates are to be meaningful.
The present landscape of software testing is shaped by several converging developments. The most important is the near-universal integration of automated testing into development workflows. In the practice of continuous integration and continuous delivery, a pipeline automatically builds the software, runs unit and integration tests, and rejects code that breaks the suite. This makes tests not merely a quality gate but an integral part of the development infrastructure. The practice of writing tests as executable specifications has become standard under the banner of behavior-driven development, where scenarios are written in a near-natural language and then automated.
Testing for non-functional qualities continues to expand in importance. Performance testing measures responsiveness, throughput, and resource usage under different loads; security testing probes for vulnerabilities that could be exploited; usability testing assesses whether real users can effectively accomplish their goals; and accessibility testing ensures software can be used by people with disabilities. Each of these has developed its own techniques and tooling while remaining embedded in the broader testing discipline.
A persistent distinction in the field is between the goals of prevention and detection. Some approaches, such as test-driven development and the practice of writing requirements as executable tests, aim to prevent defects from being introduced in the first place by forcing clarity about expected behavior. Others, such as exhaustive exploratory campaigns and mutation testing, aim to detect defects that are already present. Modern teams typically use both, but the balance between them remains a matter of judgment, project context, and available resources.
Finally, the field retains a healthy relationship with the formal methods tradition, which attempts to prove a program correct by mathematical reasoning rather than by testing. Formal verification can, in principle, establish correctness for all inputs, which testing cannot. But formal methods are expensive, require high levels of expertise, and are applied primarily where correctness is safety-critical or where the cost of failure is extreme. In practice, testing and formal verification are complementary: formal methods may be used on the most critical components, while testing remains the general-purpose tool for the rest. The enduring limitation of testing—that it cannot demonstrate the absence of defects—is not a failing but the founding condition of the discipline, which organizes itself around managing that uncertainty as effectively as possible.