воскресенье, декабря 07, 2008

Опять про тестирование приватных функций.

На RSDN-е опять обсуждают "Как тестировать приватные функции класса?", предлагают разные решения и спорят какое из них лучше.
Подобные споры меня всегда очень удручают. Почему? Потому что проблемы на самом деле нет. А попытки решать проблему которой нет порождают чудовищно уродливые решения.

На вопрос "Как тестировать приватные функции класса?" есть только один ответ: - "Только через публичный контракт этого класса." Если в голову лезут еще какие либо "решения", значит смысл модульного тестирования, а тем более TDD, до вас еще не дошел.

Что есть модульный тест? Это пример того, как внешний код будет использовать ваш класс. Зачем может понадобиться модульный тест для приватных методов класса, неужели вы рассчитываете на то, что внешний код будет вызывать приватные методы вашего класса? Так почему не сделать эти методы публичными?
Вопрос о тестировании приватных членов - это вопрос новичков. Вот у меня есть куча приватных методов и мне надо покрыть их тестами, как же мне это сделать? Это делается всегда одним и только одним способом - тестированием публичного контракта класса. Если у вас есть приватный метод, который вы не можете протестировать через публичный контракт, значит у вас что то не так с дизайном класса.

Если вы пользуетесь методикой TDD то вопрос о тестировании приватных членов у вас никогда не возникнет. Сначала вы пишете тест, в котором отрабатываете какой либо из сценариев использования вашего класса (которого еще нет). В результате, после написания теста, вы имеете публичный контракт (или часть контракта) своего будущего класса, который складывается из методов, использованных в тесте. Далее, вы начинаете писать реализацию класса, и совершенно ясно, что все мясо в виде приватных членов, которое вы теперь накодите, уже тестируется через вызовы публичных членов.

Я вот недавно писал библиотеку, в которой доля публичных методов менее 20%, остальные private и protected. Модульные тесты покрывают 95% кода библиотеки, причем тестируются только публичные члены. Я легко достиг бы и 100% покрытия, дописав guard-тесты, поскольку непокрытый код, это по большей части, валидация входных параметров и строки, выкидывающие исключения, но мне просто лень. В этой библиотеке есть полностью приватные классы, которых не видно снаружи, и они покрыты тестами на 100%.

Скажу более, в паре приватных методов я нашел большие куски по 6 -10 строк кода, не покрытые тестами. При внимательном рассмотрении, оказалось что этот код просто лишний :). Он никогда не вызывался ни при каких тестовых сценариях, и я его просто выкинул. Это, кстати, хорошая иллюстрация того, что понимается под последней буквой D (design) в TDD.

Так что никогда не ломайте голову над тем "как тестировать приватные функции класса". Тестируйте сам класс, а не его методы. Сосредоточьтесь на вариантах использования класса, на том, как его будет использовать внешний код. И если после этого у вас остались приватные члены, непокрытые тестами, во-первых, проверьте свои модульные тесты, возможно они не охватывают какой-то из вариантов использования вашего класса. Во-вторых, если с тестами все в порядке, проверьте дизайн тестируемого класса, непокрытый тестами код говорит о том, что он никогда не используется, и его можно выкинуть.

Небольшое дополнение, по прошествии одного дня.
Очень редко бывают случаи когда, действительно надо тестировать приватные члены. Предположим, у нас есть класс хранящий множество объектов и выполняющий с ними какие-то действия. Внутри этого класса, объекты хранятся в массиве, или в линейном списке. И вот по каким либо причинам, например для достижения большей скорости работы, нам необходимо реорганизовать внутренний механизм хранения объектов, вместо списка нам нужно сбалансированное двоичное дерево. Заметьте, что при этом публичный контракт класса не меняется. Но нам надо проверить, что внутренний механизм хранения в классе работает соответствующим образом. Тут без тестирования приватных членов, вероятно, не обойтись. Повторю, что такие случаи возникают крайне редко.