Привет, Хабр.
Меня зовут Михаил, я технический автор, работаю с инструментами тестирования в команде ТестОпс. В какой-то момент мне стало интересно — а как получила распространение мысль о том, что разработчикам тоже надо писать тесты?
У меня было смутное представление о некотором тёмном «раньше», и условно-ограниченно-просвещённом «сейчас», когда мысль о том, что тестирование не должно жить отдельно от разработки, кажется, стала нормальной.
Мостик между этими двумя мирами — автотесты, они нужны и тестированию, и разработке. Фреймворк JUnit сознательно писали как можно более простым — в первую очередь для того, чтобы сделать его повседневным инструментом для разработчиков. Люди, работавшие с первыми фреймворками автотестирования, стали также авторами подходов экстремального программирования (XP) и разработки через тестирование (TDD) — т. е. подходов, настаивающих на том, что тестирование — это не «обязаловка», а интегральная часть разработки.
С учётом этого, я решил заняться «археологией» автотестирования: посмотреть на прародителя современных фреймворков xUnit, SUnit для Smalltalk. Я хотел потрогать его руками, а также понять, что двигало его автором. В результате получилось довольно интересное путешествие, которым я хотел бы с вами поделиться.
Вначале я посмотрю на то, что из себя представляло автоматизированное тестирование в 1990-е. Чтобы понять, что добавил SUnit, попробую запустить на нём несколько примитивных тестов. А потом посмотрю, что можно наскрести по сусекам интернета о мотивации создателей и пользователей. Как они пришли к тому, что барьер между разработкой и тестированием надо преодолеть? Сам я не был участником этого процесса (годами не вышел), так что придётся опираться на вторичные источники.
Небольшое пояснение. Немного странно говорить об «археологии» фреймворка в живом, пускай не слишком распространённом языке. Но меня интересует именно первая версия этого фреймворка, написанная в 1994 году, поэтому прошу не обижаться тех, кто им по-прежнему пользуется.
Что уже было в 1990-е
Что из себя представляло автоматизированное тестирование в 1990-е годы? Инструменты тогда были такие:
Инструменты записи и воспроизведения. Тестировщик выполняет действия с тестируемой системой, инструмент записывает действия и потом воспроизводит. К 1990-м это была уже опробованная технология, которая особенно хорошо работала для старых систем, без графических интерфейсов (благодаря тому, что взаимодействие с пользователем было очень простым). Вот пример такого инструмента.
Скриптовые инструменты. Здесь тестировщик уже не записывает действия, а пишет скрипт, который управляет системой. Когда в 1990-е говорили "автоматизированное тестирование", имели в виду в первую очередь такие инструменты, управляемые скриптами.
Общее у всех этих инструментов было то, что разработчики ими не пользовались. Это были большие и сложные системы, работа с которыми требовала слишком много дополнительных усилий.
Тесты, написанные разработчиками. Судя по всему, в объектно-ориентированных языках тогда уже существовала практика писать тесты; я не знаю, насколько она была распространена, но важно то, что она существовала в совершенно отдельном мире от инструментов контроля качества.
Чего ещё не было
Не было инфраструктуры, которая позволила бы связать эти системы воедино, автоматически запускать любые тесты — и чёрного ящика, и белого. Тесты создавались специалистами по тестированию, передавались автоматизаторам, которые писали специальные скрипты, и всё это происходило при минимальном вмешательстве со стороны разработчиков.
Что изменил Smalltalk
Почему именно в Smalltalk возник первый фреймворк для автотестов?
Smalltalk — интересный и во многом экспериментальный язык. Он создавался для образовательных целей, и последовательно реализовал принципы объектно-ориентированного программирования.
Для нас важно то, что в Smalltalk задолго до Agile одним из главных принципов стала инкрементальная разработка. Это выглядело так:
пишем небольшой кусок кода
пишем в «рабочем пространстве» (workspace) небольшое выражение, которое запускает наш код
выводим результат или пользуемся инструментами рабочего пространства для того, чтобы анализировать результат динамически
в случае ошибок система позволяет прямо во время выполнения дополнять код
У этого процесса есть существенные недостатки.
Во-первых, у наших тестов нет повторяемости; выполненное в рабочем пространстве выражение каждый раз надо вбивать повторно.
Во-вторых, это процесс довольно «эгоистичный» — никто, кроме написавшего код человека, не знает, что он был проверен, и как именно он был проверен. Менеджер, тестировщик, другой разработчик, сам этот разработчик через несколько месяцев могут только гадать, какой была проверка.
На Хабре уже высказали гипотезу, что SUnit вырос органически как попытка закрыть эти недостатки. Как именно он это сделал? Засучим рукава и попробуем запустить тесты, которые Кент Бек, автор SUnit, описал в 1994 году.
Что SUnit умел в 1994-м
Чтобы это сделать, вначале я установлю Smalltalk. Из его современных реализаций можно собрать целый зоопарк; я выбрал Pharo (основанный на Squeak, основанный на Smalltalk-80). Pharo Launcher скачивается здесь.
И здесь мы сталкиваемся с отличительной чертой Smalltalk: это одновременно и язык программирования, и IDE, и операционная система.

В Smalltalk нет разделения между кодом и данными: всё является объектами, в том числе классы. Хранятся эти объекты в образах (images), моментальных снимках системы наподобие дампов памяти. Каждое приложение — это такой образ.
Создаю новый образ в Pharo Launcher:

Меня попросят выбрать версию Pharo. В археологических интересах, конечно, хотелось бы запустить нечто как можно более раннее. Но даже самая ранняя из доступных здесь версий Pharo — 2.0 — это 2013 год, на 19 лет позже нужного нам года. Так что не будем пытаться залезать так далеко назад. Вместо этого запустим тесты из статьи Кента Бека с сегодняшним SUnit — благо, он по-прежнему интегрирован в Pharo.
Начну с того, что создам тест. Для этого открою системный браузер:

Он покажет все классы в системе — и предоставляемые изначально, и написанные разработчиком. Тут можно, например, увидеть внутрянку SUnit:

Чтобы добавить новый тест, я очищаю окно исходного кода, ввожу там новый код, и нажимаю Ctrl+S (принять):
TestCase << #SetTestCase package: 'MyTests'
Здесь я пользуюсь современным синтаксисом Pharo, чтобы создать новый класс ('SetTestCase'), дочерний от 'TestCase': так в SUnit создают новые тесты. А 'package' указывает пакет класса (в моём случае пакет создаётся с нуля).
Теперь можно добавить тестовый метод, в котором будет код нашего теста. Выбираю свой пакет MyTests, в нём выбираю класс SetTestCase, и добавляю новый код в нижнее окно:

Когда в окне классов выбран класс, система понимает, что мы добавляем код к этому классу.
Что тут написано? Общий принцип синтаксиса в Smalltalk такой:
объект — сообщение объекту — аргумент сообщения
Например:
empty add: 5
Здесь множество empty — объект, add: — сообщение, 5 — аргумент.
В коде я определяю метод testAdd (он должен начинаться на test, чтобы SUnit распознал его как тест), а затем пишу в нём тест:
Настройка: декларирую локальную переменную empty и присваиваю ей значение пустого множества
Выполнение: добавляю в это множество элемент (для примера — цифра 5)
Проверка: наконец, проверяю, что множество содержит добавленный элемент.
Запускается тест просто: щёлкаю правой кнопкой мыши по классу SetTestCase и выбираю Run tests. SUnit автоматически находит методы, начинающиеся на test, выполняет их, и лаконично сообщает о результатах:

Фикстуры
Переменную empty можно вынести в контекст теста, т.е. в фикстуру, которая автоматически выполняется перед тестом. В SUnit нет того многообразия, к которому мы привыкли сегодня, когда в каждом тестовом классе можно создавать произвольное число фикстур. Здесь всё просто: один класс, один setUp, и один tearDown.
Чтобы вынести переменную empty в контекст, её вначале надо сделать из локальной переменной инстанса. Для этого я в окне исходного кода добавляю классу 'SetTestCase' новый параметр для поля slots:
TestCase << #SetTestCase slots: { #empty }; package: 'MyTests'
Ctrl+S, очищаю окно исходного кода и создаю там метод setUp:
setUp empty := Set new
Поскольку empty теперь — переменная инстанса, специально декларировать её тут уже не нужно.
Наконец, убираю создание empty из нашего теста:
testAdd empty add: 5. self assert: [empty includes: 5]
Ошибки
Прекрасно! Теперь пришло время ближе познакомиться с ошибками.
SUnit с самого начала различал два типа ошибок:
ошибки тестируемого кода (failures), т.е. то, что я выше прямо проверял через
assertошибки в самих тестах (errors)
Иногда бывает нужно убедиться, что тестовый код вызывает ошибку. В диалекте SUnit, с которым я работаю здесь, это делается через оператор raise. Добавляю новый тест к классу SetTestCase:
testIllegal self should: [empty at: 5] raise: Error
Здесь происходит вот что.
[empty at: 5]пытается обратиться к несуществующему элементу в пустом множестве. Это должно вызвать ошибку.... raise: Error— блок, который проверяет, что предыдущий блок вызвал ошибку, и возвращаетtrueилиfalseself should:наша проверка, которая засчитывает тест пройденным, если следующий блок возвращаетtrue.
Сюиты
Чтобы запускать несколько тестовых классов одновременно, SUnit позволяет объединять их в сюиты. В отличие от тестовых классов, сюиты создаются не наследованием от TestSuite, а просто созданием нового объекта этого класса.
Я это сделаю в отдельной песочнице (playground) — временном рабочем пространстве. Щёлкаю левой кнопкой мыши в любой точке интерфейса, выбираю Browse > Playground. Ввожу там следующий код:
| suite |
suite := TestSuite named: 'SetTestSuite'.
suite addTest: (SetTestCase suite).
suite run
Здесь я делаю следующее:
объявляю переменную
suiteприсваиваю ей новый объект класса TestSuite с именем
SetTestSuiteдобавляю к сюите тестовый класс SetTestCase
выполняю сюиту
Щелкаю Do it all:

Песочница выполняет весь этот код, и показывает результат в новом окне:

Это — не какой-то специализированный интерфейс SUnit, в песочнице возможность вот так изучать результаты выполнения предоставляется для любого кода; собственно, это нормальный цикл разработки на Smalltalk.
Результат здесь — объект класса TestResult. В нём тесты классифицированы по категориям:
ошибки кода (failures)
ошибки тестов (errors)
тесты без ошибок (passed)
пропущенные тесты (skipped)
Кроме того, есть данные времени выполнения тестов. Всё это было в TestResult уже в 1994 году.
Впечатления после раскопок
Итак, что мы имеем?
Способ запускать тесты программно, а не вручную (класс TestCase)
Способ обеспечить атомарный контекст для каждого теста (setUp и tearDown)
Различение сбоев в программе и в тесте, а также время выполнения теста (класс TestResult)
Способ классифицировать тесты (TestSuite)
Самое главное — в отличие от других инструментов тестирования, существовавших в то время, всё это доступно на языке разработки, и максимально удобно для ежедневного использования при написании кода.
Вот, собственно, и всё. Предельно простая функциональность, и первоначальная её реализация поместилась на страницах довольно короткой статьи. Для особой аутентичности наверное можно было бы за вечер-другой повторить этот фреймворк самому и запустить на [симуляторе Smalltalk-80].
Про JUnit Мартин Фаулер сказал, что «история разработки программного обеспечения не знает другого случая, когда столь много людей были обязаны столь многим столь малому количеству строк кода». Это ещё в большей степени относится к SUnit.
Чего добивался автор
Этот довольно простой код стал несущей стеной для современного тестирования. Рассчитывал ли его автор на что-либо подобное?
По его собственным словам — однозначно нет. Он решал простую задачу, быстро запустить тесты, и решал тем методом, который был максимально органичным для Smalltalk.
Smalltalk — «радикально» объектно-ориентированный язык. По словам Кента Бека:: «Когда я проектировал первую версию xUnit, я применил один из моих обычных приёмов: превратить что-то в объект, в данном случае превратить всё рабочее пространство в класс. Тогда каждый фрагмент кода из пространства оказывается представлен методом (с префиксом «тест» в качестве примитивной аннотации)».
Всё это было сделано буквально «на коленке», перед встречей с клиентом, когда нужно было объяснить, как лучше всего запускать тесты на Smalltalk. Решение было настолько простым, что о какой-либо его ценности Кент Бек даже не задумывался.
Как насчёт идей Agile, экстремального программирования или разработки через тестирование, к которым автор фреймворка пришёл впоследствии? Они как-то повлияли на этот фреймворк?
Тут не всё однозначно. В черновике книги «Smalltalk Best Practice Patterns», от 1996 года (т. е. уже после создания SUnit) даётся пример разработки на Smalltalk с применением всех описанных в книге паттернов. Там следы TDD найти сложно. Напротив, всё идёт по классической схеме Smalltalk: вначале пишем код, потом для проверки запускаем выражение, которое вызывает этот код.
Правда, надо сказать, что уже в 1997 году в Chrysler Кент Бек организовал команду по принципам экстремального программирования. Ещё раньше на него произвёл впечатление разработчик, показавший, что тестирование не замедляет, а, наоборот, ускоряет написание софта (об этом написано здесь).
И тем не менее, кажется, что влияние этих идей на создание фреймворка если и было, то только косвенное. Кажется, что всё было гораздо проще: я хочу запустить тесты, как мне удобнее всего это сделать?
Параллельные наработки
Интересно, что параллельно Беку похожие инструменты создали ещё несколько людей.
В 1996 году Джерард Месарош написал механизм для автоматического запуска тестов для фреймворка событий, над которым он тогда работал — тоже на Smalltalk. По его словам, это тоже было сделано из чисто практических соображений, сэкономить на времени запуска тестов.
Другой интересный проект был создан в начале 1990-х в фирме Taligent (созданной совместно IBM и Apple), на C++. Организатор тестирования там понял, что нагрузку по юнит-тестированию нужно возложить на разработчиков. Частью его решения было написать собственный фреймворк для юнит-тестирования, который по структуре на самом деле напоминал SUnit — хоть и возник независимо от него.
Вот другой случай — Дэйв Фарли упоминает о том, что писал «какие-то юнит-тесты» в начале 1990-х годов. Правда, это был небольшой эпизод, и, когда он познакомился с JUnit, то перешёл полностью на него. Но побуждение всё равно важное.
Что здесь интересно. Бек, Месарош и Фарли все так или иначе причастны к разновидностям Agile. И для всех трёх работа с тестовыми фреймворками предшествовала этому движению. Настолько, что, по словам Фарли, «я не понимал по-настоящему экстремального программирования, пока не увидел JUnit».
Итоги
Пережив пик популярности в первой половине 1990-х годов, интерес к Smalltalk сошёл на нет после 1996 года; в первом квартале 2024 года Smalltalk использовался в 0,031% пулл-риквестов на GitHub. Коммерческие применения Smalltalk остались довольно нишевыми, и волну хайпа на веб-технологии он не поймал.
Зато Smalltalk популяризовал многие вещи, ставшие потом индустриальными стандартами: графические интерфейсы — и фреймворки для автоматизированного тестирования.
Вернёмся к первоначальному вопросу: как получила распространение мысль о том, что разработчикам тоже надо писать тесты? Через написание примитивного по меркам времени инструмента, подсказанного повседневной практикой разработки. Эта практика пришла из экспериментального языка Smalltalk; и, судя по всему, она просто удачно совпала с потребностью времени в разработке малыми итерациями.
SaemonZixel
Очень редко когда на Хабре можно встретить такую хорошую статью затрагивающую Smalltalk.
mikhail-lankin Автор
спасибо огромное! рад слышать что вам понравилось