The post has been translated automatically. Original language: Russian
The thesis
Testing remains the main way to test software, but passing the tests does not mean that the program is correct in all possible situations. Each test verifies a specific scenario, whereas the number of possible program states, inputs, and sequences of actions can be huge or virtually unlimited.
Therefore, there is a fundamental difference between the statements "the tests were successful" and "the program is working correctly." The first indicates the results of the checks performed, the second requires a much stronger justification.
Review
Regular testing is built around examples of program behavior. The developer sets the input data, executes the program, and compares the result with the expected result. Unit, integration, and system tests are based on this principle.
This approach works well when you need to test known scenarios. For example, you can make sure that the function correctly calculates the tax for several values, the server returns the expected response, or a banking transaction changes the balance in accordance with the specified rules.
The problem arises when there are too many possible options. Even a simple function that takes several numeric parameters can have a huge input data space. If you add to this the state of the system, parallel execution, errors from external services, and different sequences of user actions, it becomes impossible to sort through all the options.
Therefore, testing is always associated with sampling. We check some of the possible behavior of the system and assume that the results of these checks characterize the program as a whole quite well.
Decision
The first step towards stricter verification is to change the test object itself. Instead of asking "does the program work correctly on this example?" you can ask the question "which property of the program should always be executed?".
For example, instead of checking several specific transfers between bank accounts, you can formulate a requirement: the amount of money in two accounts should not change after an internal transfer. Instead of dozens of checks of the permissible engine speed, it is possible to determine the condition that the speed should never exceed the set limit.
This creates a transition from individual examples to system properties. Tests do not disappear at the same time, but they begin to be used as one of the ways to verify formulated properties.
This transition is important because the property describes not a single case, but a whole class of acceptable behavior. In the future, such properties can be used for automatic test construction, program status analysis, and formal verification.
Dignities
The main advantage of regular testing lies in its practicality. It is relatively easy to integrate into the development process, is well automated, and allows you to quickly detect errors in the most important scenarios.
Tests are also useful as a means of protecting the program from the recurrence of errors that have already been found. After fixing the defect, the corresponding script can be saved, and the build system will check it every time the program is changed.
In addition, testing works directly with the actual program. It allows you to detect errors not only in algorithms, but also in the interaction of components, environment settings, working with databases and external systems.
Disadvantages
The main limitation is that the tests check only the executed scripts. If a certain state of the program did not occur during the test, the test result does not say anything about the behavior of the system in this state.
Increasing the number of tests reduces the risk, but does not eliminate this problem. Even the complete execution of all lines of the program does not mean that all combinations of states and all valid sequences of actions have been checked.
There is another difficulty. The expected test result is determined by the person, so the error may be not only in the program, but also in the test itself. If the developer misunderstood the requirement, the test can successfully confirm the incorrect behavior.
Thus, testing answers the question of whether an error has been detected in the tested scenarios well, but it answers the question of whether errors exist outside of these scenarios much worse.
Conclusion
Testing remains a necessary part of software development, but it is important to understand the limits of its capabilities. A large number of successfully completed tests increases confidence in the program, but in itself is not proof of its correctness.
The next step is to move from individual examples to the general properties of the program. If you first determine what should remain true for an entire class of input data and states, the verification can be built around these requirements.
This is where the movement begins from traditional testing to more rigorous ways of verifying software.
Тезис
Тестирование остаётся основным способом проверки программного обеспечения, однако успешное прохождение тестов не означает, что программа корректна во всех возможных ситуациях. Каждый тест проверяет конкретный сценарий, тогда как число возможных состояний программы, входных данных и последовательностей действий может быть огромным или практически неограниченным.
Поэтому между утверждениями «тесты прошли успешно» и «программа работает правильно» существует принципиальная разница. Первое говорит о результатах выполненных проверок, второе требует значительно более сильного обоснования.
Обзор
Обычное тестирование строится вокруг примеров поведения программы. Разработчик задаёт входные данные, выполняет программу и сравнивает полученный результат с ожидаемым. На этом принципе основаны модульные, интеграционные и системные тесты.
Такой подход хорошо работает, когда необходимо проверить известные сценарии. Например, можно убедиться, что функция правильно рассчитывает налог для нескольких значений, сервер возвращает ожидаемый ответ или банковская операция изменяет баланс в соответствии с заданными правилами.
Проблема возникает тогда, когда возможных вариантов становится слишком много. Даже простая функция, принимающая несколько числовых параметров, может иметь огромное пространство входных данных. Если к этому добавить состояние системы, параллельное выполнение, ошибки внешних сервисов и разные последовательности действий пользователя, перебрать все варианты становится невозможно.
Поэтому тестирование всегда связано с выборкой. Мы проверяем некоторую часть возможного поведения системы и предполагаем, что результаты этих проверок достаточно хорошо характеризуют программу в целом.
Решение
Первым шагом к более строгой проверке становится изменение самого объекта тестирования. Вместо вопроса «правильно ли программа работает на этом примере?» можно задавать вопрос «какое свойство программы должно выполняться всегда?».
Например, вместо проверки нескольких конкретных переводов между банковскими счетами можно сформулировать требование: сумма денег на двух счетах после внутреннего перевода не должна изменяться. Вместо десятков проверок допустимой скорости двигателя можно определить условие, согласно которому скорость никогда не должна превышать установленный предел.
Так появляется переход от отдельных примеров к свойствам системы. Тесты при этом не исчезают, но начинают использоваться как один из способов проверки сформулированных свойств.
Этот переход важен потому, что свойство описывает не один случай, а целый класс допустимого поведения. В дальнейшем такие свойства могут использоваться для автоматического построения тестов, анализа состояний программы и формальной верификации.
Достоинства
Главное достоинство обычного тестирования заключается в его практичности. Оно относительно просто внедряется в процесс разработки, хорошо автоматизируется и позволяет быстро обнаруживать ошибки в наиболее важных сценариях.
Тесты также удобны как средство защиты программы от повторного появления уже найденных ошибок. После исправления дефекта соответствующий сценарий можно сохранить, и система сборки будет проверять его при каждом изменении программы.
Кроме того, тестирование работает непосредственно с реальной программой. Оно позволяет обнаруживать ошибки не только в алгоритмах, но и во взаимодействии компонентов, настройках среды, работе с базами данных и внешними системами.
Недостатки
Основное ограничение связано с тем, что тесты проверяют только выполненные сценарии. Если определённое состояние программы не возникло во время проверки, результат тестирования ничего не говорит о поведении системы в этом состоянии.
Увеличение числа тестов уменьшает риск, но не устраняет эту проблему. Даже полное выполнение всех строк программы не означает, что были проверены все сочетания состояний и все допустимые последовательности действий.
Есть и другая сложность. Ожидаемый результат теста определяет человек, поэтому ошибка может находиться не только в программе, но и в самой проверке. Если разработчик неверно понял требование, тест может успешно подтверждать неправильное поведение.
Таким образом, тестирование хорошо отвечает на вопрос, обнаружена ли ошибка в проверенных сценариях, но значительно хуже отвечает на вопрос, существуют ли ошибки за пределами этих сценариев.
Вывод
Тестирование остаётся необходимой частью разработки программного обеспечения, однако важно понимать границы его возможностей. Большое количество успешно выполненных тестов повышает уверенность в программе, но само по себе не является доказательством её корректности.
Следующий шаг состоит в переходе от отдельных примеров к общим свойствам программы. Если сначала определить, что должно оставаться истинным для целого класса входных данных и состояний, проверку можно строить уже вокруг этих требований.
Именно с этого начинается движение от традиционного тестирования к более строгим способам проверки программного обеспечения.