Класс Айсберг и юнит-тестирование Google - PullRequest
2 голосов
/ 11 июня 2019

Я прохожу процесс модульного тестирования существующего кода, который написан не с учетом модульного тестирования.

Есть несколько классов, структурированных примерно так:

class Texture
{
public:
    friend class Model;
private:
 void Load( int a, int b);
 void Update(int a, int b);
 void Use(int a, int b);    
}

class Material
{
public:
    friend class Model;
private:
 void Load(int a);
 void Update(int a);
 void Use(int a);    
}

class Mesh
{
public:
    friend class Model;
private:
 void Load(int a, int b, int c);
 void Update(int a, int b, int c);
 void Use(int a, int b, int c);    
}


class Model
{
    public:

    void Load(); // call all the individual Load()
    void Use(); // call all the individual Use()
}

Причина, по которой они хранятся как частные, заключается в том, что это дизайн таким образом, что только класс Model может их вызывать, следовательно, друг.

[ В настоящем коде есть идиома «Адвокат-клиент», которая ограничивает доступ Model к этим классам, но я оставляю это вне фрагмента кода ]

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

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

Так что сейчас я не уверен, является ли это плохим дизайном кода, и я должен перепроектировать их, чтобы облегчить модульное тестирование, или те, которые я просто продолжаю с модульным тестированием, как есть.

Хотелось бы услышать ваше мнение

Ответы [ 2 ]

1 голос
/ 14 июня 2019

Чтобы сделать этот код тестируемым, я бы ввел три чисто виртуальных интерфейса (ITexture, IMesh, IMaterial) и добавил бесплатный метод для создания таких интерфейсов (например, getTexture), которые возвращали быsmart_ptr типа ITexture.Затем в файле cpp реализуйте метод get[...] и используйте его в рабочем коде для создания объекта Model.В модульных тестах я создавал макет для каждого класса интерфейса и устанавливал правильные ожидания для введенных макетов (например, используя gmock или записывал свой собственный макет).

Пример для Mesh, заголовочный файл,IMesh.hpp:

class IMesh {
public:
    virtual ~IMesh() = default;
    virtual void Load(int a, int b, int c) = 0;
    virtual void Update(int a, int b, int c) = 0;
    virtual void Use(int a, int b, int c) = 0; 
};
std::unique_ptr<MeshI> getMesh(/*whatever is needed to create mesh*/);

файл реализации, MeshImpl.cpp:

#include "IMesh.hpp";

class Mesh : public IMesh {
public:
    Mesh(/*some dependency injection here as well if needed*/);
    void Load(int a, int b, int c) override;
    void Update(int a, int b, int c) override;
    void Use(int a, int b, int c) override; 
};
Mesh::Mesh(/*[...]*/) {/*[...]*/}
void Mesh:Load(int a, int b, int c) {/*[...]*/}
void Mesh:Update(int a, int b, int c) {/*[...]*/}
void Mesh:Use(int a, int b, int c) {/*[...]*/}

Внедрение зависимостей:

Model model{getMesh(), getTexture(), getMaterial()};

Благодаря такому подходу можно достичь:

  1. Лучшая развязка - дружба - это очень сильный механизм связи, в то время как в зависимости от чисто виртуальных интерфейсов - общий подход)
  2. Лучшая тестируемость - не только для класса Model - потому что все методыв интерфейсе должно быть public, чтобы класс Model мог его использовать, теперь вы можете тестировать каждый интерфейс отдельно
  3. Лучшая инкапсуляция: можно создавать необходимые классы только с помощью методов-получателей - реализация недоступнаДля пользователя все личные вещи скрыты.
  4. Лучшая расширяемость: теперь пользователь может предоставлять различные реализации IMesh и вводить его в режим.l если необходимо.

Подробнее о методах DI см. этот вопрос

0 голосов
/ 15 июня 2019

Я бы сказал, что использование friend в этом случае неудачно.На мой взгляд, один хороший вариант использования для friend состоит в том, чтобы разрешить доступ к закрытым элементам между классами, которые концептуально имеют тесную связь.Когда я пишу, они концептуально имеют тесную связь, я имею в виду, что тесная связь не является следствием использования friend, но тесная связь между этими классами обусловлена ​​их зависимостью, которая является следствиемих определенные роли.В таких случаях friend - это механизм для правильного обращения с этой жесткой муфтой.Например, контейнеры и их соответствующие классы итераторов концептуально тесно связаны.

В вашем случае мне кажется, что классы не так тесно связаны на концептуальном уровне.Вы используете friend для другой цели, а именно для обеспечения соблюдения архитектурного правила: только Model должны использовать методы Load, Update и Use.К сожалению, этот шаблон имеет ограничения: если у вас есть другой класс Foo и второе архитектурное правило, которому Foo разрешается вызывать только методы Use, вы не можете выразить оба архитектурных правила: если вы делаете Fooтакже друг других классов, тогда Foo будет предоставлен не только доступ к Use, но также к Load и Update - вы не можете предоставить права доступа детально.

Если мое понимание верно, то я бы сказал, что Load, Update и Use концептуально не private, то есть они не представляют детали реализации класса, который должен быть скрыт для внешней стороны: онипринадлежат к «официальному» API класса, только с дополнительным правилом, что только Model должен их использовать.Часто private методы являются закрытыми, потому что разработчик хочет сохранить свободу переименовывать или удалять их, потому что другой код просто не может получить к ним доступ.Я полагаю, что это не является целью.

Учитывая все это, я бы сказал, что было бы лучше по-другому справиться с этой ситуацией.Сделайте методы Load, Update и Use общедоступными, а также добавьте комментарии для объяснения архитектурных ограничений.И хотя моя аргументация не касалась тестируемости, это также решает одну из ваших проблем тестирования, а именно, позволяет вашим тестам также получать доступ к Load, Update и Use.

Если вы также хотитеуметь высмеивать ваши классы Texture, Material и Mesh, а затем принять во внимание предложение от Quarra для введения соответствующих интерфейсов.


Несмотря на то, что для вашего конкретногоНапример, я предлагаю сделать методы Load, Update и Use общедоступными, я не противник деталей реализации юнит-тестирования.Альтернативные реализации одного и того же интерфейса имеют разные потенциальные ошибки.И поиск ошибок - одна из основных целей тестирования (см. Майерс, Бадгетт, Сандлер: Искусство тестирования программного обеспечения или Байзер: методы тестирования программного обеспечения и многие другие).

В качестве примера рассмотрим memcpy функция: давайте предположим, что вы должны реализовать и протестировать это.Вы начинаете с простого решения, копируя побайтовое, и тщательно его проверяете.Затем вы понимаете, что для вашей 32-битной машины вы можете работать быстрее, если исходный адрес и целевой адрес выровнены по 32-битной схеме: в этом случае вы можете скопировать четыре байта одновременно.Когда вы реализуете это изменение, то новый memcpy внутренне выглядит совсем иначе: есть начальная проверка, подходит ли выравнивание указателя.Если это не подходит, то выполняется оригинальная побайтовая копия, в противном случае выполняется более быстрая процедура копирования (которая также должна обрабатывать случай, когда число байтов не кратно четырем, поэтому может бытьнекоторые дополнительные байты для копирования в конце).

Интерфейс memcpy остается прежним.Тем не менее, я думаю, что вам определенно нужно расширить набор тестов для новой реализации: у вас должны быть тестовые случаи для двух четырехбайтовых выровненных указателей, для случаев, когда только один указатель выровнен для четырехбайтовых символов и т. Д. Вам нужны случаи, когдаоба указателя выровнены по четырем байтам, а число копируемых байтов кратно четырем, и случаи, когда они не кратны четырем, и т. д. То есть ваш набор тестов будет значительно расширен - только потому, что реализациядетали изменились.Новые тесты необходимы для поиска ошибок в новой реализации - хотя все тесты все еще могут использовать общедоступный API, а именно функцию memcpy.

Таким образом, неверно предполагать единицу измеренияТестирование не связано с деталями реализации, и также неверно полагать, что тесты не являются специфичными для реализации только потому, что они тестируют через открытый API.

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

...