你无法忍受 Google Test 的 9 个特性

TDD · horance · Created at · 1835 hits
3

用例描述必须遵循严格的标识符规则

这种严格的规则,虽然给Google Test的实现带来了诸多便捷之处,但这给用户造成了很大的负担。尤其当一个用例需要描述一个数学逻辑规则时,用例的表达力将大大折扣。

TEST_F(RobotCleanerTest, at_the_beginning_the_robot_should_be_in_at_the_initial_position)
{
    ASSERT_EQ(Position(0, 0, NORTH), robot.getPosition());
}

贯穿着“重复设计”的坏味道

如下例,每个TEST_F用例都要重复一次RobotCleanerTest

struct RobotCleanerTest : testing::Test
{
protected:
    RobotCleaner robot;
};

TEST_F(RobotCleanerTest, at_the_beginning_the_robot_should_be_in_at_the_initial_position)
{
    ASSERT_EQ(Position(0, 0, NORTH), robot.getPosition());
}

TEST_F(RobotCleanerTest, should_be_face_west_after_turn_left)
{
   robot.turnLeft();
   ASSERT_EQ(Position(0, 0, WEST), robot.getPosition());
}

Fixture与TEST_F存在隐晦的继承关系

这也是上例中将RobotCleaner robot声明为protected的原因。这种隐晦的继承关系,让刚刚入门使用Google Test的人都大吃一惊。

当然,如果你了解过Google Test的实现技术(TEST_F展开后将生成Fixture的一个子类,并自动地注册到框架中),或者已经习惯了他的的设计,这自然不是问题。

TEST, TEST_F的设计容易让人误解、误用

TEST_F的第一个参数是Fixture的名字。如下例如果被误用为TEST,最理想的情况下,则发生编译时错误。如本例所示,编译器将提示robot是一个未定义的变量。最坏的情况下是,错误发生在运行时,可能存在没有调用预期的SetUp/TearDown的风险。

struct RobotCleanerTest : testing::Test
{
protected:
    RobotCleaner robot;
};

TEST_F(RobotCleanerTest, at_the_beginning_the_robot_should_be_in_at_the_initial_position)
{
    ASSERT_EQ(Position(0, 0, NORTH), robot.getPosition());
}

重写SetUp/TearDown时,缺乏Override的保护

Google Test通过在子类中改写Setup/TearDown来定制Fixture的功能,但程序员往往易于混淆setUp, Setup, SetUp, set_up,尤其在C++98中,由于缺失override的编译时保护,易于让程序员写出违背原意的逻辑代码,这样的错误很可能是运行时错误。

struct RobotCleanerTest : testing::Test
{
    virtual void Setup()   // 本应该为SetUp
    {
        robot.reset();
    }

protected:
    RobotCleaner robot;
};

不符合OO的习惯

每个TEST_F/TEST的实现,感觉是一个个游离的函数,是一个典型的过程式设计,通过重复地使用RobotCleanerTest而使它们联系在一起,这太过于牵强。

此外,需要提取函数时,要么将函数提取到父类的Fixture中,要么提取到匿名的namespace中,物理上隔离非常远,尤其用例数目很多的时候,问题更突出。无论怎么样,Google Test缺乏严格意义上的OO设计。

struct RobotCleanerTest : testing::Test
{

protected:
    RobotCleaner robot;
};

TEST_F(RobotCleanerTest, at_the_beginning_the_robot_should_be_in_at_the_initial_position)
{
    ASSERT_EQ(Position(0, 0, NORTH), robot.getPosition());
}

TEST_F(RobotCleanerTest, should_be_face_west_after_turn_left_1_times)
{
    robot.turnLeft();
    ASSERT_EQ(Position(0, 0, WEST), robot.getPosition());
}

TEST_F(RobotCleanerTest, should_be_face_south_after_turn_left_2_times)
{
    robot.turnLeft();
    robot.turnLeft();
    ASSERT_EQ(Position(0, 0, SOUTH), robot.getPosition());
}

TEST_F(RobotCleanerTest, should_be_face_east_after_turn_left_3_times)
{
    robot.turnLeft();
    robot.turnLeft();
    robot.turnLeft();
    ASSERT_EQ(Position(0, 0, EAST), robot.getPosition());
}

TEST_F(RobotCleanerTest, should_be_face_north_after_turn_left_4_times)
{
    robot.turnLeft();
    robot.turnLeft();
    robot.turnLeft();
    robot.turnLeft();
    ASSERT_EQ(Position(0, 0, NORTH), robot.getPosition());
}

断言违背直觉

把期望值放在前面,而把实际值放在后面,严重违反了英语的阅读习惯。犹如我讨厌诸如if (NULL != ptr)的反人类的代码一样。

ASSERT_EQ(Position(0, 0, WEST), robot.getPosition());

Global级Fixture不能自动发现

GlobalEnvironment需要手动注册到框架,才能被框架发现,而不像TEST_F, TEST, TEST_G无需显式地注册,便能被框架自动发现,设计缺乏统一性,一致性。

#include "gtest/gtest.h"

struct GlobalEnvironment : testing::Environment
{
    virtual void SetUp()
    { ... }

    virtual void TearDown()
    { ... }
};

int main(int argc, char** argv)
{
    testing::AddGlobalTestEnvironment(new GlobalEnvironment);
    testing::InitGoogleTest(&argc, argv);
    return RUN_ALL_TESTS();
}

断言缺乏可扩展性

使用Google Test的断言时,你可以在ASSERT_EQ, ASSERT_NE, ASSERT_TRUE, ASSERT_FALSE之中做选择。当然你可以认为这无可厚非,但这样的设计最大的问题在于:只能使用框架本身所提供的几个为数不多的断言原语,缺乏可扩展性,或者扩展起来非常困难。

例如你想增加个一个ASSERT_NIL的断言,扩展起来变得非常不自然。

没有理由,就是不喜欢

在C++社区中,有很多人在使用Google Test。这归功于它在平台性移植、部署与安装等方面非常成功,尤其符合微软平台上的C++程序员的胃口;其次,Google Test的TEST, TEST_F实现的自动发现机制,相对于CppUnit等框架显得更技高一筹。

但对于高级别的、骨灰级的C++程序员,是无法容忍上述的Google Test的致命性缺陷的,例如严格的标识符命名规则,这种强制的约束几乎等于杀了他。

About Me

刘光聪,程序员,敏捷教练,开源软件爱好者,具有多年大型遗留系统的重构经验,对OOFPDSL等领域具有浓厚的兴趣。


「软件匠艺社区」旨在传播匠艺精神,通过分享好的「工作方式」和「习惯」以帮助程序员更加快乐高效地编程。
No Reply at the moment.
需要 Sign In 后回复方可回复, 如果你还没有账号你可以 Sign Up 一个帐号。