Привет, Хаброжители! Юнит-тестирование — это процесс проверки отдельных модулей программы на корректность работы. Правильный подход к тестированию позволит максимизировать качество и скорость разработки проекта. Некачественные тесты, наоборот, могут нанести вред: нарушить работоспособность кода, увеличить количество ошибок, растянуть сроки и затраты. Грамотное внедрение юнит-тестирования — хорошее решение для развития проекта.
Научитесь разрабатывать тесты профессионального уровня, без ошибок автоматизировать процессы тестирования, а также интегрировать тестирование в жизненный цикл приложения. Со временем вы овладеете особым чутьем, присущим специалистам по тестированию. Как ни удивительно, практика написания хороших тестов способствует созданию более качественного кода.
В этой книге: — Универсальные рекомендации по оценке тестов. — Тестирование для выявления и исключения антипаттернов. — Рефакторинг тестов вместе с рабочим кодом. — Использование интеграционных тестов для проверки всей системы.
У большинства сетевых и печатных ресурсов имеется один недостаток: они подробно излагают основы юнит-тестирования, но практически не выходят за эти рамки. Такие ресурсы могут быть очень ценными, однако обучение на этом не заканчивается. Существует и следующий уровень: умение не просто писать тесты, но делать это так, чтобы ваши усилия приносили максимальную отдачу. К сожалению, многим людям приходится самим разбираться, как выйти на этот уровень, часто методом проб и ошибок. Эта книга поможет вам в этом. В ней приводится точное определение того, что собой представляет качественный тест. Это определение формирует единую систему отсчета, которая поможет вам взглянуть на многие из ваших тестов в новом свете и увидеть, какие из них работают на пользу проекта, а какие следует отрефакторить или вообще удалить.
Если у вас мало опыта в юнит-тестировании, из этой книги вы многое узнаете. Опытный программист, скорее всего, уже понимает некоторые идеи, изложенные здесь. Книга поможет ему осознать, почему приемы и практики, которыми он пользовался все это время, настолько полезны. И не стоит недооценивать этот навык: умение четко донести свои идеи коллегам чрезвычайно полезно.
Важно понимать, как и когда переиспользовать код между тестами. Переиспользование кода между секциями подготовки — хороший способ сокращения и упрощения ваших тестов. В этом разделе будет показано, как сделать это правильно.
Ранее я упоминал, что подготовка тестовых данных часто занимает много места. Есть смысл выделить эту подготовку в отдельные методы или классы, которые затем переиспользуются между тестами. Существуют два способа реализации такого переиспользования, но я рекомендую использовать только один из них; второй способ приводит к повышению затрат на сопровождение теста.
Первый (неправильный) способ переиспользования тестовых данных — инициализация их в конструкторе теста (или методе, помеченном атрибутом [SetUp], если вы используете NUnit), как показано в листинге 3.7.
Два теста в листинге 3.7 имеют общую логику конфигурации. Они содержат одинаковые секции подготовки, а следовательно, эти секции можно полностью выделить в конструктор CustomerTests — именно это и было сделано выше.
Такой подход позволяет значительно сократить объем кода в тестах — вы можете избавиться от большинства (или даже от всех) конфигураций в тестах. Однако у этого подхода есть два серьезных недостатка:
??
— он создает сильную связность (high coupling) между тестами;
— ??он ухудшает читаемость тестов.
Обсудим эти недостатки более подробно.
Сильная связность (high coupling) между тестами как антипаттерн
В новой версии, приведенной в листинге 3.7, все тесты связаны друг с другом: изменение логики подготовки одного теста повлияет на все тесты в классе. Например, если заменить строку
строкой
то тесты, ожидающие 10 единиц шампуня на складе, начнут падать.
Тем самым нарушается важное правило: изменение одного теста не должно влиять на другие тесты. Это правило похоже на то, которое обсуждалось в главе 2: тесты должны работать в изоляции друг от друга. Тем не менее это не одно и то же правило. В данном случае речь идет о независимом изменении тестов, а не об их независимом выполнении. Оба свойства являются важными атрибутами хорошо спроектированного теста.
Чтобы следовать этим правилам, необходимо избегать совместного состояния (shared state) в классах тестов. Следующие два приватных поля служат примерами такого совместного состояния:
Использование конструкторов в тестах ухудшает читаемость
Другой недостаток выделения кода подготовки в конструктор — ухудшение читаемости теста. С таким конструктором просмотр самого теста больше не дает вам полной картины. Чтобы понять, что делает тест, вам приходится смотреть в два места: сам тест и конструктор тест-класса.
Даже если логика подготовки данных проста — допустим, только создание экземпляров Store и Customer, — ее все равно лучше разместить в самом тесте. В противном случае вы будете задаваться вопросом, действительно ли здесь создаются только экземпляры тестируемых классов или же происходит также дополнительная их настройка. Автономный тест, не зависящий от конструктора тест-класса, не оставит вам подобной неопределенности.
3.3.3. Более эффективный способ переиспользования тестовых данных
Использование конструктора — не лучший подход к переиспользованию тестовых данных. Второй (правильный) способ — написать фабричные методы, как показано в листинге 3.8.
Листинг 3.8. Выделение общего кода инициализации в приватные фабричные методы
Выделяя общий код инициализации в приватные фабричные методы, можно сократить код теста с сохранением полного контекста того, что происходит в этом тесте. Более того, приватные методы не связывают тесты друг с другом, при условии что вы сделаете их достаточно гибкими (то есть позволите тестам указать, как должны создаваться тестовые данные).
К примеру, возьмем следующую строку:
Здесь тест явно указывает, что магазин должен содержать 10 единиц шампуня. Код получается одновременно и читаемым, и пригодным для переиспользования. Он читаем, потому что вам не приходится изучать внутреннее устройство фабричного метода, для того чтобы понять атрибуты созданного магазина. Он пригоден для переиспользования, потому что этот метод также можно использовать в других тестах.
Обратите внимание: в этом конкретном примере писать фабричные методы не обязательно, так как логика подготовки весьма проста. Этот код приводится исключительно в демонстрационных целях.
У правила о переиспользовании тестовых данных есть одно исключение. Вы можете создавать тестовые данные в конструкторе в случае, если эти данные используются всеми или почти всеми тестами в проекте. Такая ситуация часто встречается с интеграционными тестами, которые работают с базой данных. Все такие тесты требуют подключения к базе данных, код инициализации которой можно написать один раз и потом переиспользовать во всех интеграционных тестах. Но даже в этом случае будет разумнее добавить базовый класс и инициализировать базу данных в конструкторе этого класса, а не в отдельных классах тестов. Пример общего кода инициализации в базовом классе приведен в листинге 3.9.
Листинг 3.9. Общий код инициализации в базовом классе
Обратите внимание на то, что класс CustomerTests остается без конструктора. Он получает доступ к экземпляру _database, наследуя его от базового класса IntegrationTests.
Владимир Хориков — разработчик, Microsoft MVP и автор на платформе Pluralsight. Профессионально занимается разработкой программного обеспечения более 15 лет, а также обучением команд тонкостям юнит-тестирования. За последние годы Владимир опубликовал несколько популярных серий в блогах, а также онлайн-курс на тему юнит-тестирования. Главное достоинство его стиля обучения, которое часто отмечают студенты, — сильная теоретическая подготовка, которая затем используется на практике.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 25% по купону — Unit Testing
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Научитесь разрабатывать тесты профессионального уровня, без ошибок автоматизировать процессы тестирования, а также интегрировать тестирование в жизненный цикл приложения. Со временем вы овладеете особым чутьем, присущим специалистам по тестированию. Как ни удивительно, практика написания хороших тестов способствует созданию более качественного кода.
В этой книге: — Универсальные рекомендации по оценке тестов. — Тестирование для выявления и исключения антипаттернов. — Рефакторинг тестов вместе с рабочим кодом. — Использование интеграционных тестов для проверки всей системы.
Для кого написана эта книга
У большинства сетевых и печатных ресурсов имеется один недостаток: они подробно излагают основы юнит-тестирования, но практически не выходят за эти рамки. Такие ресурсы могут быть очень ценными, однако обучение на этом не заканчивается. Существует и следующий уровень: умение не просто писать тесты, но делать это так, чтобы ваши усилия приносили максимальную отдачу. К сожалению, многим людям приходится самим разбираться, как выйти на этот уровень, часто методом проб и ошибок. Эта книга поможет вам в этом. В ней приводится точное определение того, что собой представляет качественный тест. Это определение формирует единую систему отсчета, которая поможет вам взглянуть на многие из ваших тестов в новом свете и увидеть, какие из них работают на пользу проекта, а какие следует отрефакторить или вообще удалить.
Если у вас мало опыта в юнит-тестировании, из этой книги вы многое узнаете. Опытный программист, скорее всего, уже понимает некоторые идеи, изложенные здесь. Книга поможет ему осознать, почему приемы и практики, которыми он пользовался все это время, настолько полезны. И не стоит недооценивать этот навык: умение четко донести свои идеи коллегам чрезвычайно полезно.
Структура книги
Одиннадцать глав этой книги разделены на четыре части. В части I изложены основы юнит-тестирования, а также напоминаются наиболее общие практики юнит-тестирования:
??
Глава 1 показывает цели юнит-тестирования, в ней приводится краткий обзор того, как отличить хороший тест от плохого.
??
В главе 2 анализируется определение юнит-тестирования и обсуждаются две основные школы в области юнит-тестирования.
??
Глава 3 рассматривает некоторые базовые вопросы — такие как структура юнит-тестов, переиспользование тестовых данных и параметризация тестов.
В части II мы перейдем к сути дела — вы увидите, какими свойствами должен обладать хороший юнит-тест, а также узнаете, как провести рефакторинг тестов для повышения их качества:
??
В главе 4 определяются четыре характеристики, по которым можно оценить качество теста, а также предоставляется общая система координат, которая используется на протяжении всей книги.
??
В главе 5 объясняется, для чего нужны моки (mocks), и анализируется их связь с хрупкостью тестов.
??
В главе 6 рассматриваются три стиля юнит-тестирования и то, какой из этих стилей производит тесты лучшего качества и почему.
??
Глава 7 показывает, как провести рефакторинг раздутых, чрезмерно усложненных тестов и получить тесты, сочетающие в себе максимальную эффективность с минимальными затратами на сопровождение.
В части III изучаются вопросы интеграционного тестирования:
??
В главе 8 рассматривается интеграционное тестирование в целом, его достоинства и недостатки.
??
В главе 9 обсуждаются моки (mocks) и как работать с ними так, чтобы максимально повысить эффективность ваших тестов.
??
В главе 10 рассматривается работа с реляционными базами данных в тестах.
В главе 11 части IV представлены стандартные антипаттерны юнит-тестирования.
??
Глава 1 показывает цели юнит-тестирования, в ней приводится краткий обзор того, как отличить хороший тест от плохого.
??
В главе 2 анализируется определение юнит-тестирования и обсуждаются две основные школы в области юнит-тестирования.
??
Глава 3 рассматривает некоторые базовые вопросы — такие как структура юнит-тестов, переиспользование тестовых данных и параметризация тестов.
В части II мы перейдем к сути дела — вы увидите, какими свойствами должен обладать хороший юнит-тест, а также узнаете, как провести рефакторинг тестов для повышения их качества:
??
В главе 4 определяются четыре характеристики, по которым можно оценить качество теста, а также предоставляется общая система координат, которая используется на протяжении всей книги.
??
В главе 5 объясняется, для чего нужны моки (mocks), и анализируется их связь с хрупкостью тестов.
??
В главе 6 рассматриваются три стиля юнит-тестирования и то, какой из этих стилей производит тесты лучшего качества и почему.
??
Глава 7 показывает, как провести рефакторинг раздутых, чрезмерно усложненных тестов и получить тесты, сочетающие в себе максимальную эффективность с минимальными затратами на сопровождение.
В части III изучаются вопросы интеграционного тестирования:
??
В главе 8 рассматривается интеграционное тестирование в целом, его достоинства и недостатки.
??
В главе 9 обсуждаются моки (mocks) и как работать с ними так, чтобы максимально повысить эффективность ваших тестов.
??
В главе 10 рассматривается работа с реляционными базами данных в тестах.
В главе 11 части IV представлены стандартные антипаттерны юнит-тестирования.
Переиспользование тестовых данных между тестами
Важно понимать, как и когда переиспользовать код между тестами. Переиспользование кода между секциями подготовки — хороший способ сокращения и упрощения ваших тестов. В этом разделе будет показано, как сделать это правильно.
Ранее я упоминал, что подготовка тестовых данных часто занимает много места. Есть смысл выделить эту подготовку в отдельные методы или классы, которые затем переиспользуются между тестами. Существуют два способа реализации такого переиспользования, но я рекомендую использовать только один из них; второй способ приводит к повышению затрат на сопровождение теста.
Первый (неправильный) способ переиспользования тестовых данных — инициализация их в конструкторе теста (или методе, помеченном атрибутом [SetUp], если вы используете NUnit), как показано в листинге 3.7.
[Fact]
public void Purchase_succeeds_when_enough_inventory()
{
bool success = _sut.Purchase(_store, Product.Shampoo, 5);
Assert.True(success);
Assert.Equal(5, _store.GetInventory(Product.Shampoo));
}
[Fact]
public void Purchase_fails_when_not_enough_inventory()
{
bool success = _sut.Purchase(_store, Product.Shampoo, 15);
Assert.False(success);
Assert.Equal(10, _store.GetInventory(Product.Shampoo));
}
}
Два теста в листинге 3.7 имеют общую логику конфигурации. Они содержат одинаковые секции подготовки, а следовательно, эти секции можно полностью выделить в конструктор CustomerTests — именно это и было сделано выше.
Такой подход позволяет значительно сократить объем кода в тестах — вы можете избавиться от большинства (или даже от всех) конфигураций в тестах. Однако у этого подхода есть два серьезных недостатка:
??
— он создает сильную связность (high coupling) между тестами;
— ??он ухудшает читаемость тестов.
Обсудим эти недостатки более подробно.
Сильная связность (high coupling) между тестами как антипаттерн
В новой версии, приведенной в листинге 3.7, все тесты связаны друг с другом: изменение логики подготовки одного теста повлияет на все тесты в классе. Например, если заменить строку
_store.AddInventory(Product.Shampoo, 10);
строкой
_store.AddInventory(Product.Shampoo, 15);
то тесты, ожидающие 10 единиц шампуня на складе, начнут падать.
Тем самым нарушается важное правило: изменение одного теста не должно влиять на другие тесты. Это правило похоже на то, которое обсуждалось в главе 2: тесты должны работать в изоляции друг от друга. Тем не менее это не одно и то же правило. В данном случае речь идет о независимом изменении тестов, а не об их независимом выполнении. Оба свойства являются важными атрибутами хорошо спроектированного теста.
Чтобы следовать этим правилам, необходимо избегать совместного состояния (shared state) в классах тестов. Следующие два приватных поля служат примерами такого совместного состояния:
private readonly Store _store;
private readonly Customer _sut;
Использование конструкторов в тестах ухудшает читаемость
Другой недостаток выделения кода подготовки в конструктор — ухудшение читаемости теста. С таким конструктором просмотр самого теста больше не дает вам полной картины. Чтобы понять, что делает тест, вам приходится смотреть в два места: сам тест и конструктор тест-класса.
Даже если логика подготовки данных проста — допустим, только создание экземпляров Store и Customer, — ее все равно лучше разместить в самом тесте. В противном случае вы будете задаваться вопросом, действительно ли здесь создаются только экземпляры тестируемых классов или же происходит также дополнительная их настройка. Автономный тест, не зависящий от конструктора тест-класса, не оставит вам подобной неопределенности.
3.3.3. Более эффективный способ переиспользования тестовых данных
Использование конструктора — не лучший подход к переиспользованию тестовых данных. Второй (правильный) способ — написать фабричные методы, как показано в листинге 3.8.
Листинг 3.8. Выделение общего кода инициализации в приватные фабричные методы
public class CustomerTests
{
[Fact]
public void Purchase_succeeds_when_enough_inventory()
{
Store store = CreateStoreWithInventory(Product.Shampoo, 10);
Customer sut = CreateCustomer();
bool success = sut.Purchase(store, Product.Shampoo, 5);
Assert.True(success);
Assert.Equal(5, store.GetInventory(Product.Shampoo));
}
[Fact]
public void Purchase_fails_when_not_enough_inventory()
{
Store store = CreateStoreWithInventory(Product.Shampoo, 10);
Customer sut = CreateCustomer();
bool success = sut.Purchase(store, Product.Shampoo, 15);
Assert.False(success);
Assert.Equal(10, store.GetInventory(Product.Shampoo));
}
private Store CreateStoreWithInventory(
Product product, int quantity)
{
Store store = new Store();
store.AddInventory(product, quantity);
return store;
}
private static Customer CreateCustomer()
{
return new Customer();
}
}
Выделяя общий код инициализации в приватные фабричные методы, можно сократить код теста с сохранением полного контекста того, что происходит в этом тесте. Более того, приватные методы не связывают тесты друг с другом, при условии что вы сделаете их достаточно гибкими (то есть позволите тестам указать, как должны создаваться тестовые данные).
К примеру, возьмем следующую строку:
Store store = CreateStoreWithInventory(Product.Shampoo, 10);
Здесь тест явно указывает, что магазин должен содержать 10 единиц шампуня. Код получается одновременно и читаемым, и пригодным для переиспользования. Он читаем, потому что вам не приходится изучать внутреннее устройство фабричного метода, для того чтобы понять атрибуты созданного магазина. Он пригоден для переиспользования, потому что этот метод также можно использовать в других тестах.
Обратите внимание: в этом конкретном примере писать фабричные методы не обязательно, так как логика подготовки весьма проста. Этот код приводится исключительно в демонстрационных целях.
У правила о переиспользовании тестовых данных есть одно исключение. Вы можете создавать тестовые данные в конструкторе в случае, если эти данные используются всеми или почти всеми тестами в проекте. Такая ситуация часто встречается с интеграционными тестами, которые работают с базой данных. Все такие тесты требуют подключения к базе данных, код инициализации которой можно написать один раз и потом переиспользовать во всех интеграционных тестах. Но даже в этом случае будет разумнее добавить базовый класс и инициализировать базу данных в конструкторе этого класса, а не в отдельных классах тестов. Пример общего кода инициализации в базовом классе приведен в листинге 3.9.
Листинг 3.9. Общий код инициализации в базовом классе
public class CustomerTests : IntegrationTests
{
[Fact]
public void Purchase_succeeds_when_enough_inventory()
{
/* Здесь используется _database */
}
}
public abstract class IntegrationTests : IDisposable
{
protected readonly Database _database;
protected IntegrationTests()
{
_database = new Database();
}
public void Dispose()
{
_database.Dispose();
}
}
Обратите внимание на то, что класс CustomerTests остается без конструктора. Он получает доступ к экземпляру _database, наследуя его от базового класса IntegrationTests.
Об авторе
Владимир Хориков — разработчик, Microsoft MVP и автор на платформе Pluralsight. Профессионально занимается разработкой программного обеспечения более 15 лет, а также обучением команд тонкостям юнит-тестирования. За последние годы Владимир опубликовал несколько популярных серий в блогах, а также онлайн-курс на тему юнит-тестирования. Главное достоинство его стиля обучения, которое часто отмечают студенты, — сильная теоретическая подготовка, которая затем используется на практике.
» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок
Для Хаброжителей скидка 25% по купону — Unit Testing
По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Demonter
Есть вопрос по электронной версии этой, да и других книг, представленных на сайте издательства. На тех, кто делает предзаказ бумажных версий, акция на электронную в подарок распространяется? Если да, то может как-то автоматизировать рассылку ссылок на скачивание электронок после того, как для них открывается продажа, либо размещать их в личном кабинете?
Иначе выходит, что предзаказ невыгоден и неудобен.
ph_piter Автор
Распространяется, попробуем автоматизировать.