比较大型的项目中,通常会依照总体软件架构,将交付件切分到不同的交付链中,处在中间层的交付件,往往有着自己的上游和下游。
那么,如何确保自己可以放心的向下游交付呢?
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 工程目录