Статья была представлена на конференции «eXtreme Programming and Flexible Processes in Software Engineering — XP2000». © 2000 Тим Маккиннон, Стив Фриман, Филип Крейг. Запланирована к публикации в издании «XP eXamined» издательства Addison-Wesley.
Аннотация
Юнит-тестирование — одна из основополагающих практик экстремального программирования, однако большую часть нетривиального кода сложно тестировать изолированно. При создании набора тестов трудно избежать того, чтобы они не получались сложными, неполными и с трудом поддавались поддержке и интерпретации. Использование мок-объектов для юнит-тестирования улучшает как качество кода предметной области, так и самих тестов. Такой подход позволяет писать юнит-тесты для любых компонентов, упрощает структуру тестов и предотвращает загрязнение кода предметной области элементами тестовой инфраструктуры.

1. Введение
— Однажды, — произнес, наконец, Черепаха Квази с глубоким вздохом, — я был настоящей Черепахой.
Льюис Кэрролл «Приключения Алисы в стране чудес»
Юнит-тестирование — одна из основополагающих практик экстремального программирования [Beck 1999], однако большую часть нетривиального кода сложно тестировать изолированно. Важно убедиться, что каждый раз тестируется только один элемент функциональности, и получать уведомление сразу при возникновении любой проблемы. Стандартное юнит-тестирование затруднено, поскольку оно выполняется через внешний доступ к тестируемому коду.
Мы предлагаем технику под названием «мок-объекты», которая предполагает замену кода предметной области заглушками, эмулирующими реальный код. Такие квази-объекты передаются в целевой код предметной области, который они тестируют изнутри — отсюда и термин «эндотестирование». Данный подход схож с созданием стабов, но имеет два важных отличия: тестирование выполняется на более мелком уровне гранулярности, а сами тесты и стабы используются для управления разработкой производственного кода. По нашему опыту, использование мок-объектов для юнит-тестирования позволяет создавать более надежные тесты и улучшает структуру как основного, так и тестового кода. Юнит-тесты, написанные с использованием мок-объектов, имеют единый формат, что дает команде разработки общий язык. Мы убеждены, что код следует писать так, чтобы его было легко тестировать, и пришли к выводу, что мок-объекты — эффективная техника для достижения этой цели. Кроме того, мы выяснили, что рефакторинг мок-объектов позволяет сократить затраты на написание кода стабов.
Эту статью мы начнем с описания того, как мок-объекты используются в юнит-тестировании. Затем мы опишем преимущества мок-объектов при написании юнит-тестов и кода, а также связанные с ними издержки. И наконец мы вкратце опишем, как пользоваться мок-объектами.
2. Юнит-тестирование с мок-объектами
Ключевой аспект юнит-тестирования — тестировать лишь один элемент функциональность за раз; следует точно знать, что именно тестируется и в чем могут быть проблемы. Тестовый код должен передавать свои намерения как можно проще и яснее. Это может быть нелегко, если тест должен приводить в определенное состояние код предметной области или если код предметной области имеет побочные эффекты. Хуже того, код предметной области может скрывать функции, позволяющие настроить необходимое для тестов состояние.
Ключевой аспект модульного тестирования заключается в том, чтобы проверять лишь один элемент функциональности за раз; необходимо точно знать, что именно тестируется и в чем может заключаться проблема. Тестовый код должен максимально просто и понятно выражать свое назначение. Это может быть затруднительно, если тесту требуется задать определенное состояние предметной области или если код предметной области вызывает побочные эффекты. Что еще хуже, в коде предметной области может не быть предоставлено возможностей для установки состояния, необходимого для теста.
Например, авторы этой статьи создавали инструменты для расширения среды разработки IBM VisualAge под язык Java, и один из них генерировал шаблонные классы. Этот инструмент не должен был создавать новый шаблонный класс, если он уже существует в среде. Наивный юнит-тест для этого требования создавал бы заранее известный класс, пытался бы сгенерировать шаблонный класс с тем же именем и затем проверял, остался ли исходный класс неизменным. Однако в VisualAge это порождает побочные задачи: необходимо убедиться, что исходный класс был создан правильно, проверить наличие у пользователя соответствующих прав, а также выполнить очистку данных в случае падения теста — ни одна из которых не имеет прямого отношения к цели тестирования.
Мы можем избежать этих проблем, предоставив собственную реализацию, которая имитирует элементы VisualAge, необходимые для выполнения нашего теста. Мы называем такие реализации мок-объектами. Наши мок-объекты можно инициализировать состоянием, релевантным для теста, и они могут проверять корректность входных данных, полученных от юнит-теста. В примере ниже JUnitCreatorModel представляет объект, генерирующий тестовые классы в рабочей среде VisualAge, а myMockPackage и myMockWorkspace — это реализации мок-объектов для интерфейсов, поставляемых с VisualAge:
public void testCreationWithExistingClass() {
myMockPackage.addContainedType(
new MockType(EXISTING_CLASS_NAME));
myMockWorkspace.addPackage(mockPackage);
JUnitCreatorModel creatorModel =
new JunitCreatorModel(myMockWorkspace, PACKAGE_NAME);
try {
creatorModel.createTestCase(EXISTING_CLASS_NAME);
fail("Should generate an exception for existing type");
} catch (ClassExistsException ex) {
assertEquals(EXISTING_CLASS_NAME, ex.getClassName());
}
myMockWorkspace.verify();
}
Здесь важно отметить два момента. Во-первых, этот тест проверяет не саму среду VisualAge, а лишь тот фрагмент кода, который мы написали или, если мы придерживаемся принципов разработки через тестирование, собираемся написать. Полноценное поведение системы проверяется в ходе функционального тестирования. Во-вторых, мы не пытаемся переписать VisualAge, а лишь воспроизводим те ответы, которые необходимы для конкретного теста. Большинство методов в мок-реализациях либо не выполняют никаких действий, либо просто сохраняют значения в своих внутренних коллекциях. Например, в классе MockPackage есть метод:
public void addContainedType(Type type) {
myContainedTypes.add(type);
}
Мок-объект — это замещающая реализация, которая эмулирует или инструментирует другой код предметной области. Он должен быть проще реального кода, не дублировать его реализацию и позволять настраивать внутреннее состояние для удобства тестирования. Акцент в мок-реализациях делается на абсолютной простоте, а не на полноте. Например, мок-класс коллекции может всегда возвращать одинаковые результаты при вызове метода индексации, независимо от фактических параметров. Мы обнаружили, что тревожным сигналом чрезмерного усложнения мок-объекта является ситуация, когда он начинает вызывать другие мок-объекты — это может означать, что юнит-тест недостаточно локален. При использовании мок-объектов реальными остаются только сам модульный тест и целевой код предметной области.
3. Не просто стабы
Как техника, мок-объекты очень близки к серверным стабам [Binder 1999]. Основные проблемы использования серверных стабов, которые отмечает Байндер, заключаются в следующем: создание стабов может быть чрезмерно сложным, затраты на их разработку и поддержку — слишком высокими, между стабами могут возникать циклические зависимости, а переключение между стабом и промышленным кодом — сопряжено с рисками.
Главное различие между нашим подходом к использованию стабов и подходом Байндера заключается в степени убежденности в том, что разработку кода предметной области можно направлять с помощью тестов, а отдельные классы — тестировать изолированно. Как отметил один из наших рецензентов: «До появления экстремального программирования предложения тестировщиков [о рефакторинге кода для улучшения тестируемости] вызывали бы лишь смех».
Кроме того, детализированные юнит-тесты в сочетании с прошедшим рефакторинг кодом мок-объектов позволяют снизить затраты на написание стабов и помогают обеспечить возможность независимого тестирования компонентов предметной области. Если мок-объекты становятся слишком сложными для управления, это указывает на то, что их клиенты в коде предметной области являются кандидатами на рефакторинг, поэтому мы избегаем создания цепочек из мок-объектов. Наконец, наш подход к написанию кода, при котором мы передаем стаб-объекты в качестве параметров вместо перелинковки кода предметной области, позволяет четко определить границы юнит-тестирования и снижает риск ошибок при сборке.
4. Зачем использовать мок-объекты?
4.1. Локализация юнит-тестов
4.1.1. Отсрочка инфраструктурных решений
Важный аспект экстремального программирования — не принимать решения по инфраструктуре до того, как это станет необходимым. Например, мы можем захотеть реализовать функциональность, не привязываясь к конкретной базе данных. Пока окончательный выбор не сделан, мы можем написать мок-класс, который обеспечивает минимальное поведение, ожидаемое от нашей базы данных. Это позволяет продолжать писать юнит-тесты для кода приложения, не дожидаясь работающей базы данных. Мок-код также дает нам первоначальное определение функциональности, которая потребуется от базы данных.
4.1.2. Управление масштабом
Юнит-тесты, в отличие от функциональных, должны проверять один элемент функциональности за раз. Юнит-тест, зависящий от сложного состояния системы, может быть трудно настроить, особенно по мере развития остальных частей системы. Мок-объекты позволяют избежать этой проблемы, предоставляя легковесную эмуляцию требуемого состояния системы. Более того, сложная настройка состояния локализована в одном мок-объекте, а не разбросана по множеству юнит-тестов.
Один из авторов этой статьи работал над проектным инструментом, который выгружал код из VisualAge в другую систему контроля версий. По мере развития инструмента проводить юнит-тестирование становилось все сложнее, поскольку стоимость сброса и перенастройки окружения резко возросла. Позже инструмент был подвергнут рефакторингу с использованием мок-реализаций как VisualAge, так и системы контроля версий. В результате его структура улучшилась, а тестирование упростилось.
4.1.3. Ничего не упуская
Некоторые юнит-тесты проверяют очень сложно воспроизводимые условия. Например, для проверки сбоя сервера, мы можем написать мок-объект, реализующий локальное проксирование сервера. Каждый юнит-тест может затем сконфигурировать прокси-объект так, чтобы он давай сбой в ожидаемых условиях, а разработчики могут писать клиентский код, чтобы пройти этот текст.
Некоторые юнит-тесты должны проверять условия, которые очень сложно воспроизвести. Например, для тестирования сбоев сервера мы можем создать мок-объект, который реализует локальный прокси-сервер. Затем каждый юнит-тест может настроить этот прокси так, чтобы он вызывал сбой при определенных условиях, а разработчики — написать клиентский код, который успешно проходит этот тест. Например:
public void testFileSystemFailure() {
myMockServer.setFailure(FILE_SYSTEM_FAILURE);
myApplication.connectTo(myMockServer);
try {
myApplication.doSomething();
fail("Application server should have failed");
} catch (ServerFailedException e) {
assert(true);
}
myMockServer.verify();
}
При таком подходе мок-сервер работает локально, а его сбои происходят контролируемым образом. Тест не зависит от компонентов, внешних по отношению к системе разработки, и изолирован от других возможных сбоев в реальной среде. Такой подход к тестированию можно применять повторно для других типов отказов, и весь набор тестов в совокупности документирует возможные сбои сервера, которые обрабатывает наш клиентский код.
В случае использования ресурсоемкого виджета мы можем создать аналогичный набор юнит-тестов. Мы настраиваем мок-виджет на требуемое состояние и затем проверяем, что он используется корректно. Например, юнит-тест, проверяющий, что виджет опрашивается ровно один раз при отправке регистрационного ключа, может выглядеть следующим образом:
public void testPollCount() {
myMockWidget.setResponseCode(DEVICE_READY);
myMockWidget.setExpectedPollCount(1);
myApplication.sendRegistrationKey(myMockWidget);
myMockWidget.verify();
}
Мок-виджет позволяет запускать тесты на машинах разработчиков без установки реального виджета. Мы также можем инструментировать мок-виджет таким образом, чтобы он проверял корректность собственного вызова — что может быть невозможно при использовании реального виджета.
4.2. Улучшенные тесты
4.2.1. Принцип быстрых сбоев
Объекты предметной области часто дают сбой с задержкой — уже после того, как ошибка возникла. Это одна из причин, почему процесс отладки может быть таким сложным. В тестах, которые проверяют состояние таких объектов, все утверждения выполняются единым блоком уже после выполнения кода предметной области. Это затрудняет изоляцию конкретной точки, в которой произошел сбой. Один из авторов столкнулся с такими проблемами при разработке библиотеки для финансовых расчетов. Юнит-тесты сравнивали наборы результатов после завершения каждого вычисления. Каждый сбой требовал значительных усилий по трассировке для выявления его причины, а проверка промежуточных значений без нарушения инкапсуляции была затруднительна.
Мок-реализация, в свою очередь, может проверять утверждения при каждом взаимодействии с кодом предметной области и, следовательно, с большей вероятностью завершится сбоем в нужный момент, сгенерировав при этом полезное сообщение об ошибке. Это значительно упрощает определение конкретной причины сбоя, особенно если в сообщении также описывается разница между ожидаемыми и фактическими значениями.
Например, в приведенном выше коде виджета мок-виджет «знает», что его должны опросить лишь один раз, и немедленно дает сбой при попытке повторного опроса:
class MockWidget implements Widget {
...
public ResponseCode getDeviceStatus() {
myPollCount++;
if (myPollCount > myExpectedPollCount) {
fail("Polled too many times", myExpectedPollCount,
myPollCount);
}
return myResponseCode;
}
}
4.2.2. Рефакторинг утверждений
При тестировании без мок-объектов каждый юнит-тест обычно содержит собственный набор утверждений, относящихся к коду предметной области. В ходе рефакторинга их можно вынести в общие методы в рамках самого юнит-теста, однако разработчик должен не забывать применять эти методы в новых тестах. В случае же с мок-объектами эти утверждения изначально встроены в их код и применяются по умолчанию при каждом использовании объекта. По мере роста набора юнит-тестов мок-объект начинает использоваться по всей системе, и содержащиеся в нем утверждения автоматически распространяются на новый код. Более того, по мере выявления новых необходимых утверждений, их можно добавить непосредственно в мок-объект, и они автоматически начнут применяться ко всем уже существующим тестам.
В ходе разработки авторы нередко сталкивались с ситуациями, когда утверждения в их мок-объектах вызывали неожиданные сбои. Как правило, это является своевременным напоминанием об ограничении, которое программисты упустили из виду, однако иногда причиной оказывается то, что вызвавшие сбой ограничения не всегда применимы в данном контексте. Подобные случаи указывают на потенциальных кандидатов для рефакторинга — либо код предметной области, либо сами мок-объекты — и способствуют более глубокому пониманию системы разработчиками.
4.3. Влияние на стиль кодирования
Мы заметили, что разработка с применением мок-объектов благоприятно сказалась на стиле кодирования наших команд.
Во-первых, в языках с контролируемой областью видимости, таких как Java, детальное юнит-тестирование может быть затруднительно без нарушения границ области видимости путем предоставления тестовому коду доступа к деталям класса или пакета, либо размещения юнит-тестов непосредственно в пакетах предметной области. Страуструп ввел в C++ концепцию дружественных функций специально для решения этой проблемы [Stroustrup 1992]. Независимо от выбранного решения, такой подход противоречит изначальным принципам проектирования. Разработка с использованием мок-объектов снижает необходимость раскрытия внутренней структуры кода предметной области. В этом случае тест в большей степени оперирует поведением и в меньшей — структурой тестируемого кода.
Во-вторых, использование объектов-одиночек все чаще признается сомнительной практикой [C2]. Юнит-тестирование при наличии одиночек может быть затруднено из-за необходимости управления состоянием между тестами. Более того, у объектов-одиночек могут отсутствовать методы, позволяющие юнит-тесту настроить требуемое состояние или проверить результат после выполнения. Разработка с использованием мок-объектов поощряет стиль кодирования, при котором объекты передаются в код, который в них нуждается. Это делает возможной подмену объектов и снижает риск возникновения непредвиденных побочных эффектов.
В-третьих, разработка с использованием мок-объектов обычно способствует переходу к поведению, при котором используются объекты, аналогичные паттерну Посетитель [Gamma 1994] — мы называем их Умными обработчиками. Например, вместо кода, который запрашивает атрибуты объекта и записывает каждый из них в поток, первым шагом было бы передать объекту поток, в который он сам записывает свои атрибуты. Это сохраняет инкапсуляцию объекта. Таким образом, код изменяется с:
public void printPersonReport(Person person, PrintWriter writer) {
writer.println(person.getName());
writer.println(person.getAge());
writer.println(person.getTelephone());
}
на:
public void printPersonReport(Person person, PrintWriter writer) {
person.printDetails(writer);
}
public class Person {
public void printDetails(PrintWriter writer) {
writer.println(myName);
writer.println(myAge);
writer.println(myTelephone);
}
...
}
Однако по мере усложнения кода становится труднее писать чистые тесты, поскольку универсальный метод println, используемый в printDetails, не отражает нашего понимания предметной области. Вместо этого мы можем написать объект-обработчик, который конкретизирует это взаимодействие между потоком и объектом Person:
public void handleDetails(PersonHandler handler) {
handler.name(myName);
handler.age(myAge);
handler.telephone(myTelephone);
}
Это разделяет аспекты ввода и вывода при отображении объекта Person в потоке. Мы можем проверить как то, что получаем ожидаемые входные данные, так и то, что заданный набор значений корректно отображается. Юнит-тест для входных данных обработчика в таком случае будет выглядеть следующим образом:
void testPersonHandling() {
myMockHandler.setExpectedName(NAME);
myMockHandler.setExpectedAge(AGE);
myMockHandler.setExpectedTelephone(TELEPHONE);
myPerson.handleDetails(myMockHandler);
myMockHandler.verify();
}
За ним последует отдельный юнит-тест, проверяющий корректность вывода данных в коде предметной области для PersonHandler:
void testPersonHandler() {
myMockPrintWriter.setExpectedOutputPattern(
".*" + NAME + ".*" + AGE + ".*" + TELEPHONE + ".*");
myHandler.name(NAME);
myHandler.age(AGE);
myHandler.telephone(TELEPHONE);
myHandler.writeTo(myMockPrintWriter);
myMockPrintWriter.verify();
}
Эти три особенности приводят к тому, что код, разработанный с использованием мок-объектов, соответствует Закону Деметры [Lieberherr 1989], причем это соответствие возникает как эмерджентное свойство. Юнит-тесты подталкивают нас к написанию кода предметной области, который ссылается только на локальные объекты и параметры, без явного намерения сделать это.
4.4. Определение интерфейсов
При написании кода, который зависит от других связанных объектов, мы обнаружили, что разработка с использованием мок-объектов является эффективной техникой для определения интерфейсов этих объектов. Для каждой новой функциональности мы пишем юнит-тест, использующий мок-объекты для моделирования поведения, которое ожидается от нашего целевого объекта его окружением; каждый мок-объект представляет собой гипотезу о том, что в конечном итоге будет делать реальный код. По мере стабилизации кластера из объекта предметной области и его мок-объектов мы можем выявить их взаимодействия для определения новых интерфейсов, которые должна реализовывать система. Интерфейс будет состоять из тех методов мок-объекта, которые не связаны с установкой или проверкой ожиданий. В статически типизированных языках можно затем заменить ссылки на мок-объект в коде предметной области ссылками на новый интерфейс.
Так, представленный выше класс Person изначально использует MockPersonHandler для запуска своих юнит-тестов:
public class Person {
public void handleDetails(MockPersonHandler handler) {
handler.name(myName);
handler.age(myAge);
handler.telephone(myTelephone);
}
...
}
После прохождения всех тестов мы можем выделить следующий интерфейс:
public interface PersonHandler {
void name(String name);
void age(int age);
void telephone(String telephone);
void writeTo(PrintWriter writer);
}
Затем мы вернемся к классу Person и изменим сигнатуры методов для работы с новым интерфейсом:
public void handleDetails(PersonHandler handler) { ... }
Такой подход гарантирует создание минимального интерфейса, необходимого коду предметной области, следуя принципу экстремального программирования, запрещающему добавление функциональности, выходящей за рамки текущего понимания системы.
5. Ограничения мок-объектов
Как и в случае с любыми юнит-тестами, всегда существует риск, что мок-объект может содержать ошибки — например, возвращать значения в градусах вместо радиан. Аналогично, юнит-тестирование не выявит сбои, возникающие при взаимодействии между компонентами. Например, отдельные вычисления для сложной математической формулы могут находиться в пределах допустимой погрешности и успешно проходить юнит-тесты, но совокупная ошибка может оказаться недопустимой. Именно поэтому функциональное тестирование по-прежнему необходимо даже при наличии качественных юнит-тестов. Экстремальное программирование снижает, но не исключает полностью эти риски за счет таких практик, как парное программирование и непрерывная интеграция. Мок-объекты снижают эти риски еще больше благодаря простоте своей реализации.
В некоторых случаях создание мок-объектов для представления типов из сложной внешней библиотеки может быть затруднительным. Наиболее сложным аспектом обычно является определение значений и структур параметров, передаваемых в код предметной области. В событийно-ориентированной системе объект, представляющий событие, может быть корнем графа объектов, каждый из которых требует мокирования для корректной работы кода предметной области. Этот процесс может быть затратным, и иногда его стоимость необходимо сопоставлять с преимуществами от наличия юнит-тестов. Тем не менее, когда необходимо создать заглушки лишь для небольшой части библиотеки, мок-объекты могут быть полезным инструментом.
Важный вывод, который мы сделали при внедрении мок-объектов, заключается в том, что в статически типизированных языках библиотекам стоит определять свои API через интерфейсы, а не классы — это позволит клиентам библиотек использовать подобные методы. Нам удалось расширить VisualAge, поскольку его API был построен на интерфейсах, в то время как класс Vector в Java версии 1 имел множество методов с модификатором final при отсутствии интерфейса, что делало невозможным его подмену.
6. Шаблон юнит-тестирования
В процессе работы с мок-объектами авторы обнаружили, что их юнит-тесты приобрели единый формат:
Создание экземпляров мок-объектов
Задание состояния мок-объектов
Определение ожидаемого поведения в мок-объектах
Вызов кода предметной области с передачей мок-объектов в качестве параметров
Проверка соответствия в мок-объектах
При таком подходе тест четко демонстрирует, чего код предметной области ожидает от своего окружения, по сути документируя свои предусловия, постусловия и предполагаемое использование. Все эти аспекты определяются в исполняемом тестовом коде, расположенном рядом с соответствующим кодом предметной области. Иногда мы замечаем, что дискуссии о том, какие объекты следует проверять, приводят к лучшему пониманию тестов и, следовательно, предметной области. По нашему опыту, такой подход облегчает новым читателям понимание юнит-тестов, поскольку сокращает объем контекста, который им необходимо удерживать в памяти. Мы также обнаружили, что этот подход полезен для демонстрации новым программистам принципов написания эффективных юнит-тестов.
Мы используем этот шаблон так часто, что в процессе рефакторинга вынесли типовые утверждения в набор классов Expectation [Mackinnon 2000]. Это значительно ускоряет создание различных типов мок-объектов. Пока что по итогу рефакторинга мы выделили классы ExpectationCounter, ExpectationList и ExpectationSet. Например, класс ExpectationList имеет следующий интерфейс:
public class ExpectationList extends MockObject {
public ExpectationList(String failureMessage);
public void addExpectedItem(Object expectedItem);
public void addActualItem(Object actualItem);
public void verify() throws AssertionFailedException;
}
Метод verify проверяет, что в ходе теста соответствующие фактические и ожидаемые элементы были добавлены в одном порядке. В случае несоответствия выводится сообщение об ошибке с указанием места расхождения. Мок-объект, для которого важна последовательность, будет либо наследовать от класса ExpectationList, либо делегировать ему выполнение проверок.
7. Выводы
Мы выяснили, что мок-объекты представляют собой незаменимый инструмент для разработки юнит-тестов. Они способствуют созданию лучше структурированных тестов и сокращают затраты на написание кода стабов, обеспечивая единый формат юнит-тестов, который легко изучить и понять. Они также упрощают отладку, предоставляя тесты, которые определяют конкретную точку сбоя в момент возникновения проблемы. В некоторых случаях использование мок-объектов является единственным способом тестирования кода предметной области, зависящего от состояния, которое сложно или невозможно воспроизвести. Что еще важнее, тестирование с помощью мок-объектов улучшает качество кода предметной области за счет сохранения инкапсуляции, сокращения глобальных зависимостей и прояснения взаимодействий между классами. Мы были рады заметить, что коллеги, также принявшие этот подход, наблюдали те же преимущества в своих тестах и коде предметной области.
8. Источники
[Beck 1999] Beck K. Extreme Programming Explained: Embrace Change. Reading: Addison-Wesley, 1999.
[Binder 1999] Binder R.V. Testing Object-Oriented Systems: Models, Patterns, and Tools. Reading: Addison-Wesley, 1999.
[C2] Various Authors. Singletons Are Evil, онлайн по адресу: http://c2.com/cgi/wiki?t (дата обращения: 7 апреля 1999)
[Gamma 1994] Гамма Э., Хелм Р., Джонсон Р., Влиссидес Дж. Паттерны объектно-ориентированного проектирования. СПб.: Питер, 2021.
[Lieberherr 1989] Lieberherr K.J., Holland I.M. Assuring Good Style for Object-Oriented Programs // IEEE Software, vol. 6, № 5, September 1989.
[Mackinnon 2000] Mackinnon T. JUnitCreator, онлайн по адресу: http://www.xpdeveloper.com/cgi-bin/wiki.cgi?JUnitCreator (дата обращения: 17 февраля 2000).
[Stroustrup 1992] Страуструп Б. Дизайн и эволюция С++. М.: ДМК Пресс, 2006.
9. Благодарности
Мы хотели бы поблагодарить рецензентов и следующих коллег за их вклад в эту статью: Тома Айерста, Оливера Байа, Мэтью Кука, Свена Хауорта, Тунга Мака, Питера Маркса, Айвена Мура, Джона Нолана, Пола Симмонса и Дж.Д. Уэзерспуна.
10. Примечания
Мы разместим примеры кода по адресу www.xpdeveloper.com.
11. Товарные знаки
VisualAge является товарным знаком корпорации IBM.