TDD實例
github地址
項目中對于 TDD
的實戰,依賴的是 GoogleTest
框架
我負責編碼單元對中控提供
- 設置編碼單元
- 設置視頻源
- 設置視頻輸出
- 狀態檢測
- 開啟通道
- 關閉通道
這 6 個接口,中控通過 http
調用編碼單元接口,為了解耦和方便進行 TDD
測試,我們將這幾個方法寫成一個抽象類,編碼單元再實現具體的方法:
class IEventHandler {public:virtual bool StartChannel(int channel_id) = 0;virtual bool StopChannel(int channel_id) = 0;virtual bool ConfigChannel(int channel_id,const tucodec::SChannelSourceConfig &source_config) = 0;virtual bool ConfigChannel(int channel_id,const tucodec::SChannelDestConfig &dest_config) = 0;virtual bool ConfigChannel(int channel_id,const tucodec::SChannelEncoderConfig &encoder_config) = 0;virtual void DetectWorker(int channel_id, int duration,tucodec::SWorkerStatus &worker_status) = 0;};
這里 ConfigChannel
方法處理不同的參數結構體。
在測試用例中首先繼承 testing::Test
和 public IEventHandler
class HttpControllerTest : public testing::Test, public IEventHandler {public:bool StartChannel(int channel_id) override {channel_id_ = channel_id; return true;}bool StopChannel(int channel_id) override {channel_id_ = channel_id; return true;}bool ConfigChannel(int channel_id,const tucodec::SChannelSourceConfig &source_config) override {channel_id_ = channel_id;// 其他參數省略source_config_.username = source_config.username;return true;}bool ConfigChannel(int channel_id,const tucodec::SChannelDestConfig &dest_config) override {channel_id_ = channel_id;// 其他參數省略dest_config_.port = dest_config.port;return true;}bool ConfigChannel(int channel_id,const tucodec::SChannelEncoderConfig &encoder_config) override {channel_id_ = channel_id;// 其他參數省略encoder_config_.output_h = encoder_config.output_h; return true;}void DetectWorker(int channel_id, int duration,tucodec::SWorkerStatus &worker_status) override {channel_id_ = channel_id;}; protected:void SetUp() override { tucodec::TuLog::Init("", "http_controller_test.log", 50, 2 * 1024 * 1024);tucodec::TuLog::Instance()->SetPriority(tucodec::kTulogDebug); controller_.Init();}void TearDown() override { tucodec::TuLog::Destory();}protected : HttpController controller_{this};tucodec::SChannelSourceConfig source_config_;tucodec::SChannelDestConfig dest_config_;tucodec::SChannelEncoderConfig encoder_config_;int channel_id_{};
};TEST_F(HttpControllerTest, GetStateTest) {unsigned char get_json[] = "{\n"" \"cmd\":\"get_state\",\n"" \"chn_id\":1,\n"" \"duration\":2\n""}";controller_.ResolveData(get_json, sizeof(get_json));ASSERT_EQ(channel_id_, 1);
}TEST_F(HttpControllerTest, StartChannelTest) {unsigned char start_json[] = "{\n"" \"cmd\":\"start_chn\",\n"" \"chn_id\":1\n""}";controller_.ResolveData(start_json, sizeof(start_json));ASSERT_EQ(channel_id_, 1);
}TEST_F(HttpControllerTest, StopChannelTest) {unsigned char stop_json[] = "{\n"" \"cmd\":\"stop_chn\",\n"" \"chn_id\":2\n""}";controller_.ResolveData(stop_json, sizeof(stop_json));ASSERT_EQ(channel_id_, 2);
}TEST_F(HttpControllerTest, DestConfigTest) {unsigned char destination_json[] = "{\n"" \"cmd\":\"set_destination\",\n"" \"chn_id\":1,\n"" \"address\":\"192.168.1.221\",\n"" \"port\":554,\n"" \"destination_type\":\"onvif\",\n"" \"stream_id\":\"live1\"\n""}";controller_.ResolveData(destination_json, sizeof(destination_json));ASSERT_EQ(channel_id_, 1);ASSERT_EQ(dest_config_.address, "192.168.1.221");ASSERT_EQ(dest_config_.port, 554);ASSERT_EQ(dest_config_.destination_type, "onvif");ASSERT_EQ(dest_config_.stream_id, "live1");
}
實現具體接口,這里只需要對他進行賦值即可,將本身傳入 HttpController
中,這里我們先不測試網絡,直接調用接收到 http
請求后解析 json
的函數。再根據參數中命令選擇調用 設置編碼源還是具體哪一個接口。因為需要在回調函數中解析 json
,這里就直接繼承 DataCallBack
類,在接收方法中直接調用 ResolveData
。
class HttpController : public tucodec::DataCallBack {public:explicit HttpController(IEventHandler* event_handler);virtual ~HttpController();bool Init(const std::string &server_ip = "0.0.0.0", int server_port = 8081);bool ResolveData(unsigned char* data, unsigned int len);void ReceiveDataCallBack(int local_busy_handle,int remote_handle,unsigned char* data,unsigned int data_length,void* user) override {};void ReceiveDataCallBack(int local_busy_handle,const std::string &remote_ip,int remote_port,unsigned char* data,unsigned int data_length,void* user) override {};void ReceiveDataCallBack(int local_busy_handle,const std::string& remote_ip,int remote_port,const std::string& path,unsigned char* data,unsigned int data_length,void* user) override {tucodec::TuLog::Instance()->Info("receive data: %s length: %d ", data, data_length); //NOLINTtucodec::Server* server = reinterpret_cast<tucodec::Server*>(user);if (ResolveData(data, data_length)) {unsigned char response[] = "{\"code\":0}";int len = sizeof(response);server->SendResponse2Client(0, response, len);} else {unsigned char response[] = "{\"code\":-1}";int len = sizeof(response);server->SendResponse2Client(0, response, len);}};void StatusCallBack(int local_busy_handle,int remote_handle,std::string remote_ip,int remote_port,int remote_status,void* user) override {};private:IEventHandler* event_handler_;tucodec::Server* server_;
};
const char *k_set_codec_source = "set_codec_source";
const char *k_set_destination = "set_destination";
const char *k_start_chn = "start_chn";
const char *k_stop_chn = "stop_chn";
const char *k_get_state = "get_state";
const char *k_service_state = "service_state";HttpController::HttpController(IEventHandler *event_handler) {event_handler_ = event_handler;server_ = nullptr;
}HttpController::~HttpController() {if (server_ != nullptr) {delete server_;server_ = nullptr;}
}bool HttpController::Init(const std::string &server_ip, int server_port) {return true;
}bool HttpController::ResolveData(unsigned char *data, unsigned int len) {tucodec::TuJson json;std::stringstream x_read_write;x_read_write.write((const char *) data, len);json.Load(x_read_write);std::string cmd = json.GetString("cmd");int channel_id = json.GetInt("chn_id");if (cmd == k_set_codec_source) {SourceCodecHandler source_codec_handler;return source_codec_handler.HandleJson(json, channel_id, event_handler_);}if (cmd == k_set_destination) {DestinationHandler destination_handler;return destination_handler.HandleJson(json, channel_id, event_handler_);}if (cmd == k_get_state) {GetStatusHandler get_status_handler;return get_status_handler.HandleJson(json, channel_id, event_handler_);}if (cmd == k_start_chn) {return event_handler_->StartChannel(channel_id);}if (cmd == k_stop_chn) {return event_handler_->StopChannel(channel_id);}return false;
}
這邊還添加了一個類專門用來處理不同 json
,方便以后擴展
class IHttpHandler {public:virtual bool HandleJson(tucodec::TuJson &json, int channel_id, //NOLINTIEventHandler* event_handler_) = 0; //NOLINT
};class DestinationHandler : public IHttpHandler {public:bool HandleJson(tucodec::TuJson &json, int channel_id,IEventHandler* event_handler_) override;
};class SourceCodecHandler : public IHttpHandler {public:bool HandleJson(tucodec::TuJson &json, int channel_id,IEventHandler* event_handler_) override;
};class GetStatusHandler : public IHttpHandler {public:bool HandleJson(tucodec::TuJson &json, int channel_id,IEventHandler* event_handler_) override;
};
bool DestinationHandler::HandleJson(tucodec::TuJson &json,int channel_id, IEventHandler *event_handler_) { //NOLINTif (event_handler_ == nullptr) {return false;}tucodec::ChannelDestConfig channel_dest_config;channel_dest_config.port = json.GetInt("port");channel_dest_config.address = json.GetString("address");channel_dest_config.stream_id = json.GetString("stream_id");channel_dest_config.destination_type = json.GetString("destination_type");return event_handler_->ConfigChannel(channel_id, channel_dest_config);
}
bool SourceCodecHandler::HandleJson(tucodec::TuJson &json,int channel_id, IEventHandler *event_handler_) { //NOLINTif (event_handler_ == nullptr) {return false;}// codectucodec::ChannelEncoderConfig channel_encoder_config;// 省略部分參數channel_encoder_config.output_w = json.GetInt("output_w");if (!event_handler_->ConfigChannel(channel_id, channel_encoder_config)) {tucodec::TuLog::Instance()->Error("EncoderConfig Error!");return false;}// sourcetucodec::ChannelSourceConfig channel_source_config;channel_source_config.source_type = json.GetString("source_type");channel_source_config.address = json.GetString("address");channel_source_config.port = json.GetInt("port");channel_source_config.username = json.GetString("username");channel_source_config.password = json.GetString("password");return event_handler_->ConfigChannel(channel_id, channel_source_config);;
}bool GetStatusHandler::HandleJson(tucodec::TuJson &json, int channel_id, IEventHandler *event_handler_) {if (event_handler_ == nullptr) {return false;}tucodec::SWorkerStatus status;int duration = json.GetInt("duration");event_handler_->DetectWorker(channel_id, duration, status);return status.encoder_working == 1 && status.source_working == 1;
}
解析接口測試完畢后,接下來加入網絡模塊。
在 HttpController
類中先添加成員變量 tucodec::Server *server_;
,修改構造和析構
HttpController::HttpController(IEventHandler *event_handler) {event_handler_ = event_handler;server_ = nullptr;
}HttpController::~HttpController() {if (server_ != nullptr) {delete server_;server_ = nullptr;}
}
Init
方法中添加:
if (server_ == nullptr) {server_ = tucodec::NetWorkFactory::CreateHttpServer(server_ip,server_port);server_->SetDataCallBack(this,reinterpret_cast<void *>(server_));if (!server_->StartServer()) {tucodec::TuLog::Instance()->Error("start http server failed!");return false;}
}
tucodec::TuLog::Instance()->Info("start http server success!");
測試類中加入客戶端初始化以及阻塞函數:
void Wait(int time) {while (watcher_ == 0 && time > 0) {std::this_thread::sleep_for(std::chrono::seconds(1));--time;}
}protected:
void SetUp() override {watcher_ = 0;tucodec::TuLog::Init("", "http_controller_test.log", 50, 2 * 1024 * 1024);tucodec::TuLog::Instance()->SetPriority(tucodec::kTulogDebug);controller_.Init();client_ = tucodec::NetWorkFactory::CreateHttpClient("apt/v1/test",tucodec::kRequestPost);client_->ConnectServer("127.0.0.1", 8081);client_->SetDataCallBack(nullptr, nullptr);
}void TearDown() override {delete client_;client_ = nullptr;tucodec::TuLog::Destory();
}
protected :
tucodec::Client *client_ = nullptr;
HttpController controller_{this};
int watcher_{};
添加測試用例:
TEST_F(HttpControllerTest, SourceCodeConfigTest) {unsigned char source_codec_json[] = "{\n"" \"cmd\":\"set_codec_source\",\n"" \"chn_id\":1,\n"" \"source_type\":\"onvif\"\n""}";client_->SendRequest2Server(source_codec_json, sizeof(source_codec_json));Wait(2);// 省略參數ASSERT_EQ(channel_id_, 1);ASSERT_EQ(source_config_.source_type, "onvif");
}
盡量解耦和,讓業務依賴于接口,而非接口依賴于業務,每一個小模塊都可以單獨測試,在保證程序按預期穩定運行的基礎上(有測試用例的存在),再將我們的代碼進行重構,小步前進,出現問題也能快速定位,或者 revert
。
我們這里用的是 TestCase
級別的事件機制
gtest
提供了多種事件機制,非常方便我們在案例之前或之后做一些操作。總結一下 gtest
的事件一共有 3 種:
- 全局的,所有案例執行前后。
TestSuite
級別的,在某一批案例中第一個案例前,最后一個案例執行后。TestCase
級別的,每個TestCase
前后。
全局事件
要實現全局事件,必須寫一個類,繼承 testing::Environment
類,實現里面的 SetUp
和 TearDown
方法。
SetUp()
方法在所有案例執行前執行TearDown()
方法在所有案例執行后執行
class FooEnvironment : public testing::Environment
{
public:virtual void SetUp(){std::cout << "Foo FooEnvironment SetUP" << std::endl;}virtual void TearDown(){std::cout << "Foo FooEnvironment TearDown" << std::endl;}
};
當然,這樣還不夠,我們還需要告訴 gtest
添加這個全局事件,我們需要在 main
函數中通過 testing::AddGlobalTestEnvironment
方法將事件掛進來,也就是說,我們可以寫很多個這樣的類,然后將他們的事件都掛上去。
int main(int argc, char* argv[])
{testing::AddGlobalTestEnvironment(new FooEnvironment);testing::InitGoogleTest(&argc, argv);return RUN_ALL_TESTS();
}
TestSuite事件
我們需要寫一個類,繼承 testing::Test
,然后實現兩個靜態方法
SetUpTestCase()
方法在第一個TestCase
之前執行TearDownTestCase()
方法在最后一個TestCase
之后執行
class FooTest : public testing::Test {protected:static void SetUpTestCase() {shared_resource_ = new ;}static void TearDownTestCase() {delete shared_resource_;shared_resource_ = NULL;}// Some expensive resource shared by all tests.static T* shared_resource_;
};
在編寫測試案例時,我們需要使用 TEST_F
這個宏,第一個參數必須是我們上面類的名字,代表一個 TestSuite
。
TEST_F(FooTest, Test1){// you can refer to shared_resource here
}
TEST_F(FooTest, Test2){// you can refer to shared_resource here
}
TestCase事件
TestCase
事件是掛在每個案例執行前后的,實現方式和上面的幾乎一樣,不過需要實現的是 SetUp
方法和 TearDown
方法:
SetUp()
方法在每個TestCase
之前執行TearDown()
方法在每個TestCase
之后執行
class FooCalcTest:public testing::Test
{
protected:virtual void SetUp(){m_foo.Init();}virtual void TearDown(){m_foo.Finalize();}FooCalc m_foo;
};TEST_F(FooCalcTest, HandleNoneZeroInput)
{EXPECT_EQ(4, m_foo.Calc(12, 16));
}TEST_F(FooCalcTest, HandleNoneZeroInput_Error)
{EXPECT_EQ(5, m_foo.Calc(12, 16));
}
在 TEST_F
中使用的變量可以在初始化函數SetUp中初始化,在 TearDown
中銷毀,并且所有的 TEST_F
是互相獨立的,都是在初始化以后的狀態開始運行,一個 TEST_F
不會影響另一個 TEST_F
所使用的數據