Please enable Javascript to view the contents

Envoy 的单元测试套路

 ·  ☕ 2 分钟

Envoy 的单元测试套路

以下假设已经读过:

被测试对象

/source/extensions/transport_sockets/tcp_stats/tcp_stats.h

1
2
3
4
5
6
class TcpStatsSocket : public TransportSockets::PassthroughSocket,
                       Logger::Loggable<Logger::Id::connection> {



  Network::TransportSocketCallbacks* callbacks_{};

/source/extensions/transport_sockets/tcp_stats/tcp_stats.cc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
46: void TcpStatsSocket::onConnected() {
47:   if (config_->update_period_.has_value()) {
48:     timer_ = callbacks_->connection().dispatcher().createTimer([this]() {
49:       recordStats();
50:       timer_->enableTimer(config_->update_period_.value());
51:     });
52:     timer_->enableTimer(config_->update_period_.value());
53:   }
54: 
55:   transport_socket_->onConnected();
56: }


void TcpStatsSocket::setTransportSocketCallbacks(Network::TransportSocketCallbacks& callbacks) {
  callbacks_ = &callbacks;
  transport_socket_->setTransportSocketCallbacks(callbacks);
}

77: absl::optional<struct tcp_info> TcpStatsSocket::querySocketInfo() {
78:   struct tcp_info info;
79:   memset(&info, 0, sizeof(info));
80:   socklen_t optlen = sizeof(info);
81:   const auto result = callbacks_->ioHandle().getOption(IPPROTO_TCP, TCP_INFO, &info, &optlen); // <-----
82:   if (result.return_value_ != 0) {
83:     ENVOY_LOG(debug, "Failed getsockopt(IPPROTO_TCP, TCP_INFO): rc {} errno {} optlen {}",
84:               result.return_value_, result.errno_, optlen);
85:     return absl::nullopt;
86:   } else {
87:     return info;
88:   }
89: }

Mocks

EXPECT_CALL

EXPECT_CALL(`*`mock_object`*`,`*`method_name`*`(`*`matchers...`*`))

Creates an expectation that the method method_name of the object mock_object is called with arguments that match the given matchers matchers.... EXPECT_CALL must precede any code that exercises the mock object.

ON_CALL

ON_CALL(`*`mock_object`*`,`*`method_name`*`(`*`matchers...`*`))

Defines what happens when the method method_name of the object mock_object is called with arguments that match the given matchers matchers.... Requires a modifier clause to specify the method’s behavior. Does not set any expectations that the method will be called.

ON_CALLEXPECT_CALL 的区别:https://google.github.io/googletest/gmock_cheat_sheet.html#OnCall

To customize the default action for a particular method of a specific mock object, use ON_CALL. ON_CALL has a similar syntax to EXPECT_CALL, but it is used for setting default behaviors when you do not require that the mock method is called. See Knowing When to Expect for a more detailed discussion.

/test/mocks/network/io_handle.h

1
2
3
class MockIoHandle : public IoHandle {
49:   MOCK_METHOD(Api::SysCallIntResult, getOption,
50:               (int level, int optname, void* optval, socklen_t* optlen));

/test/mocks/network/mocks

1
2
3
4
class MockTransportSocketCallbacks : public TransportSocketCallbacks {
public:
  MOCK_METHOD(IoHandle&, ioHandle, ());
  MOCK_METHOD(const IoHandle&, ioHandle, (), (const));

Unit test

/test/extensions/transport_sockets/tcp_stats/tcp_stats_test.cc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class TcpStatsTest : public testing::Test {

32:   void initialize(bool enable_periodic) {
33:     envoy::extensions::transport_sockets::tcp_stats::v3::Config proto_config;
34:     if (enable_periodic) {
35:       proto_config.mutable_update_period()->MergeFrom(
36:           ProtobufUtil::TimeUtil::MillisecondsToDuration(1000));
37:     }
38:     config_ = std::make_shared<Config>(proto_config, *store_.rootScope());
  :     // mock transport_callbacks_.ioHandle() 返回 this->io_handle_
39:     ON_CALL(transport_callbacks_, ioHandle()).WillByDefault(ReturnRef(io_handle_));
  :     // mock io_handle_.getOption() 返回 this->tcp_info_
40:     ON_CALL(io_handle_, getOption(IPPROTO_TCP, TCP_INFO, _, _))
41:         .WillByDefault(Invoke([this](int, int, void* optval, socklen_t* optlen) {
42:           ASSERT(*optlen == sizeof(tcp_info_));
43:           memcpy(optval, &tcp_info_, sizeof(tcp_info_));
44:           return Api::SysCallIntResult{0, 0};
45:         }));
46:     createTcpStatsSocket(enable_periodic, timer_, inner_socket_, tcp_stats_socket_);
47:   }

  void createTcpStatsSocket(bool enable_periodic, NiceMock<Event::MockTimer>*& timer,
                            NiceMock<Network::MockTransportSocket>*& inner_socket_out,
                            std::unique_ptr<TcpStatsSocket>& tcp_stats_socket) {
    auto inner_socket = std::make_unique<NiceMock<Network::MockTransportSocket>>();
    inner_socket_out = inner_socket.get();
    tcp_stats_socket = std::make_unique<TcpStatsSocket>(config_, std::move(inner_socket));
    tcp_stats_socket->setTransportSocketCallbacks(transport_callbacks_);
  }    
    
    ...
  NiceMock<Network::MockIoHandle> io_handle_;// mock
  NiceMock<Network::MockTransportSocketCallbacks> transport_callbacks_;// mock
  struct tcp_info tcp_info_;

  std::unique_ptr<TcpStatsSocket> tcp_stats_socket_;
}



096: // Validate that the configured update_period is honored, and that stats are updated when the timer
097: // fires.
098: TEST_F(TcpStatsTest, Periodic) {
099:   initialize(true);
100: 
101:   EXPECT_CALL(*timer_, enableTimer(std::chrono::milliseconds(1000), _));
102:   tcp_info_.tcpi_notsent_bytes = 42;
103:   timer_->callback_();
104:   EXPECT_EQ(42, gaugeValue("cx_tx_unsent_bytes"));
105: 
106:   EXPECT_CALL(*timer_, disableTimer());
107:   tcp_stats_socket_->closeSocket(Network::ConnectionEvent::RemoteClose);
108: }

我在上面的 tcp_stats_test.cc#L42 中设置了断点:

1
42:           ASSERT(*optlen == sizeof(tcp_info_));

以下是调用 Stack,可以用 行号 对应到 tcp_stats_test.cc 等上面代码片段的位置。

Envoy::Extensions::TransportSockets::TcpStats::TcpStatsTest::initialize(bool)::'lambda'(int, int, void*, unsigned int*)::operator()(int, int, void*, unsigned int*) const (/workspaces/envoy/test/extensions/transport_sockets/tcp_stats/tcp_stats_test.cc:42)
...
...
testing::Action<Envoy::Api::SysCallResult<int> (int, int, void*, unsigned int*)>::Perform(tuple<int, int, void*, unsigned int*>) const (@testing::Action<Envoy::Api::SysCallResult<int> (int, int, void*, unsigned int*)>::Perform(tuple<int, int, void*, unsigned int*>) const:23)
testing::internal::FunctionMocker<Envoy::Api::SysCallResult<int> (int, int, void*, unsigned int*)>::PerformDefaultAction(tuple<int, int, void*, unsigned int*>&&, basic_string<char, char_traits<char>, allocator<char>> const&) const (@testing::internal::FunctionMocker<Envoy::Api::SysCallResult<int> (int, int, void*, unsigned int*)>::PerformDefaultAction(tuple<int, int, void*, unsigned int*>&&, basic_string<char, char_traits<char>, allocator<char>> const&) const:35)
...
testing::internal::FunctionMocker<Envoy::Api::SysCallResult<int> (int, int, void*, unsigned int*)>::UntypedPerformDefaultAction(void*, basic_string<char, char_traits<char>, allocator<char>> const&) const (@testing::internal::FunctionMocker<Envoy::Api::SysCallResult<int> (int, int, void*, unsigned int*)>::UntypedPerformDefaultAction(void*, basic_string<char, char_traits<char>, allocator<char>> const&) const:19)
testing::internal::UntypedFunctionMockerBase::UntypedInvokeWith(void*) (@testing::internal::UntypedFunctionMockerBase::UntypedInvokeWith(void*):60)
testing::internal::FunctionMocker<Envoy::Api::SysCallResult<int> (int, int, void*, unsigned int*)>::Invoke(int, int, void*, unsigned int*) (@testing::internal::FunctionMocker<Envoy::Api::SysCallResult<int> (int, int, void*, unsigned int*)>::Invoke(int, int, void*, unsigned int*):34)
Envoy::Network::MockIoHandle::getOption(int, int, void*, unsigned int*) (/workspaces/envoy/test/mocks/network/io_handle.h:49)
Envoy::Extensions::TransportSockets::TcpStats::TcpStatsSocket::querySocketInfo() (/workspaces/envoy/source/extensions/transport_sockets/tcp_stats/tcp_stats.cc:81)
Envoy::Extensions::TransportSockets::TcpStats::TcpStatsSocket::recordStats() (/workspaces/envoy/source/extensions/transport_sockets/tcp_stats/tcp_stats.cc:92)
Envoy::Extensions::TransportSockets::TcpStats::TcpStatsSocket::onConnected()::$_2::operator()() const (/workspaces/envoy/source/extensions/transport_sockets/tcp_stats/tcp_stats.cc:49)
...
Envoy::Extensions::TransportSockets::TcpStats::TcpStatsTest_Periodic_Test::TestBody() (/workspaces/envoy/test/extensions/transport_sockets/tcp_stats/tcp_stats_test.cc:103)
void testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) (@void testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*):35)
void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*) (@void testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>(testing::Test*, void (testing::Test::*)(), char const*):29)
testing::Test::Run() (@testing::Test::Run():47)
testing::TestInfo::Run() (@testing::TestInfo::Run():53)
testing::TestSuite::Run() (@testing::TestSuite::Run():65)
testing::internal::UnitTestImpl::RunAllTests() (@testing::internal::UnitTestImpl::RunAllTests():223)
bool testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*) (@bool testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*):35)
bool testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*) (@bool testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>(testing::internal::UnitTestImpl*, bool (testing::internal::UnitTestImpl::*)(), char const*):29)
testing::UnitTest::Run() (@testing::UnitTest::Run():48)
RUN_ALL_TESTS() (@RUN_ALL_TESTS():8)
Envoy::TestRunner::runTests(int, char**) (/workspaces/envoy/test/test_runner.cc:171)
main (/workspaces/envoy/test/main.cc:34)
__libc_start_main (@__libc_start_main:64)
_start (@_start:15)

debug unit test 前的准备工作

以下假设已经读过:

1
bazel build --compilation_mode=dbg --fission=no //test/extensions/transport_sockets/tcp_stats:tcp_stats_test && bazel build --compilation_mode=dbg --fission=no //test/extensions/transport_sockets/tcp_stats:tcp_stats_test.dwp

.vscode/launch.json :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "lldb",
            "request": "launch",
            "name": "Debug tcp_stats_test",
            "program": "${workspaceFolder}/bazel-bin/test/extensions/transport_sockets/tcp_stats/tcp_stats_test",
            "args": [],
            "cwd": "${workspaceFolder}",
            "sourceMap": {
                "/proc/self/cwd": "${workspaceFolder}"
            }
        }        
    ]
}
分享

Mark Zhu
作者
Mark Zhu
An old developer