Когда внутри одного класса используется другой, то в общем случае подход к тестированию мне ясен. Для внутреннего класса создаётся интерфейс и далее используется внедрение зависимости, когда конкретную реализацию этого класса мы подставляем при создании внешнего (через конструктор, через сеттер или ещё как-нибудь). Тогда при тестировании создаётся stub или mock, в зависимости от того, что в данный момент тестируется, и просто подставляется вместо этого внутреннего класса.
Но это оправдано в случаях с достаточно большими классами, а что делать, если внутренний класс слишком простой, чтобы создавать для него интерфейс?
Допустим, я пишу некий графический движок, и у меня есть класс меша, который представляет собой сетку полигонов. Для простоты примера примем, что в меше хранятся не отдельные полигоны, а просто массив вершин, и при рендере считается, что каждые три вершины, идущие подряд - это один полигон. Каждая вершина представлена её координатами (хранящимися в векторе - классе Vector3D).
Класс Mesh тестировать нужно - там, например, может быть метод getBoundingBox(), вычисляющий ограничивающий сетку параллепипед. Нужен тест для контроля верности таких вычислений.
Но точно так же нужны тесты и для класса Vector3D! Например, у него может быть метод cross(), вычисляющий векторное произведение, что достаточно нетривиально, чтобы это тестировать.
Использовать в этом случае интерфейс Vector3DInterface нереально - вершин в меше может быть сотни тысяч и тогда тот самый небольшой оверхед на вызов виртуальных методов и хранение таблицы виртуальных функций станет весьма ощутимым.
Использовать вместо Vector3D именно в классе Mesh более простой класс Point с тремя публичными полями x, y, z и без методов тоже не вариант - в некоторых других классах или в методах класса Mesh могут потребоваться именно методы класса Vector3D и создавать в них каждый раз Vector3D из Point - те же самые тормоза на сотнях тысяч вершин.
Если же просто написать тесты и для Vector3D, и для Mesh, то придётся нарушить правило тестирования - не привязываться к порядку тестов. Чтобы сначала тестировать внутренний класс, а потом внешний.
Ну, и, наконец, данный пример - просто пример, вопрос касается не этой конкретной ситуации, а проблемы в целом.
Так как же тестировать мелкий внутренний класс, когда внедрение зависимости недопустимо?
Лично я не вижу в этом проблемы, только я б разделил бы на 3 части
1) тесты для вектора
2) тесты для всяких алгоритмов меша (сама реализация getBoundingBox не обзятально должна сидеть в меше ). Вот кстати сами алгоритмы можешь вынести в отдельный интерфейс и использовать депенденси инжекшн для мокинга.
3) тесты самого меша.
Вообще класс вектора это не обьект (в понимании ООП) а абстрактный тип данных (АТД). В С++ например, довольно важно понимать, что у тебя подразумевается под ключевым словом class.
На части я тоже разделил бы, само собой. Тесты для Vector3D отдельно, тесты для Mesh отдельно. Выносить ли логику getBoundingBox() куда-то или нет, роли в данном случае не играет.
Потому что всё равно получается, что тесты для Mesh (или для того, куда я вынесу логику getBoundingBox()) должны запускаться после тестов для Vector3D, иначе при падении теста для Mesh будет не ясно - то ли проблема в методах Mesh, то ли в методах Vector3D, которые используются внутри.
И тест теряет свою информационную роль - указывать на место возникновения проблемы. Я допущу ошибку в методе Vector3D, а падать тест начнёт для Mesh, потому что вызывался раньше. И это совсем не хорошо.
По поводу АТД - вектор как раз не АТД, в нём нет абстракции и не должно быть по причинам производительности. Когда вершин в сцене миллион, даже лишний байт в структуре данных становится в итоге гигабайтом. А для таблицы виртуальных функций, хранимой по указателю, нужно минимум 4 байта.
Вы немного не верено меня поняли, либо я не не очень понятно и выразился, я насчет вектора имел ввиду, что как раз глупо выделять из него интерфейс и так далее. Далее насчет тестирования меша и порядка вызова тестов:
1. В любом случае вы увидите все упавшие тесты.
2. Вектор такой же базовый тип данных как и число, вы же не опасаетесь, что складывая 2 числа их сложение может поломаться а из за этого поломается у вас меш?
Если сильно смущает, то выделяйте тогда уж тесты в 2 группы:
1) базовая (все мат функции и тд.)
2) все остальное, тестирование сложных классов.
У нас очень похожий подход, проект разбит на несколько библиотек, которые можно тестировать отдельно. Например библиотека Core - в ней сидит класс вектор. Библиотека Render, в ней класс Mesh.
3. Я все же считаю, что ничего страшного не происходит - цель тестов упасть, если что то не так, отказавшись от юниттестов на вектор или на меш вы сделаете только хуже.
Большое спасибо! Теперь понял свою ошибку - я почему-то считал, что после падения одного теста непременно нужно прекращать всё тестирование.
И потому не понимал, зачем в фреймворках есть разные типы ассертов - останавливающие тесты и не останавливающие. Теперь понятно - как раз для подобных случаев, когда нужно запомнить, что была ошибка, но проверять дальше.
И совет с разделением хороший - действительно, можно в крайнем случае разделить проект на разные библиотеки и тестировать их раздельно.
Пишете консольное приложение и там гоняете тесты. Параллельно на листочке пишете своё решение и сверяете ответы. Пишете свой assert. Вектор и матрица же умеет в operator ==, operator !=, вывести на экран и прочее? Посмотрите Google C++ Testing Framework.
Каким образом пишутся сами тесты я знаю. И с Google Testing Framework знаком. Проблема вовсе не в этом!
Проблема в том, что в вышеописанной ситуации при обычном описании тестов они будут зависеть от порядка из вызова, что считается плохой практикой (в том же gtest есть даже специальная опция для запуска тестов каждый раз в новом случайном порядке - чтобы гарантированно не привязываться к порядку тестов). А классический способ обхода этого - внедрение зависимости - тут не применимо. Вопрос был в том, как разрешить эту трудность.