Test-Driven Development Разработка через тестирование Основные правила Пишем новый код только тогда, когда автоматический тест не сработал. Устраняем дублирование. Следствия правил Проектируя код, мы постоянно запускаем его и получаем представление о том, как он работает, это помогает нам принимать правильные решения. Мы самостоятельно пишем свои собственные тесты, так как не можем ждать, пока кто-нибудь напишет их для нас. Следствия правил Наша среда разработки должна быстро реагировать на небольшие модификации кода. Дизайн программы должен базироваться на множестве сильно связанных компонент, которые слабо сцеплены друг с другом, благодаря чему тестирование упрощается. Порядок действий Написать тест. Заставить компилироваться. Запустить тест и убедиться, что он не работает. Заставить тест работать. Рефакторинг. Проверить, что тест работает. Что дает TDD? Если количество дефектов в программе становится достаточно низким, то команда контроля качества может перейти от реактивной к превентивной работе. Если количество неприятных сюрпризов будет небольшим, то менеджеры проекта смогут с высокой точностью оценивать трудозатраты и привлекать заказчиков к процессу ежедневной разработки проекта. Что дает TDD? Если темы технических дискуссий будут четко определены, то программисты смогут взаимодействовать друг с другом постоянно, а не раз в день или раз в неделю. Если плотность дефектов будет достаточно небольшой мы сможем каждый день получать интегрированный рабочий продукт с добавленной в него новой функциональностью, что позволит построить принципиально иные отношения с заказчиком. Тест - Это процедура, которая позволяет подтвердить или опровергнуть работоспособность кода Автоматические тесты можно выполнить даже если не хватает времени на тестирование. Тесты должны выполняться быстро. Тест позволяет избежать страха и сомнений при изменении кода. Изолированный тест (Isolated Test) Выполнение одного теста не должно влиять на другие тесты. Порядок выполнения тестов не должен иметь значения. Приложение должно быть собрана из множества небольших, взаимодействующих друг с другом объектов. Список тестов (Test List) Составить список задач. Задачи возникающие в процессе работы добавляются в этот список. Цикл Тест => Реализация => Рефакторинг не должен прерываться. Вначале тест (Test First) Тесты должны писаться до кода. Это позволяет контролировать количество работы и следить за дизайном кода. Вначале оператор assert (Assert First) Частью чего является новая функциональность? Какие имена присвоить используемым элементам? Каким образом можно правильно проверить результат работы функциональности? Что считать правильным результатом работы функциональности? Какие другие тесты можно придумать исходя из данного теста? Пример – обмен данными с другой системой через сокет После обмена данными сокет должен быть закрыт, а в буфер должна быть прочитана строка abc [Test] public void CompleteTransactionTest() { Assert.IsTrue(reader.IsClosed); Assert.AreEqual("abc", reply.Contents); } Откуда должен быть прочитан объект reply? [Test] public void CompleteTransactionTest() { Buffer reply = reader.GetContent(); Assert.IsTrue(reader.IsClosed); Assert.AreEqual("abc", reply.Contents); } Откуда берется сокет? [Test] public void CompleteTransactionTest() { Socket reader = new Socket("localhost", DEFAULT_PORT); Buffer reply = reader.GetContent(); Assert.IsTrue(reader.IsClosed); Assert.AreEqual("abc", reply.Contents); } Открываем сервер [Test] public void CompleteTransactionTest() { Server writer = new Server("abc", DEFAULT_PORT); Socket reader = new Socket("localhost", DEFAULT_PORT); Buffer reply = reader.GetContent(); Assert.IsTrue(reader.IsClosed); Assert.AreEqual("abc", reply.Contents); } Тестовые данные (Test Data) Данные должны быть простые в понимании и понятные. Более короткие списки данных предпочтительнее длинных списков Не стоит использовать одну константу в разных местах для обозначения более чем одного понятия. Реалистичные данные (Realistic Data) Вы занимаетесь тестированием системы реального времени, используя цепочки внешних событий, которые возникают при эксплуатации этой системы. Вы сравниваете вывод текущей системы с выводом предыдущей системы. Вы выполняете рефакторинг кода, имитирующего некоторый реальный процесс, и ожидаете, что после выполнения рефакторинга результирующие данные будут в точности такими же как до рефакторинга Понятные данные (Evident Rate) Bank bank = new Bank(); bank.AddRate("USD", "GBR", STANDART_RATE); bank.SetComission(STANDART_COMISSION); Money result = bank.Convert(new Money(100, "USD"), "GBR"); Assert.AreEqual(new Money(49.25, "GBR"), result); Понятные данные (Evident Rate) Bank bank = new Bank(); bank.AddRate("USD", "GBR", 2.0); bank.SetComission(0.015); Money result = bank.Convert( new Money(100, "USD"), "GBR"); Assert.AreEqual( new Money(100/2.0*(1 - 0.015), "GBR"), result); Тест одного шага (One Step Test) Тест должен добавить новую функциональность. Вы должны реализовать тест за один шаг. Начальный тест (Starter Test) Первый тест не выполняет каких либо сложных действий. Тривиальные входные и выходные данные. Объясняющий тест (Explanation Test) Объясняйте работу кода в виде тестов. Просите у других объяснять работу их кода в виде тестов. Преобразовывайте диаграммы последовательностей в тесты. Тест для изучения (Learning Test) Перед использованием метода библиотеки, поведение которого не совсем ясно. При проверки новой версии библиотеки, которую вы используете. Регрессионный тест (Regression Test) Напишите тест прежде чем исправлять ошибку. Перерыв (Break) Если вы зашли в тупик, то пора сделать перерыв. Начать сначала (Do over) Если не удается найти хорошего решения, то необходимо отказаться от неудачного кода. Дочерний тест (Child Test ) Что делать, если тест оказался слишком большим? Поддельный объект (Mock Object) Замена реального объекта (например, базы данных) его фальшивой реализацией. Не стоит использовать глобальные переменные для хранения объектов тестов. NMock Самошунтирование (Self Shunt) Для проверки корректности взаимодействия одного объекта с другим, можно заставить тестируемый объект взаимодействовать не с целевым объектом, а с вашим тестом. Строка журнал (Log String) Как проверить, что обращение к методам осуществляется в правильном порядке? Создать строку, использовать ее в качестве журнала, каждый раз при обращении к методу добавлять новый текст. Тестирование обработки ошибок (Crush Test Dummy) Создать специальный объект, который вместо реальной работы генерирует исключение. [Test, ExpectedException(typeof(IOException))] public void SaveExceptionTest() { FileStream fileStream = new FullFileStream("1", FileMode.Create); new Data().SaveToFile(fileStream); } Поддельный объект internal class FullFileStream : FileStream { public FullFileStream(string path, FileMode mode) : base(path, mode) { } public override void Write(byte[] array, int offset, int count) { throw new IOException(); } } Сломанный тест (Broken Test) Если вы работаете над проектом один, то можно оставить сломанный тест, как закладку. Чистый выпускаемый код (Clean Check-in) При работе в составе команды к завершению рабочего дня все тесты должны работать. Подделка (Fake It) Сделайте так, чтобы тестируемый метод возвращал константу. После того как тест начал срабатывать, трансформируйте константу в выражение. Триангуляция (Triangulate) [Test] public void SumTest() { Assert.AreEqual(4, Plus(3, 1)); } private int Plus(int augend, int addend) { return 4; } Триангуляция (Triangulate) [Test] public void SumTest() { Assert.AreEqual(4, Plus(3, 1)); Assert.AreEqual(7, Plus(3, 4)); } private int Plus(int augend, int addend) { return augend + addend; } Очевидная реализация (Obvious Implementation) Если вы точно знаете какой код нужно написать для того, чтобы тест заработал, то смело пишите его. От одного ко многим (One to Many) Как реализовать операцию, которая работает с коллекцией объектов? Сначала реализуйте операцию для одного объекта, затем модернизируйте ее для работы с коллекцией объектов. Насколько большими должны быть шаги? Какой объем функциональности должен покрывать каждый тест? Сколько стадий должно быть преодолено в процессе каждого рефакторинга? Что подлежит тестированию? Условные операторы. Циклы. Операции. Полиморфизм. Как определить качество тестов? Длинный код инициализации. Дублирование кода инициализации. Тесты выполняются слишком медленно. Хрупкие тесты. Когда следует удалять тесты? Никогда не удаляйте тест, если в результате этого снизится ваша уверенность в корректности поведения системы. Если у вас есть два теста, которые тестируют один и тот же участок кода, но рассматриваются читателем как два различных сценария, сохраните оба теста. Нерешенные проблемы TDD Сложно произвести автоматическое тестирование GUI. Особенно в случае нестандартного интерфейса. Сложно тестировать распределенные объекты и много-поточные приложения. TDD нельзя использовать для разработки схемы базы данных. TDD нельзя использовать для разработки языка программирования. TDD и Экстремальное программирование Программирование в паре – тесты как инструмент общения Работа на свежую голову. Если вы не можете заставить тест работать, значит пришло время сделать перерыв. Частая интеграция. Интеграция после каждого цикла тестирования. Простой дизайн. Рефакторинг. Вы можете быть уверены, что поведение системы не изменилось. Частые выпуски версий.