С технической точки зрения юнит-тесты – это очень простой инструмент, основанный на паре несложных концепций: (1) тестируемый класс, (2) набор тестовых методов, завернутых в некоторый класс и (3) набор методов, с помощью которых можно удостовериться в том, что состояние тестового класса соответствует (или не соответствует) некоторому значению.
Это очень простая штуковина, которая может кардинальным образом повлиять на процесс разработки в целом. С одной стороны существует TDD (“test-first approach"), при котором тесты «драйвят» не только процессом кодирования, но и процессом проектирования (т.е. дизайном системы). С другой стороны существуют разработчики с противоположной точкой зрения, которые считают юнит-тесты пустой тратой времени, потому что они не приносят никакой ценности пользователю.
Я с уважением и пониманием отношусь к первой категории разработчиков, хотя лично для меня test-first approach не столь удобен, чтобы применять его в повседневной деятельности. Я привык думать о дизайне подсистемы с карандашом в руках и для меня слишком утомительно добавлять новый метод только после добавления теста. Но здесь, на самом деле, речь не о том, когда я предпочитаю писать юнит тесты, а что эти самые тесты для меня значат.
Общепринятое мнение о юнит-тестах сводится к тому, что они предназначены, прежде всего, для улучшения качества кода. Еще одним их полезным свойством является возможность безболезненного рефакторинга кода, поскольку тесты позволят удостовериться в том, что исходное поведение не изменилось.
С этими доводами сложно спорить, однако это далеко не единственные (и с моей точки зрения далеко не главные) преимущества юнит-тестов.
Для меня юнит-тесты – это, прежде всего, «лакмусовая бумажка» хорошего дизайна. Хороший дизайн обладает такими замечательными характеристиками, как слабая связность и четкие отношения между классами и их клиентами. Если написать юнит-тест сложно, невозможно или он получается слишком большим и запутанным, то зачастую это говорит о проблемах не столько в самом тесте, сколько в дизайне тестируемого кода.
Свойство 1. Юнит-тесты положительно влияют на модульность и дизайн системы.
При дизайне системы я часто задаю себе вопрос: «Ок, это отличная идея, но как мы это дело будем тестить?». Если класс зависит от множества других классов, напрямую использует внешние ресурсы или берет на себя слишком много ответственности, то тестировать такой класс будет невероятно сложно. Я не большой фанат дизайна ради дизайна или дизайна только ради тестируемости. Но как показывает практика так далеко заходить и не нужно: хороший дизайн системы достаточно слабосвязный, чтобы покрыть тестами большую часть ключевых частей системы (еще о влиянии юнит-тестов на дизайн и архитектуру можно почитать в заметке “Идеальная архитектура”).
Самое интересное, что юнит-тесты влияют не только на дизайн классов, но и на реализацию методов. Методы с непонятными или сложными обязанностями не только сложно читать, но их сложно и тестировать. «Чистые методы» (т.е. методы без побочных эффектов) являются идеальными с точки зрения юнит-тестирования, ведь они не зависят ни от чего, кроме своих аргументов и не делают ничего другого, кроме возвращения результата.
Но даже если не залазить в «функциональные» дебри, то необходимость юнит-тестирования может, например, помочь определиться с «тактикой» обработки исключений: должен ли метод бросать исключение или его можно «захавать» прямо на месте. Если метод не может выполнить свою работу и при этом ничего не говорит вызывающему коду, то проверить его работоспособность с помощью юнит-теста будет затруднительно, а значит этот метод должен каким-то образом сообщить своим клиентам о неудаче.
Свойство 2. Юнит-тесты – это отличный источник спецификации системы.
В одном из подкастов Кент Бек (Kent Beck), папа JUnit-а, TDD и экстремального программирования, дал следующую характеристику юнит-теста: каждый юнит-тест должен рассказывать историю о тестируемом классе. Юнит-тест – это полезнейший источник спецификации (формальной или неформальной), а не просто набор Assert-ов. Зачастую именно чтение юнит-тестов может помочь понять граничные условия и бизнес-правила, применимые для класса или подсистемы.
Из этого свойства юнит-тестов следует одно неприятное следствие: качеству тестов нужно уделять не меньше внимания, чем продакшн коду. Это значит, что тест должен легко читаться и должен быть простым в сопровождении, в противном случае жизнь его закончится после первого же падения. Программисту, которому придется исправлять этот тест, проще будет снести его совсем вместо того, чтобы разбираться, как это чудо устроено. Кроме того, тесты могут использоваться в качестве спецификации только в том случае, когда они представляют «более высокий уровень абстракции» по сравнению с кодом бизнес логики. Никто не будет использовать тесты в качестве спецификации, если разобраться в самом коде проще, нежели в юнит-тесте.
Свойство 3. Юнит-тесты позволяют повторно использовать потраченные программистом усилия.
Все мы знаем, что радостная и любимая нами стадия разработки значительно короче того уныния, которое начинается при сопровождении системы. Это баян, однако тот факт, что об этом все знают, не делает наш код более простым в сопровождении. Помимо того, что юнит-тесты положительно влияют на дизайн и являются источником спецификации, они еще содержат опыт, потраченный разработчиком при реализации некоторой возможности.
Во-первых, юнит-тесты позволяют лучше понять, о чем думал программист, какие граничные условия он проверил, а какие нет. Во-вторых, во время реализации программист погружается гораздо глубже в контекст решаемой задачи, что может выражаться в дополнительных комментариях внутри тестов, а его потраченные усилия могут быть использованы повторно (ведь каждому последующему программисту понадобиться меньше времени, чтобы оценить глубину всех глубин).
Свойство 4. Юнит-тесты экономят наше время.
С первого взгляда может показаться, что это свойство юнит-тестов является полнейшим абсурдом, поскольку на разработку тестов требуется дополнительное время, которое можно было потратить на разработку полезных возможностей.
Разумная составляющая в этом, конечно есть, особенно если тесты пишутся ради тестов, либо же они настолько убоги, что стоимость сопровождения системы утраивается. Я согласен с тем, что от плохих тестов больше вреда, чем пользы, но если подходить к их написанию с той же ответственностью и разумным прагматизмом, что и к остальному коду, то польза от них будет ощутимая.
Помимо явных преимуществ в долгосрочной перспективе (все же сопровождать код, с нормальными тестами легче), существуют преимущества и более «близорукие». В крупной системе для проверки новой возможности может уходить куча времени просто на то, чтобы поднять рабочее окружение, запустить пяток сервисов, клиентское приложение, затем проклацать через десять экранов, чтобы понять, что текстбокс, который вы только что добавили, ведет себя неправильно. Звучит неправдоподобно, но это значит, что либо вам повезло с вашими проектами, либо, наоборот, мне не повезло с моими. Как ни крути, большинство юнит-тестов – это самый быстрый способ запустить или отладить поведение определенных классов.
Заключение
Я не являюсь ярым фанатом юнит-тестов, я не поклонник TDD, и не сторонник 100% покрытия. Если понять тесты может только их автор, а для получения достойного покрытия было натыкано столько абстракций, что тут сам черт ногу сломит, то нам с вами не по пути. Для меня тесты – это, прежде всего возможность взглянуть на модульность системы под другим углом, рассмотреть классы с точки зрения их входов и выходов, проанализировать граничные условия и понять, что должен делать класс, а что нет. Как и любой инструмент его проще использовать неправильно, а для правильного использования нужен опыт и здравый смысл; тесты – это хорошая штука, только нужно научиться их готовить.
Дополнительные ссылки
1. SERadio Episode 167: The History of JUnit and the Future of Testing with Kent Beck
2. Kent Beck. Development Testing
Это два просто потрясающих подкаста с участием Кента Бека, в котором он рассказывает о культуре тестирования разработчиком (development testing), o JUnit, о Continuous Testing и о многом другом. Очень рекомендую!
Это очень простая штуковина, которая может кардинальным образом повлиять на процесс разработки в целом. С одной стороны существует TDD (“test-first approach"), при котором тесты «драйвят» не только процессом кодирования, но и процессом проектирования (т.е. дизайном системы). С другой стороны существуют разработчики с противоположной точкой зрения, которые считают юнит-тесты пустой тратой времени, потому что они не приносят никакой ценности пользователю.
Я с уважением и пониманием отношусь к первой категории разработчиков, хотя лично для меня test-first approach не столь удобен, чтобы применять его в повседневной деятельности. Я привык думать о дизайне подсистемы с карандашом в руках и для меня слишком утомительно добавлять новый метод только после добавления теста. Но здесь, на самом деле, речь не о том, когда я предпочитаю писать юнит тесты, а что эти самые тесты для меня значат.
Общепринятое мнение о юнит-тестах сводится к тому, что они предназначены, прежде всего, для улучшения качества кода. Еще одним их полезным свойством является возможность безболезненного рефакторинга кода, поскольку тесты позволят удостовериться в том, что исходное поведение не изменилось.
С этими доводами сложно спорить, однако это далеко не единственные (и с моей точки зрения далеко не главные) преимущества юнит-тестов.
Для меня юнит-тесты – это, прежде всего, «лакмусовая бумажка» хорошего дизайна. Хороший дизайн обладает такими замечательными характеристиками, как слабая связность и четкие отношения между классами и их клиентами. Если написать юнит-тест сложно, невозможно или он получается слишком большим и запутанным, то зачастую это говорит о проблемах не столько в самом тесте, сколько в дизайне тестируемого кода.
Свойство 1. Юнит-тесты положительно влияют на модульность и дизайн системы.
При дизайне системы я часто задаю себе вопрос: «Ок, это отличная идея, но как мы это дело будем тестить?». Если класс зависит от множества других классов, напрямую использует внешние ресурсы или берет на себя слишком много ответственности, то тестировать такой класс будет невероятно сложно. Я не большой фанат дизайна ради дизайна или дизайна только ради тестируемости. Но как показывает практика так далеко заходить и не нужно: хороший дизайн системы достаточно слабосвязный, чтобы покрыть тестами большую часть ключевых частей системы (еще о влиянии юнит-тестов на дизайн и архитектуру можно почитать в заметке “Идеальная архитектура”).
Самое интересное, что юнит-тесты влияют не только на дизайн классов, но и на реализацию методов. Методы с непонятными или сложными обязанностями не только сложно читать, но их сложно и тестировать. «Чистые методы» (т.е. методы без побочных эффектов) являются идеальными с точки зрения юнит-тестирования, ведь они не зависят ни от чего, кроме своих аргументов и не делают ничего другого, кроме возвращения результата.
Но даже если не залазить в «функциональные» дебри, то необходимость юнит-тестирования может, например, помочь определиться с «тактикой» обработки исключений: должен ли метод бросать исключение или его можно «захавать» прямо на месте. Если метод не может выполнить свою работу и при этом ничего не говорит вызывающему коду, то проверить его работоспособность с помощью юнит-теста будет затруднительно, а значит этот метод должен каким-то образом сообщить своим клиентам о неудаче.
Свойство 2. Юнит-тесты – это отличный источник спецификации системы.
В одном из подкастов Кент Бек (Kent Beck), папа JUnit-а, TDD и экстремального программирования, дал следующую характеристику юнит-теста: каждый юнит-тест должен рассказывать историю о тестируемом классе. Юнит-тест – это полезнейший источник спецификации (формальной или неформальной), а не просто набор Assert-ов. Зачастую именно чтение юнит-тестов может помочь понять граничные условия и бизнес-правила, применимые для класса или подсистемы.
Из этого свойства юнит-тестов следует одно неприятное следствие: качеству тестов нужно уделять не меньше внимания, чем продакшн коду. Это значит, что тест должен легко читаться и должен быть простым в сопровождении, в противном случае жизнь его закончится после первого же падения. Программисту, которому придется исправлять этот тест, проще будет снести его совсем вместо того, чтобы разбираться, как это чудо устроено. Кроме того, тесты могут использоваться в качестве спецификации только в том случае, когда они представляют «более высокий уровень абстракции» по сравнению с кодом бизнес логики. Никто не будет использовать тесты в качестве спецификации, если разобраться в самом коде проще, нежели в юнит-тесте.
Свойство 3. Юнит-тесты позволяют повторно использовать потраченные программистом усилия.
Все мы знаем, что радостная и любимая нами стадия разработки значительно короче того уныния, которое начинается при сопровождении системы. Это баян, однако тот факт, что об этом все знают, не делает наш код более простым в сопровождении. Помимо того, что юнит-тесты положительно влияют на дизайн и являются источником спецификации, они еще содержат опыт, потраченный разработчиком при реализации некоторой возможности.
Во-первых, юнит-тесты позволяют лучше понять, о чем думал программист, какие граничные условия он проверил, а какие нет. Во-вторых, во время реализации программист погружается гораздо глубже в контекст решаемой задачи, что может выражаться в дополнительных комментариях внутри тестов, а его потраченные усилия могут быть использованы повторно (ведь каждому последующему программисту понадобиться меньше времени, чтобы оценить глубину всех глубин).
Свойство 4. Юнит-тесты экономят наше время.
С первого взгляда может показаться, что это свойство юнит-тестов является полнейшим абсурдом, поскольку на разработку тестов требуется дополнительное время, которое можно было потратить на разработку полезных возможностей.
Разумная составляющая в этом, конечно есть, особенно если тесты пишутся ради тестов, либо же они настолько убоги, что стоимость сопровождения системы утраивается. Я согласен с тем, что от плохих тестов больше вреда, чем пользы, но если подходить к их написанию с той же ответственностью и разумным прагматизмом, что и к остальному коду, то польза от них будет ощутимая.
Помимо явных преимуществ в долгосрочной перспективе (все же сопровождать код, с нормальными тестами легче), существуют преимущества и более «близорукие». В крупной системе для проверки новой возможности может уходить куча времени просто на то, чтобы поднять рабочее окружение, запустить пяток сервисов, клиентское приложение, затем проклацать через десять экранов, чтобы понять, что текстбокс, который вы только что добавили, ведет себя неправильно. Звучит неправдоподобно, но это значит, что либо вам повезло с вашими проектами, либо, наоборот, мне не повезло с моими. Как ни крути, большинство юнит-тестов – это самый быстрый способ запустить или отладить поведение определенных классов.
Заключение
Я не являюсь ярым фанатом юнит-тестов, я не поклонник TDD, и не сторонник 100% покрытия. Если понять тесты может только их автор, а для получения достойного покрытия было натыкано столько абстракций, что тут сам черт ногу сломит, то нам с вами не по пути. Для меня тесты – это, прежде всего возможность взглянуть на модульность системы под другим углом, рассмотреть классы с точки зрения их входов и выходов, проанализировать граничные условия и понять, что должен делать класс, а что нет. Как и любой инструмент его проще использовать неправильно, а для правильного использования нужен опыт и здравый смысл; тесты – это хорошая штука, только нужно научиться их готовить.
Дополнительные ссылки
1. SERadio Episode 167: The History of JUnit and the Future of Testing with Kent Beck
2. Kent Beck. Development Testing
Это два просто потрясающих подкаста с участием Кента Бека, в котором он рассказывает о культуре тестирования разработчиком (development testing), o JUnit, о Continuous Testing и о многом другом. Очень рекомендую!