首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >单元测试友好的单例

单元测试友好的单例
EN

Code Review用户
提问于 2016-11-19 11:24:24
回答 2查看 5.5K关注 0票数 13

在我的项目中,我们有一些单例,这在单元测试中往往是有问题的。所以我想找出解决问题的办法。到目前为止,我带着的是:

smart_singleton.h

代码语言:javascript
复制
class smart_singleton
{
public:
    static std::shared_ptr<smart_singleton> get_instance();

private:
    smart_singleton();

    smart_singleton(const smart_singleton&) = delete;
    smart_singleton operator=(const smart_singleton&) = delete;
    smart_singleton(smart_singleton&&) = default;
    smart_singleton& operator=(smart_singleton&&) = default;

    static std::weak_ptr<smart_singleton> weak_instance;
};

smart_singleton.cpp

代码语言:javascript
复制
std::weak_ptr<smart_singleton> smart_singleton::weak_instance;

std::shared_ptr<smart_singleton> smart_singleton::get_instance()
{
    if (auto existing_instance = weak_instance.lock()) {
        return existing_instance;
    } else {
        std::shared_ptr<smart_singleton> tmp_shared(new smart_singleton());
        weak_instance = tmp_shared;
        return tmp_shared;
    }
}

smart_singleton::smart_singleton()
{

}

不同之处在于,您需要保存代码中任何地方的"get_instance()“中的一个get_instance,以便对象不被销毁。在您的生成代码中,它将位于主函数的某个位置(或者某个对象在main的整个范围内都是活动的)。在UT中,这将持续一次测试。

重要信息:

请不要将注意力集中在单例模式本身被劝阻。我的项目有单身汉,改变这意味着要付出很大的努力。我的尝试只是使单元测试更容易。因此,我感兴趣的是,在C++中拥有一个经典的单例实现与我的实现之间的区别,以及后一个可能导致的问题。

EN

回答 2

Code Review用户

回答已采纳

发布于 2016-11-20 06:50:15

我喜欢这个主意。

首先,get_instance()方法似乎不是线程安全的:

std::shared_ptr smart_singleton::get_instance() { if (autoexisting_instance= weak_instance.lock()) {返回existing_instance;}weak_instance.lock{ std::shared_ptr tmp_shared(新的smart_singleton());weak_instance = tmp_shared;返回tmp_shared;}

它并不是线程安全的,但是在这种情况下,文档清楚地说明这一点非常重要。但是,如果您要走这条路线,就应该像这样将get_instance与创建分离开来:

代码语言:javascript
复制
std::shared_ptr<smart_singleton> smart_singleton::create_instance()
{
    if (weak_instance.lock()) {
        throw already_instantiated_error{}; // something better
    }
    std::shared_ptr<smart_singleton> tmp_shared(new smart_singleton());
    weak_instance = tmp_shared;
    return tmp_shared;
}

std::shared_ptr<smart_singleton> smart_singleton::get_instance()
{
    return weak_instance.lock();
}

然后,您可以在文档中注意到,如果没有实例,get_instance()返回一个nullptr;在“全局”范围(如main)中,它们应该调用auto keep_alive = smart_singleton::create_instance();

smart_singleton.h需要头部保护,它应该包括<memory>

代码语言:javascript
复制
#ifndef SMART_SINGLETON_H_
#define SMART_SINGLETON_H_

#include <memory>

...

#endif

对我来说,这个设计的主要问题是它很容易使用错误。任何类缓存单例的静态副本都会导致单元测试失败。单元测试必须盲目地进入,并假定任何地方都没有违规的构造。

最好有一个smart_singleton::reset(),它重置单元测试可以调用的所有存储在单例中的数据。

如果我是这样做的话,我会将它变成一个CRTP类,这样创建smart_singletons就变得非常容易了:

代码语言:javascript
复制
template<class T>
class smart_singleton
{
public:
    static std::shared_ptr<T> get_instance()
    {
        if (auto existing_instance = weak_instance.lock()) {
            return existing_instance;
        } else {
            std::shared_ptr<T> tmp_shared(new T());
            weak_instance = tmp_shared;
            return tmp_shared;
        }
    }
protected:
    smart_singleton() {}
private:
    smart_singleton(const smart_singleton<T>&) = delete;
    smart_singleton operator=(const smart_singleton<T>&) = delete;
    smart_singleton(smart_singleton<T>&&) = default;
    smart_singleton& operator=(smart_singleton<T>&&) = default;

    static std::weak_ptr<T> weak_instance;
};

template<class T>
std::weak_ptr<T> smart_singleton<T>::weak_instance;

现在,要使一个类成为单例,这就是我要做的所有事情:

代码语言:javascript
复制
class my_singleton : public smart_singleton<my_singleton>
{
    friend class smart_singleton<my_singleton>; // so that the smart-singleton can access the private constructor of this class
public:
    ...
private:
    my_singleton();
};
票数 3
EN

Code Review用户

发布于 2017-03-28 05:15:30

正如我在另一个答案中提到的,这个测试单例的解决方案很容易使用错误。单个实例的任何缓存实例都会使您的单元测试出错。但是,还有一种使您的单例单元测试变得友好的方法:让单例实现一个接口。

一个真实的例子是Eclipse的Ecore。Ecore是一个为您生成代码的Java建模框架。它生成的每个包都配备了一个MyPackageFactory --实际上是与MyPackageFactory.eINSTANCE.createFoo()一起使用的单例程序包。然而,MyPackageFactory实际上是一个接口;单例是用MyPackageFactoryImpl实现的。这意味着您可以将单例注入到类中,允许您模拟单元测试。

作为一个粗略的例子:

my-singleton.h

代码语言:javascript
复制
#pragma once

class MySingleton {
public:
    static MySingleton &get_instance();

    // simply define an interface
    virtual void do_something() = 0;
    virtual void set_something(int i) = 0;
};

my-singleton.cpp

代码语言:javascript
复制
#include "my-singleton.h"
#include <iostream>

struct MySingletonImpl final : MySingleton {
    int value;

    void do_something() override {
        std::cout << value << '\n';
    }

    void set_something(int i) override {
        value = i;
    }
};

MySingleton &MySingleton::get_instance() {
    static MySingletonImpl instance;
    return instance;
}

然后,它可以以一种友好的单元测试方式使用,如下所示:

uses-singleton.h

代码语言:javascript
复制
#pragma once

#include "my-singleton.h"

struct UsesSingleton {
    MySingleton *singleton;
    int i;

    explicit UsesSingleton(MySingleton &singleton, int i)
        : singleton{ &singleton }
        , i{ i }
    {}

    void do_something() {
        singleton->do_something();
        singleton->set_something(i);
        singleton->do_something();
    }
};

main.cpp

代码语言:javascript
复制
#include "uses-singleton.h"

int main() {
    UsesSingleton myStruct{ MySingleton::get_instance(), 10 };
    myStruct.do_something();
}

使用-singleton-test.cpp-(仅以googlemock为例。这也是未经测试的)

代码语言:javascript
复制
#include "gtest/gtest.h"
#include "gmock/gmock.h"

class MySingletonMock : public MySingleton {
public:
    MOCK_METHOD0(do_something, void());
    MOCK_METHOD1(set_something, void(int))
};

TEST(UsesSingletonTests, SampleTest) {
    MySingletonMock singleton;

    {
        ::testing::InSequence dummy;

        EXPECT_CALL(singleton, do_something());
        EXPECT_CALL(singleton, set_something(10));
        EXPECT_CALL(singleton, do_something());
    }

    UsesSingleton myStruct{ singleton, 10 };
    myStruct.do_something();
}

请注意,这很可能导致在运行时进行虚拟表查找,除非您在头文件中声明MySingletonImpl并在头文件中定义get_instance()

my-singleton.h

代码语言:javascript
复制
#pragma once

class MySingleton {
public:
    static MySingleton &get_instance();

    // simply define an interface
    virtual void do_something() = 0;
    virtual void set_something(int i) = 0;
};

namespace detail {
    struct MySingletonImpl final : MySingleton {
        int value;

        void do_something() override;

        void set_something(int i) override;
    private:
        // We have to hide it now because it was previously
        // hidden by virtue of being in a cpp file
        MySingletonImpl() = default;
        friend MySingleton &MySingleton::get_instance();
    };
}

MySingleton &MySingleton::get_instance() {
    static detail::MySingletonImpl instance;
    return instance;
}

my-singleton.cpp

代码语言:javascript
复制
#include "my-singleton.h"
#include <iostream>

void detail::MySingletonImpl::do_something() {
    std::cout << value << '\n';
}

void detail::MySingletonImpl::set_something(int i) {
    value = i;
}

然后编译器可能会将它优化到正确的函数调用,特别是如果get_instancedetail::MySingletonImpl &get_instance(),但我不认为这会造成任何问题。

无论哪种方式,性能损失可能都很小,所以这可能不值得你花时间。

票数 1
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/147506

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档