比较大型的项目中,通常会依照总体软件架构,将交付件切分到不同的交付链中,处在中间层的交付件,往往有着自己的上游和下游。
那么,如何确保自己可以放心的向下游交付呢?
UT的覆盖,基本聚焦点是组件对外提供的接口,级别较低,只能保障接口实现符合设计上的预期。
作为项目组一员,要保障功能上衔接上游后达到预期,就不是UT能够完全cover的了。
然后一个限制是,你不可能拿到下游的代码工程来测试自己的组件。
另外,本团队的tester必须有工具来实现接近真实环境(往往不包含上游组件)TA case。
这个时候,是不是可以设计一种工具,模拟下游对当前subsystem的接口调用?
业内应该也有不少类似的这样的实现,当前的这个实现(非常初级的版本,但是从抽象层次上,已经足够)简介如下.
tester期望:
有一种工具,比如我只要读配置脚本,脚本可能是xml/json形式,为简单记,往往json更能达成一致。工具可以读json脚本,从json文件中提取需要测试的API以及精心设计的不同参数,这样工具可以模拟下游,来从各个维度调用API,并得到预期中的结果。这样在后续的TA case设计中,tester主要关注的就是配置脚本和预期输出。
开发如何满足tester期望?
1 设计一个protobuf形式的结构,来描述一个比较至关重要的类的接口,往往特指你对外交付的组件(include + lib + .pc 三件套?)接口,不仅在入参上约定好格式,也约定结果输出的格式。
2 工具能够读配置脚本(json格式),然后将json格式默认转换为protobuf表达,完成参数获取工作。工具模拟下游代码,将参数填入,调用接口,获得输出。
3 工具将输出也以预设好的protobuf格式存储,待所有接口测试完成,以json形式固化到文件。tester可以解析文件来得到结果。
接上上面描述,自然的会出现下面的流程,可以方面的达成TA
1 tester准备精心设计好的配置脚本,存放在某个目录,比如 /tmp/your-test-multiple-directory/
2 tester将工具安装在环境中, 工具名称 your_own_tool
3 tester 可以运行命令
your_own_tool --input=/tmp/your-test-multiple-directory/ --output_summary=/tmp/your-output-summary.txt"
4 tester 解析/tmp/your-output-summary.txt来获取结果。
乱七八糟的说了一通,对于很多有经验的看官来说,其实司空见惯。
笔者接手这个工具的任务时,一开始其实有些懵的,背景知识少,也几乎没有可参考对象,从零开始。
个中经历无需赘言,码农的基操,因为水平有限,其实最终版本还经历了后续不少高手的优化,这里提供原始版本,不涉嫌泄密。如有雷同,请联系我删除这篇文章。
整个工具思路的一个重要的实现核心,是围绕着 protobuf 和json之间互转完成的, protobuf实在是比较好用,在未来的工具相关开发中,还会大量的用到。
本样例是无情阉割版本,大致体现一个意思
1 protobuf 设计
syntax = "proto3"; import "google/protobuf/timestamp.proto"; package your_own_toolproto; message Result { uint32 errorcode = 1; uint64 consumed = 4; } message Context { repeated string name = 1; } message Do_Something{ Result result = 1; string params = 2; } message setEventHandler { Result result = 1; string eventfile = 2; } message ProxyA { repeated Do_Something somethingparams = 1; setEventHandler seteventhandler = 2; } message ProxyB { uint32 dummy = 1; } message ProxyC { uint32 dummy = 1; } message Proxy { Context context = 1; ProxyA proxya = 2; ProxyA proxyb = 3; ProxyA proxyc = 4; }
复制
2 命令行以及解析
your_own_tool_util.h
#include <string> namespace your_own_tool { struct CmdParams { std::string inputfilepath; unsigned int repeatedtimes = 1; std::string output_summary_file; std::string output_details_file; }; } #endif //your_own_tool_UTIL_H
复制
debug_option.h
#ifndef DEBUGTOOL_DEBUGOPTIONS_H #define DEBUGTOOL_DEBUGOPTIONS_H #include <fstream> #include <vector> #include <boost/program_options.hpp> #include <boost/program_options/config.hpp> #include "cmdrunner.h" namespace your_own_tool { class DebugOptions { public: DebugOptions( int argc, char** argv, CmdParams& cmdParams); ~DebugOptions(); DebugOptions() = delete; DebugOptions(const DebugOptions&) = delete; DebugOptions& operator=(const DebugOptions&) = delete; bool IsParseSuccessfully() {return parseSuccess_;} private: bool IsValidCommand(); void ParseCommand(int ac, char **av); void CommandDescription(); void PrintUsage(); boost::program_options::variables_map vm_; boost::program_options::options_description cmd_line_options_; std::string inputfilepath; unsigned int repeatedtimes; std::string output_summary_file; std::string output_details_file; CmdParams &cmdParams; bool parseSuccess_ = true; }; } #endif //DEBUGTOOL_DEBUGOPTIONS_H
复制
debug_option.cpp
#include "debug_option.h" #include <syslog.h> #include <thread> #include <string> #include <iostream> using std::cout; using std::cin; using std::endl; using std::string; using namespace your_own_tool; namespace po = boost::program_options; DebugOptions::DebugOptions(int argc, char **argv, CmdParams& IncmdParams):cmdParams(IncmdParams) { ParseCommand(argc, argv); } DebugOptions::~DebugOptions() { } void DebugOptions::ParseCommand(int ac, char **av) { po::options_description cmd("mandatory operation"); cmd.add_options() ("help,h", "produce help message") ("input", po::value<std::string>(&cmdParams.inputfilepath), "configure, a json file or a directory with multiple json files") ("repeated-times", po::value<unsigned int>(&cmdParams.repeatedtimes), "repeated-times") ("output_summary", po::value<std::string>(&cmdParams.output_summary_file), \ "output summary reports to given file name") ("output_details", po::value<std::string>(&cmdParams.output_details_file), \ "output detailed reports to given name or paths") ; cmd_line_options_.add(cmd); po::store(po::command_line_parser(ac, av).options(cmd_line_options_).run(), vm_); po::notify(vm_); if (!IsValidCommand()) { parseSuccess_ = false; PrintUsage(); } } bool DebugOptions::IsValidCommand() { bool ret = true; if (!(vm_.count("input")|| vm_.count("help"))) { ret = false; } return ret; } void DebugOptions::PrintUsage() { std::cout << cmd_line_options_ << std::endl; std::cout << "For examples:"<< std::endl; std::cout << "your_own_tool --input=/tmp/your-test-multiple-directory/ --repeated-times=10 --output_summary=/tmp/your-output-summary.txt" << std::endl; std::cout << "your_own_tool input=/tmp/your-test-single-file.json --output_summary=/tmp/your-output-summary.txt" << std::endl; std::cout << "your_own_tool input=/tmp/your-test-single-file.json --output_details=/tmp/your-output-details.txt" << std::endl; }
复制
3 脚本的参数提取以及执行,结果输出
cmdrunner.h
#ifndef CMDRUNNER_H #define CMDRUNNER_H #include <fstream> #include <filesystem> #include "your_own_tool_util.h" #include "your_own_tool.pb.h" namespace fs = std::filesystem; namespace your_own_tool { class CmdRunner { public: CmdRunner( CmdParams &cmdParams ); ~CmdRunner(); CmdRunner() = delete; CmdRunner(const CmdRunner&) = delete; CmdRunner& operator=(const CmdRunner&) = delete; bool runCmd(); private: int runCmdwithDir(const fs::path &filepath); int runCmdwithFile(const fs::path &filepath); int parseJsonAndRun(const fs::path &filepath); void getoutput_summary_file(std::string &output_summary_file); void getoutput_detail_file(const fs::path &filepath, std::string &output_details_file); std::string readJsonFile(const fs::path &filepath); void writeJsonfile(your_own_toolproto::Proxy &proxy_outputdetail); private: CmdParams &cmdParams; std::string _output_summary_file; std::string _output_details_file; }; } #endif //CMDRUNNER_H
复制
cmdrunner.cpp
#include "cmdrunner.h" #include <syslog.h> #include <thread> #include <iostream> #include <chrono> #include <vector> #include <string> #include <stdlib.h> #include <google/protobuf/text_format.h> #include <google/protobuf/util/json_util.h> using std::cout; using std::cin; using std::endl; using std::string; using std::filesystem::directory_iterator; namespace fs = std::filesystem; using namespace your_own_tool; CmdRunner::CmdRunner( CmdParams &IncmdParams ):cmdParams(IncmdParams) { } CmdRunner::~CmdRunner() { } void CmdRunner::getoutput_summary_file(std::string &output_summary_file) { if(cmdParams.output_summary_file.empty()) { char summary_file_template[] = "/tmp/your_output_summary_XXXXXX.log"; mktemp(summary_file_template); output_summary_file = string(summary_file_template); } else { output_summary_file = cmdParams.output_summary_file; } } void CmdRunner::getoutput_detail_file(const fs::path &filepath, std::string &output_details_file) { if(cmdParams.output_details_file.empty()) { char details_file_template[] = "_XXXXXX.log"; mktemp(details_file_template); output_details_file = string("/tmp/your_output_details/") + filepath.stem().string() + string(details_file_template); } else { output_details_file = cmdParams.output_details_file; } } int CmdRunner::runCmdwithDir(const fs::path &filepath) { for (const auto & file : fs::directory_iterator(filepath)) { if(fs::is_regular_file(file.path())) { auto ret = runCmdwithFile(file.path()); if(ret != 0) { std::cout<< file.path().string()<< "load failed!"<< endl; } } } return 0; } int CmdRunner::runCmdwithFile(const fs::path &filepath) { getoutput_summary_file(_output_summary_file); getoutput_detail_file(filepath, _output_details_file); std::cout << "output_summary_file:"<<_output_summary_file << std::endl; std::cout << "output_details_file:"<<_output_details_file << std::endl; parseJsonAndRun(filepath); return 0; } std::string CmdRunner::readJsonFile(const fs::path &filepath) { std::ifstream ifStr(filepath.string()); if(!ifStr.is_open()) { throw std::runtime_error(std::string("Open JSON file failed: ") + filepath.string() + strerror(errno)); } std::ostringstream sin; sin << ifStr.rdbuf(); ifStr.close(); return sin.str(); } void CmdRunner::writeJsonfile(your_own_toolproto::Proxy &proxy_outputdetail) { std::string newstr; google::protobuf::TextFormat::PrintToString(proxy_outputdetail, &newstr); std::ofstream fout(_output_details_file, std::ios::out); if ( ! fout) { throw std::runtime_error(std::string("Open JSON file failed: ") + _output_details_file + strerror(errno)); } fout << newstr<< endl; fout.close(); } int CmdRunner::parseJsonAndRun(const fs::path &filepath) { your_own_toolproto::Proxy proxy_params; auto status = google::protobuf::util::JsonStringToMessage(readJsonFile(filepath), &proxy_params); if(!status.ok()) { throw std::runtime_error(std::string("Invalid JSON format: ") + filepath.string() + status.ToString()); } if(!proxy_params.has_context()) { return -1; } // only the first one. auto _context = std::make_shared<Context>(); your_own_toolproto::Proxy proxy_outputdetail; proxy_outputdetail.mutable_context()->CopyFrom(proxy_params.context()); if(proxy_params.has_proxya()) { proxy_outputdetail.mutable_proxya()->CopyFrom(proxy_params.proxya()); auto proxya = std::make_shared<ProxyA>(*_context); //duration = end time - start time auto start = std::chrono::system_clock::now(); auto ret = proxya->dosomething(...); auto end = std::chrono::system_clock::now(); auto tmconsume = (end - start).count(); std::cout << "time: " << tmconsume << "ns" << std::endl; proxy_outputdetail.mutable_proxya()->mutable_somethingparams(i)->mutable_result()->set_errorcode(ret.value()); proxy_outputdetail.mutable_proxya()->mutable_somethingparams(i)->mutable_result()->set_consumed(tmconsume/1000); } if(proxy_params.has_proxyb()) { } return 0; } bool CmdRunner::runCmd() { for(unsigned int i = 0; i < cmdParams.repeatedtimes; i++) { fs::path inputPath(cmdParams.inputfilepath); if (fs::is_directory(inputPath)) { runCmdwithDir(inputPath); } else if(fs::is_regular_file(inputPath)) { runCmdwithFile(inputPath); } } return true; }
复制
4 总入口
main.cpp
#include <string> #include <iostream> #include "debug_option.h" #include "cmdrunner.h" int main(int argc, char **argv) { your_own_tool::CmdParams cmdParams; try { your_own_tool::DebugOptions options(argc, argv, cmdParams); if(options.IsParseSuccessfully()) { your_own_tool::CmdRunner cmdRunner(cmdParams); cmdRunner.runCmd(); } } catch (const std::exception &e) { std::cerr << "error: " << e.what() << "\n"; return 1; }; return 0; }
复制
5 Makefile样本
BASE_CPPFLAGS = -I$(top_srcdir)/your_own_tool \ -I$(top_srcdir)/your_own_tool/protobuf BASE_CXXFLAGS = $(PTHREAD_CFLAGS) \ $(BASE_CPPFLAGS) AM_CXXFLAGS = $(WARN_CXXFLAGS) \ -Wno-switch-enum \ -Werror -Wextra -Wno-switch-default -Wno-missing-declarations \ -std=c++17 protobuf_dir = protobuf protobuf_srcs = \ $(protobuf_dir)/your_own_tool.pb.cc MOSTLYCLEANFILES = $(protobuf_dir) BUILT_SOURCES = \ $(protobuf_dir)/your_own_tool.pb.h %.pb.cc %.pb.h: %.proto $(PROTOC) --proto_path=$(srcdir)/$(protobuf_dir) --cpp_out=$(builddir)/$(protobuf_dir) $^ BASE_LIBS = $(PTHREAD_LIBS) $(PROTOBUF_LIBS) \ -lboost_system -lboost_program_options # binary bin_PROGRAMS = your_own_tool your_own_tool_SOURCES = \ src/main.cpp \ src/debug_option.cpp \ src/cmdrunner.cpp \ $(protobuf_dir)/your_own_tool.pb.cc your_own_tool_CPPFLAGS = $(BASE_CPPFLAGS) your_own_tool_LDFLAGS = $(PTHREAD_LDFLAGS) your_own_tool_LDADD = ${BASE_LIBS}
复制
6 工程目录