解析gtest框架运行机制
前言
Google test是一款开源的白盒单元测试框架,据说目前在Google内部已在几千个项目中应用了基于该框架的白盒测试。
最近的工作是在搞一个基于gtest框架搭建的自动化白盒测试项目,该项目上线也有一段时间了,目前来说效果还是挺不错的。
侯捷先生在《STL源码剖析》中说过一句话:”会用STL,是一种档次。对STL原理有所了解,又是一个档次。追踪过STL源码又是一个档次。第三种档次的人用起STL来,虎虎生风之势绝非第一档次的人能够望其项背。“
我认为使用一种框架时也是一样,只有当你知道框架内部是如何运行的,不仅知其然,还知其所以然,才能避免一些坑,使框架用起来更效率。
就拿平常项目中用的最简单的一个测试demo(test_foo.cpp)来说吧
int foo(int a, int b)
{
return a + b;
} class TestWidget : public testing::Environment
{
public:
virtual void SetUp();
virtual void TearDown();
}; TEST(Test_foo, test_normal)
{
EXPECT_EQ(2, foo(1, 1));
} int main(int argc, char const *argv[])
{
testing::AddGlobalTestEnvironment(new TestSysdbg);
testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
return 0;
}
你可知道gtest是如何调用被测接口,如何输出测试结果的吗?本文主要解答这个问题。
解析
从预处理开始
可能有些童鞋会问,为什么要从预处理开始讲呢?是这样的,用过gtest框架的都知道,我们所编写的每一个测试用例都是一个TEST宏,想知道背后的运行机制,就得知道那些TEST宏展开后是什么样的,而所有的宏、包含的头文件、inline函数都是在预处理阶段被预处理器展开的,然后才是经过编译器编译成汇编代码,接着汇编器把汇编代码生成可重定位的目标文件,然后链接器再把可重定位的目标文件链接成可执行文件的目标文件。
所以本文从预处理开始介绍。
需要提到的是Gtest中用到了许多的宏技巧以及c++的模板技巧。先不看源码中TEST宏的定义,直接用下面指令单独调用预处理器对源文件进行预处理:
cpp test_foo.cpp test_foo.i –I/ gtest/gtest-1.6/
打开生成的经过预处理的文件test_foo.i
class Test_foo_test_normal_Test : public ::testing::Test
{
public:
Test_foo_test_normal_Test() {} private:
virtual void TestBody();
public:
virtual void SetUp();
virtual void TearDown();
}; class Test_foo_test_normal_Test : public ::testing::Test
{
public:
Test_foo_test_normal_Test() {} private:
virtual void TestBody();
static ::testing::TestInfo* const test_info_ __attribute__ ((unused));
Test_foo_test_normal_Test(Test_foo_test_normal_Test const &);
void operator=(Test_foo_test_normal_Test const &);
}; ::testing::TestInfo* const Test_foo_test_normal_Test
::test_info_ =
::testing::internal::MakeAndRegisterTestInfo(
"Test_foo", "test_normal", __null, __null,
(::testing::internal::GetTestTypeId()),
::testing::Test::SetUpTestCase,
::testing::Test::TearDownTestCase,
new ::testing::internal::TestFactoryImpl<Test_foo_test_normal_Test>); void Test_foo_test_normal_Test::TestBody()
{
switch (0)
case 0:
default:
if (const ::testing::AssertionResult gtest_ar =
(::testing::internal::
EqHelper<(sizeof(::testing::internal::IsNullLiteralHelper(2)) == 1) >
::Compare("2", "foo(1, 1)", 2, foo(1, 1)))) ;
else
::testing::internal::AssertHelper(::testing::TestPartResult::kNonFatalFailure,
"test_foo.cpp", 17, gtest_ar.failure_message()) = ::testing::Message();
} int main(int argc, char *argv[])
{
testing::AddGlobalTestEnvironment(new TestWidget);
testing::InitGoogleTest(&argc, argv);
return (::testing::UnitTest::GetInstance()->Run());
return 0;
}
可以看到TEST宏经过预处理器处理后展开为:
- 定义了一个继承自::testing::test类的新类Test_foo_test_normal_Test,该类的名字为TEST宏两个形参的拼接而成。
- TEST宏中的测试代码被展开并定义为生成类的成员函数TestBody的函数体。
- 生成类的静态数据成员test_info_被初始化为函MakeAndRegisterTestInfo的返回值。具体意义后面介绍。
MakeAndRegisterTestInfo函数
从上面来看MakeAndRegisterTestInfo函数是一个比较关键的函数了,从字面意思上看就是生成并注册该测试案例的信息,在头文件gtest.cc中可以找到关于它的定义,他是一个testing命名空间中的嵌套命名空间internal中的非成员函数:
TestInfo* MakeAndRegisterTestInfo(
const char* test_case_name, const char* name,
const char* type_param,
const char* value_param,
TypeId fixture_class_id,
SetUpTestCaseFunc set_up_tc,
TearDownTestCaseFunc tear_down_tc,
TestFactoryBase* factory) {
TestInfo* const test_info =
new TestInfo(test_case_name, name, type_param, value_param,
fixture_class_id, factory);
GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
return test_info;
}
其中形参的意义如下:
- test_case_name:测试套名称,即TEST宏中的第一个形参。
- name:测试案例名称。
- type_param:测试套的附加信息。默认为无
- value_param:测试案例的附加信息。默认为无
- fixture_class_id:test fixture类的id
- set_up_tc :函数指针,指向函数SetUpTestCaseFunc
- tear_down_tc:函数指针,指向函数TearDownTestCaseFunc
- factory:指向工厂对象的指针,该工厂对象创建上面TEST宏生成的测试类的对象
我们看到在MakeAndRegisterTestInfo函数体中定义了一个TestInfo对象,该对象包含了一个TEST宏中标识的测试案例的测试套名称、测试案例名称、测试套附加信息、测试案例附加信息、创建测试案例类对象的工厂对象的指针这些信息。
下面大家可能就会比较好奇所谓的工厂对象,可以在gtest-internal.h中找带它的定义
template <class TestClass>
class TestFactoryImpl : public TestFactoryBase {
public:
virtual Test* CreateTest() { return new TestClass; }
};
TestFactoryImpl类是一个模板类,它的作用就是单纯的创建对应于模板形参类型的测试案例对象。因为模板的存在也大大简化了代码,否则可能就要写无数个TestFactoryImpl类了,呵呵。
GetUnitTestImpl()->AddTestInfo(set_up_tc, tear_down_tc, test_info);
乍一看似乎是对test_info对象的一些熟悉信息进行设置。究竟是怎么样呢?源码面前,了无秘密,我们还是得去找到它的源码,在gtest-internal-inl中可以找到它的定义
inline UnitTestImpl* GetUnitTestImpl() {
return UnitTest::GetInstance()->impl();
}
它的实现也是非常简单,关键还是在UnitTest类的成员函数GetInstance和返回类型的成员函数impl,继续追踪下去
class GTEST_API_ UnitTest {
public:
// Gets the singleton UnitTest object. The first time this method
// is called, a UnitTest object is constructed and returned.
// Consecutive calls will return the same object.
static UnitTest* GetInstance(); internal::UnitTestImpl* impl() { return impl_; }
const internal::UnitTestImpl* impl() const { return impl_; } private:
mutable internal::Mutex mutex_;
internal::UnitTestImpl* impl_;
} UnitTest * UnitTest::GetInstance() {
#if (_MSC_VER == 1310 && !defined(_DEBUG)) || defined(__BORLANDC__)
static UnitTest* const instance = new UnitTest;
return instance;
#else
static UnitTest instance;
return &instance;
}
根据代码和注释可知GetInstance是Unitest类的成员函数,它仅仅是生成一个静态的UniTest对象然后返回。实际上这么做是为了实现UniTest类的单例(Singleton)实例。而impl只是单纯的返回UniTest的UnitTestImpl类型的指针数据成员impl_。
再联系之前的代码,通过UnitTestImpl类的AddTestInfo设置Test_Info类对象的信息。其实绕了一圈,最终就是通过AddTestInfo设置Test_info类对象的信息,自然地,我们需要知道AddTestInfo的实现啦:
void AddTestInfo(Test::SetUpTestCaseFunc set_up_tc,
Test::TearDownTestCaseFunc tear_down_tc,
TestInfo* test_info) {
GetTestCase(test_info->test_case_name(),
test_info->type_param(),
set_up_tc,
tear_down_tc)->AddTestInfo(test_info);
}
而AddTestInfo是通过GetTestCase函数实现的
TestCase* UnitTestImpl::GetTestCase(const char* test_case_name,
const char* type_param,
Test::SetUpTestCaseFunc set_up_tc,
Test::TearDownTestCaseFunc tear_down_tc) {
// Can we find a TestCase with the given name?
const std::vector<TestCase*>::const_iterator test_case =
std::find_if(test_cases_.begin(), test_cases_.end(),
TestCaseNameIs(test_case_name)); if (test_case != test_cases_.end())
return *test_case; // No. Let's create one.
TestCase* const new_test_case =
new TestCase(test_case_name, type_param, set_up_tc, tear_down_tc); // Is this a death test case?
if (internal::UnitTestOptions::MatchesFilter(String(test_case_name),
kDeathTestCaseFilter)) {
// Yes. Inserts the test case after the last death test case
// defined so far. This only works when the test cases haven't
// been shuffled. Otherwise we may end up running a death test
// after a non-death test.
++last_death_test_case_;
test_cases_.insert(test_cases_.begin() + last_death_test_case_,
new_test_case);
} else {
// No. Appends to the end of the list.
test_cases_.push_back(new_test_case);
} test_case_indices_.push_back(static_cast<int>(test_case_indices_.size()));
return new_test_case;
}
从上面代码可以看出其实并不是一开始猜测的设置Test_Info对象的信息,而是判断包含Test_info对象中的测试套名称、测试案例名称等信息的TestCase对象的指针是否在一个vector向量中,若存在就返回这个指针;若不存在就把创建一个包含这些信息的TestCase对象的指针加入到vector向量中,并返回这个指针。
至于vector向量test_cases_,它是UnitTestImpl中的私有数据成员,在这个向量中存放了整个测试项目中所有包含测试套、测试案例等信息的TestCase对象的指针。
紧接着我们看到从GetTestCase返回的TestCase指针调用TestCase类中的成员函数AddTestInfo,在gtest.cc中可以找到它的定义如下:
void TestCase::AddTestInfo(TestInfo * test_info) {
test_info_list_.push_back(test_info);
test_indices_.push_back(static_cast<int>(test_indices_.size()));
}
调用这个函数的目的是在于将Test_info对象添加到test_info_list_中,而test_info_list_是类TestCase中的私有数据成员,它也是一个vector向量。原型为
std::vector<TestInfo*> test_info_list_;
该向量保存着整个项目中所有包含测试案例对象各种信息的Test_Info对象的指针。
而test_indices_也是类TestCase中的私有数据成员,保存着test_info_list中每个元素的索引号。它仍然是一个vector向量,原型为
std::vector<int> test_indices_;
TEST宏
此时,我们再来看看TEST宏的具体定义实现:
#if !GTEST_DONT_DEFINE_TEST
# define TEST(test_case_name, test_name) GTEST_TEST(test_case_name, test_name)
#endif #define GTEST_TEST(test_case_name, test_name)\
GTEST_TEST_(test_case_name, test_name, \
::testing::Test, ::testing::internal::GetTestTypeId()) #define TEST_F(test_fixture, test_name)\
GTEST_TEST_(test_fixture, test_name, test_fixture, \
::testing::internal::GetTypeId<test_fixture>())
可以看到,TEST宏和事件机制对于的TEST_F宏都是调用了GTEST_TEST_宏,我们再追踪这个宏的定义
#define GTEST_TEST_(test_case_name, test_name, parent_class, parent_id)\
class GTEST_TEST_CLASS_NAME_(test_case_name, test_name) : public parent_class {\
public:\
GTEST_TEST_CLASS_NAME_(test_case_name, test_name)() {}\
private:\
virtual void TestBody();\
static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;\
GTEST_DISALLOW_COPY_AND_ASSIGN_(\
GTEST_TEST_CLASS_NAME_(test_case_name, test_name));\
};\
\
::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_case_name, test_name)\
::test_info_ =\
::testing::internal::MakeAndRegisterTestInfo(\
#test_case_name, #test_name, NULL, NULL, \
(parent_id), \
parent_class::SetUpTestCase, \
parent_class::TearDownTestCase, \
new ::testing::internal::TestFactoryImpl<\
GTEST_TEST_CLASS_NAME_(test_case_name, test_name)>);\
void GTEST_TEST_CLASS_NAME_(test_case_name, test_name)::TestBody()
我们终于看到了在预处理展开中得到的案例类的定义和注册案例类对象信息的定义代码啦。唯一的疑问在于类的名字是GTEST_TEST_CLASS_NAME_,从字面意思可以知道这宏就是获得类的名字
#define GTEST_TEST_CLASS_NAME_(test_case_name, test_name) \
test_case_name##_##test_name##_Test
果不其然,宏GTEST_TEST_CLASS_NAME的功能就是把两个参数拼接为一个参数。
RUN_ALL_TESTS宏
我们的测试程序就是从main函数中的RUN_ALL_TEST的调用开始的,在gtest.h中可以找到该宏的定义
#define RUN_ALL_TESTS()\
(::testing::UnitTest::GetInstance()->Run())
RUN_ALL_TESTS就是简单的调用UnitTest的成员函数GetInstance,我们知道GetInstance就是返回一个单例(Singleton)UnitTest对象,该对象调用成员函数Run
int UnitTest::Run() {
impl()->set_catch_exceptions(GTEST_FLAG(catch_exceptions)); return internal::HandleExceptionsInMethodIfSupported(
impl(),
&internal::UnitTestImpl::RunAllTests,
"auxiliary test code (environments or event listeners)") ? 0 : 1;
}
Run函数也是简单的调用HandleExceptionsInMethodIfSupported函数,追踪它的实现
template <class T, typename Result>
Result HandleExceptionsInMethodIfSupported(
T* object, Result (T::*method)(), const char* location) { if (internal::GetUnitTestImpl()->catch_exceptions()) {
...... //异常处理省略
} else {
return (object->*method)();
}
}
HandleExceptionsInMethodIfSupported是一个模板函数,他的模板形参具现化为调用它的UnitTestImpl和int,也就是T = UnitTestImpl, Result = int。在函数体里调用UnitTestImpl类的成员函数RunAllTests
bool UnitTestImpl::RunAllTests() {
......
const TimeInMillis start = GetTimeInMillis(); //开始计时
if (has_tests_to_run && GTEST_FLAG(shuffle)) {
random()->Reseed(random_seed_);
ShuffleTests();
}
repeater->OnTestIterationStart(*parent_, i); if (has_tests_to_run) {
//初始化全局的SetUp事件
repeater->OnEnvironmentsSetUpStart(*parent_);
//顺序遍历注册全局SetUp事件
ForEach(environments_, SetUpEnvironment);
//初始化全局TearDown事件
repeater->OnEnvironmentsSetUpEnd(*parent_);
//
// set-up.
if (!Test::HasFatalFailure()) {
for (int test_index = 0; test_index < total_test_case_count();
test_index++) {
GetMutableTestCase(test_index)->Run(); //TestCase::Run
}
}
// 反向遍历取消所有全局事件.
repeater->OnEnvironmentsTearDownStart(*parent_);
std::for_each(environments_.rbegin(), environments_.rend(),
TearDownEnvironment);
repeater->OnEnvironmentsTearDownEnd(*parent_);
}
elapsed_time_ = GetTimeInMillis() - start; //停止计时
......
}
如上面代码所示,UnitTestImpl::RunAllTests主要进行全局事件的初始化,以及变量注册。而真正的执行部分在于调用GetMutableTestCase
TestCase* UnitTest::GetMutableTestCase(int i) {
return impl()->GetMutableTestCase(i); //impl返回UnitTestImpl类型指针
} TestCase* UnitTestImpl:: GetMutableTestCase(int i) {
const int index = GetElementOr(test_case_indices_, i, -1);
return index < 0 ? NULL : test_cases_[index];
}
经过两次调用返回vector向量test_cases_中的元素,它的元素类型为TestCase类型。然后调用TestCase::Run
void TestCase::Run() {
...... //省略
const internal::TimeInMillis start = internal::GetTimeInMillis();
for (int i = 0; i < total_test_count(); i++) {
GetMutableTestInfo(i)->Run(); //调用TestCase::GetMutableTestInfo
} //以及Test_Info::Run
...... //省略
} TestInfo* TestCase::GetMutableTestInfo(int i) {
const int index = GetElementOr(test_indices_, i, -1);
return index < 0 ? NULL : test_info_list_[index];
}
看到又转向调用TestCase::GetMutableTestInfo,返回向量test_info_list_的元素。而它的元素类型为Test_info。进而又转向了Test_info::Run
void TestInfo::Run() {
...... //省略
Test* const test = internal::HandleExceptionsInMethodIfSupported(
factory_, &internal::TestFactoryBase::CreateTest,
"the test fixture's constructor");
...... //省略
test->Run(); // Test::Run
...... //省略
}
在TestInfo::Run中调用了HandleExceptionsInMethodIfSupported,通过上文中的分析可以得知该函数在这个地方最终的作用是调用internal::TestFactoryBase::CreateTest将factor_所指的工厂对象创建的测试案例对象的地址赋给Test类型的指针test。所以最后调用了Test::Run。
void Test::Run() {
if (!HasSameFixtureClass()) return; internal::UnitTestImpl* const impl = internal::GetUnitTestImpl();
impl->os_stack_trace_getter()->UponLeavingGTest();
internal::HandleExceptionsInMethodIfSupported(this, &Test::SetUp, "SetUp()");
// We will run the test only if SetUp() was successful.
if (!HasFatalFailure()) {
impl->os_stack_trace_getter()->UponLeavingGTest();
internal::HandleExceptionsInMethodIfSupported(
this, &Test::TestBody, "the test body");
} // However, we want to clean up as much as possible. Hence we will
// always call TearDown(), even if SetUp() or the test body has
// failed.
impl->os_stack_trace_getter()->UponLeavingGTest();
internal::HandleExceptionsInMethodIfSupported(
this, &Test::TearDown, "TearDown()");
}
在Test::Run函数体中我们看到通过HandleExceptionsInMethodIfSupported调用了TestBody,先来看看Test中TestBody的原型声明
virtual void TestBody() = 0;
TestBody被声明为纯虚函数。一切都明朗了,在上文中通过test调用Test::Run,进而通过test::调用TestBody,而test实际上是指向继承自Test类的案例类对象,进而发生了多态,调用的是Test_foo_test_normal_Test::TestBody,也就是我们最初在TEST或者TEST_F宏中所写的测试代码。
如此遍历,就是顺序执行测试demo程序中所写的每一个TEST宏的函数体啦。
总结
经过对预处理得到的TEST宏进行逆向跟踪,到正向跟踪RUN_ALL_TESTS宏,了解了gtest的整个运行过程,里面涉及到一下GOF设计模式的运用,比如工厂函数、Singleton、Impl等。仔细推敲便可发现gtest设计层层跳转,虽然有些复杂,但也非常巧妙,很多地方非常值得我们自己写代码的时候学习的。
另外本文没有提到的地方如断言宏,输出log日志等,因为比较简单就略过了。断言宏和输出log就是在每次遍历调用TestBody的时候进行相应的判断和输出打印,有兴趣的童鞋可以自行研究啦。
下图是一个简单的TEST宏展开后的流程图
最后再简单将gtest的运行过程简述一遍:
- 整个测试项目只有一个UnitTest对象,因而整个项目也只有一个UnitTestImpl对象。
- 每一个TEST宏生成一个测试案例类,继承自Test类。
- 对于每一个测试案例类,由一个工厂类对象创建该类对象。
- 由该测试案例类对象创建一个Test_Info类对象。
- 由Test_Info类对象创建一个Test_case对象
- 创建Test_case对象的指针,并将其插入到UnitTestImpl对象的数据成员vector向量的末尾位置。
- 对每一个TEST宏进行2-6步骤,那么对于唯一一个UnitTestImpl对象来说,它的数据成员vector向量中的元素按顺序依次指向每一个包含测试案例对象信息的TestCase对象。
- 执行RUN_ALL_TESTS宏,开始执行用例。从头往后依次遍历UnitTestImpl对象中vector向量的中的元素,对于其中的每一个元素指针,经过一系列间接的方式最终调用其所对应的测试案例对象的TestBody成员函数,即测试用例代码。
参考文献
(完)
解析gtest框架运行机制的更多相关文章
- 源码深度解析SpringMvc请求运行机制(转)
源码深度解析SpringMvc请求运行机制 本文依赖的是springmvc4.0.5.RELEASE,通过源码深度解析了解springMvc的请求运行机制.通过源码我们可以知道从客户端发送一个URL请 ...
- PULL解析XML的运行机制详解
PULL解析简单易上手,基本上看一遍,基本上就会解析啦,但总是感觉对PULL解析的运行机制不是很了解,就总结了以下事件驱动到底是怎么执行的.. PULL: Android内置了PULL解析器.PULL ...
- grunt源码解析:整体运行机制&grunt-cli源码解析
前端的童鞋对grunt应该不陌生,前面也陆陆续续的写了几篇grunt入门的文章.本篇文章会更进一步,对grunt的源码进行分析.文章大体内容内容如下: grunt整体设计概览 grunt-cli源码分 ...
- 框架学习笔记:深度解析StrangeIoC内部运行机制
StrangeIoC的设计和RobotLegs一致,所以我的解析会对照RobotLegs来看. 整个框架使用的是MVCS的模式,关于MVCS模式大家可以点这里进行查看,这里就不谈了,既然Strange ...
- spring mvc 框架运行机制 + 数据绑定原理
spring mvc 运行主要的组件: 1 前端控制器 (dispatchservlet) 相当于一个重要处理器,它用来调用其他功能模块来分工的效应一次请求,主要起调度的作用. 2. handler ...
- thinkphp5.0框架运行机制分享小结
1 访问index.php 入口文件,定义应用目录,加载框架引导文件 <?php // [ 应用入口文件 ] // 定义应用目录 define('APP_PATH', __DIR__ . '/. ...
- gtest框架
解析gtest框架运行机制 1.前言 Google test是一款开源的白盒单元测试框架,据说目前在Google内部已在几千个项目中应用了基于该框架的白盒测试. 最近的工作是在搞一个基于gtest ...
- 开源通用爬虫框架YayCrawler-框架的运行机制
这一节我将向大家介绍一下YayCrawler的运行机制,首先允许我上一张图: 首先各个组件的启动顺序建议是Master.Worker.Admin,其实不按这个顺序也没关系,我们为了讲解方便假定是这个启 ...
- Web框架——XWAF的代码结构和运行机制(4)
XWAF是一套基于Servlet和java反射技术的Web应用程序框架,它利用Servlet运行机制在服务器上加载和运行,接管客户端请求,依靠ServletRequest对象获取用户请求信息,使用Se ...
随机推荐
- 软工实践 - 第二十八次作业 Beta 冲刺(6/7)
队名:起床一起肝活队 组长博客:https://www.cnblogs.com/dawnduck/p/10146478.html 作业博客:班级博客本次作业的链接 组员情况 组员1(队长):白晨曦 过 ...
- IE6中png背景图片透明的最好处理方法
在IE6浏览器下png(24位)的图片显示是不能透明的. 1.处理办法就是用DDPngMin.js <!--[if IE 6]> <script src="js/DDPng ...
- 使用HTML5的JavaScript选择器操作页面中的元素
<!doctype html><html lang="en"> <head> <meta charset="UTF-8& ...
- JQuery排错关于$(document).ready(function(){});
最近写了好多JQuery.也出了很多问题.不知道怎么回事.程序就不往下执行了.很是郁闷. 查了下资料,这里可能会有以下几种原因:1.js文件的引用路径不正确,特别是使用了命名空间,容易造成路径错误,使 ...
- webpack 3.8 使用 extract-text-webpack-plugin 3.0 抽取css失败:You may need an appropriate loader to handle this file type.
webpack 3.8.1 使用 extract-text-webpack-plugin 3.0.2 抽取css时失败,报错: ERROR in ./src/static/style/localTim ...
- 如何抓取开了gzip的网页
有时候用 file_get_contents() 函数抓取网页会发生乱码现象.有两个原因会导致乱码,一个是编码问题,一个是目标页面开了Gzip. 编码问题好办,把抓取到的内容转下编码即可($conte ...
- PowerMock
EasyMock 以及 Mockito 都因为可以极大地简化单元测试的书写过程而被许多人应用在自己的工作中,但是这 2 种 Mock 工具都不可以实现对静态函数.构造函数.私有函数.Final 函数以 ...
- 数学:Lucas定理
利用Lucas定理解决大组合数取模 Lucas定理是用来求 C(n,m) mod p,p为素数的值.(注意:p一定是素数) Lucas定理用来解决大组合数求模是很有用的 Lucas定理最大的数据处理能 ...
- Idea IntelliJ远程调试教程
总结 第一步:修改startup.sh 在倒第二行加上export JPDA_ADDRESS=8787 最后一行在start前面加上" jpda " 第二步:配置Idea, ...
- centos 防火墙关闭/开启
从配置菜单关闭防火墙是不起作用的,索性在安装的时候就不要装防火墙查看防火墙状态:/etc/init.d/iptables status暂时关闭防火墙:/etc/init.d/iptables stop ...