Внедрение зависимостей с unique_ptr для имитации

У меня есть класс Foo, который использует класс Bar. Bar используется только в Foo, а Foo управляет Bar, поэтому я использую unique_ptr (не ссылку, потому что мне не нужен Bar вне Foo):

using namespace std;
struct IBar {
    virtual ~IBar() = default;  
    virtual void DoSth() = 0;
};

struct Bar : public IBar {
    void DoSth() override { cout <<"Bar is doing sth" << endl;};    
};

struct Foo {
  Foo(unique_ptr<IBar> bar) : bar_(std::move(bar)) {}

  void DoIt() {
    bar_->DoSth();
  }
private:
  unique_ptr<IBar> bar_;
};

Пока все хорошо, все работает. Однако у меня возникла проблема, когда я хочу выполнить модульное тестирование кода:

namespace {
struct BarMock : public IBar {
  MOCK_METHOD0(DoSth, void());
};
}

struct FooTest : public Test {
  FooTest() : barMock{ make_unique<BarMock>() }, out(std::move(barMock)) {}

  unique_ptr<BarMock> barMock;
  Foo out;
};

TEST_F(FooTest, shouldDoItWhenDoSth) {
  EXPECT_CALL(*barMock, DoSth());

  out.DoIt();
}

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

Возможные варианты ДИ:

  • by shared_ptr: в этом случае слишком много (объект Bar не используется совместно Foo и ничем другим)
  • по ссылке на IBar: не вариант (Bar не хранится вне Foo, поэтому созданный объект Bar будет уничтожен, оставив Foo с оборванной ссылкой)
  • by unique_ptr: не тестируется представленным способом
  • путем передачи по значению: невозможно (происходит копирование - та же проблема, что и с unique_ptr).

Единственное решение, которое я получил, - это сохранить необработанный указатель на BarMock до того, как Foo станет единственным владельцем BarMock, то есть:

struct FooTest : public Test {
  FooTest() : barMock{new BarMock} {
    auto ptr = unique_ptr<BarMock>(barMock);
    out.reset(new Foo(std::move(ptr)));
  }

  BarMock* barMock;
  unique_ptr<Foo> out;
};

Нет ли более чистого решения? Должен ли я использовать статическую инъекцию зависимостей (шаблоны)?


person Quarra    schedule 09.11.2016    source источник
comment
Вам может быть интересно прочитать этот ответ.   -  person πάντα ῥεῖ    schedule 09.11.2016
comment
@πάντα ῥεῖ: спасибо за ссылку. Я уже видел это, и это работает для методов, которые принимают unique_ptr в качестве параметра, но я не уверен, что этот подход можно применить для конструкторов.   -  person Quarra    schedule 09.11.2016


Ответы (3)


На самом деле я бы не рекомендовал это в производственной среде, но конструктор псевдонимов для shared_ptr представляет собой, возможно, грязное и работающее решение для вашего случая.
Минимальный рабочий пример (в котором не используется gtest, извините, я из мобильного приложения и не могу протестировать его напрямую):

#include<memory>
#include<iostream>
#include<utility>

struct IBar {
    virtual ~IBar() = default;  
    virtual void DoSth() = 0;
};

struct Bar : public IBar {
    void DoSth() override { std::cout <<"Bar is doing sth" << std::endl;};    
};

struct Foo {
    Foo(std::unique_ptr<IBar> bar) : bar(std::move(bar)) {}

    void DoIt() {
        bar->DoSth();
    }
private:
    std::unique_ptr<IBar> bar;
};

int main() {
    std::unique_ptr<Bar> bar = std::make_unique<Bar>();
    std::shared_ptr<Bar> shared{std::shared_ptr<Bar>{}, bar.get()};
    Foo foo{std::move(bar)};
    shared->DoSth();
    foo.DoIt();
}

Я предполагаю, что ваш тест станет примерно таким:

struct BarMock: public IBar {
    MOCK_METHOD0(DoSth, void());
};

struct FooTest : public testing::Test {
    FooTest() {
        std::unique_ptr<BarMock> bar = std::make_unique<BarMock>();
        barMock = std::shared_ptr<BarMock>{std::shared_ptr<BarMock>{}, bar.get()};
        out = std::make_unique<Foo>{std::move(bar)};
    }

    std::shared_ptr<BarMock> barMock;
    std::unique_ptr<Foo> out;
};

TEST_F(FooTest, shouldDoItWhenDoSth) {
    EXPECT_CALL(*barMock, DoSth());
    out->DoIt();
}

Что делает конструктор псевдонимов?

template< class Y > 
shared_ptr( const shared_ptr<Y>& r, element_type *ptr );

Конструктор псевдонимов: создает shared_ptr, который имеет общую информацию о владении с r, но содержит несвязанный и неуправляемый указатель ptr. Даже если этот shared_ptr выйдет из группы последним из группы, он вызовет деструктор для объекта, изначально управляемого r. Однако вызов get() всегда будет возвращать копию ptr. Программист несет ответственность за то, чтобы этот ptr оставался действительным, пока этот shared_ptr существует, например, в типичных случаях использования, когда ptr является членом объекта, управляемого r или является псевдонимом (например, поникший) r.get()

person skypjack    schedule 09.11.2016
comment
+1 за псевдоним конструктора shared_ptr, полезно знать. Если я правильно понимаю, shared_ptr, созданный с помощью конструктора псевдонимов, не управляет переданным ptr, поэтому он работает как обычный необработанный указатель. Или есть какое-то преимущество в использовании его вместо необработанного ptr? - person Quarra; 10.11.2016

Вы можете сохранить ссылку на издевательский объект, прежде чем передать его конструктору. Я думаю, что это делает код немного хрупким из-за порядка инициализации элементов, но семантически это более ясно, что это означает. Право собственности на BarMock по-прежнему принадлежит исключительно Foo, а дескриптор ссылки хранится у FooTest (аналогично этому ответу).

В основном то же, что и ваш ответ, но с использованием ссылки вместо необработанного указателя.

class FooTest : public ::testing::Test
{
    protected:
        FooTest() :
            bar_mock_ptr(std::make_unique<BarMock>()),
            bar_mock(*bar_mock_ptr),
            foo(std::move(bar_mock_ptr))
        {}
    private:
        // This must be declared before bar_mock due to how member initialization is ordered
        std::unique_ptr<BarMock> bar_mock_ptr; // moved and should not be used anymore
    protected:
        BarMock& bar_mock;
        Foo foo; //ensure foo has the same lifetime as bar_mock
}
person Rufus    schedule 13.11.2020
comment
Это не решает проблему в производственном коде: всякий раз, когда создается Foo, ему необходимо передать ссылку IBar - это означает, что тот, кто управляет Foo, должен управлять IBar, в то время как я хочу, чтобы IBar управлялся Foo, чтобы пользователь Foo может просто внедрить зависимость, но не должен управлять ее временем жизни. - person Quarra; 13.11.2020
comment
Я не меняю определение Foo. Он по-прежнему принимает и сохраняет уникальный указатель на IBar (т. е. управляет IBar). Я просто сохраняю ссылку на IBar, прежде чем передать ее Foo для тестирования/насмешки. - person Rufus; 15.11.2020
comment
вы правы - мой плохой, я не заметил. Хороший подход. - person Quarra; 16.11.2020

В конце концов, я везде использовал этот подход:

struct FooTest : public Test {
  FooTest() : barMock{new BarMock} {
    auto ptr = unique_ptr<BarMock>(barMock);
    out.reset(new Foo(std::move(ptr)));
  }

  BarMock* barMock;
  unique_ptr<Foo> out;
};

и он отлично работает с gtest/gmock.

person Quarra    schedule 09.09.2019