项目——视频共享系统&&测试
目录
介绍
实现目标
服务器功能
服务器代码划分
环境搭建
认识第三方库
json库
数据库APT
httplib库
服务器
文件工具类
json工具类
视频数据表设计
视频数据类管理
通信接口设计
通信接口类设计
认识前端
HTML
基础标签
标题段落
图片
超链接
表格
顺序/无序
表单
选择
文本框
盒子
CSS
vue
v-cloak
v-bind
v-on
v-show
v-model
v-for
客户端
视频展示页面信息
视频展示页面新增视频按钮
播放页面视频播放
播放页面视频修改
播放页面视频删除
测试报告
项目背景
项目功能
项目计划
功能测试
自动化测试
性能测试
介绍
搭建视频共享点播服务器,可以让所有人通过浏览器访问服务器,实现视频的选择,上传,播放,删除(共享不适合删除,建议隐藏)
实现目标
主要是完成服务器端的程序业务功能的实现以及前端访问界面 html 的编写,能够支持客户端浏览器针对服务器上的所有视频进行操作
服务器功能
- 针对客户端上传的视频文件以及封面图片进行备份存储;
- 针对客户端上传的视频完成增删改查功能;
- 支持客户端浏览器进行视频的观看功能
服务器代码划分
- 数据管理模块:负责针对客户端上传的视频信息进行管理;
- 网络通信模块:搭建网络通信服务器,实现与客户端通信;
- 业务处理模块:针对客户端的各个请求进行对应业务处理并响应结果;
- 前端界面模块:完成前端浏览器上视频共享点播的各个 html 页面以及功能
环境搭建
默认在gcc/g++版本很低,两个编译器时没有如何依赖共享,都要升级到7.3版本
sudo yum install centos-release-scl-rh centos-release-scl sudo yum install devtoolset-7-gcc devtoolset-7-gcc-c++source /opt/rh/devtoolset-7/enable echo \"source /opt/rh/devtoolset-7/enable\" >> ~/.bashrc
安装 Jsoncpp 库
sudo yum install epel-releasesudo yum install jsoncpp-devel
下载 httplib 库:在搭建网络通信时就不用花大力气在这上面
git clone https://github.com/yhirose/cpp-httplib.git
Mysql 数据库及开发包安装
sudo yum install -y mariadbsudo yum install -y mariadb-serversudo yum install -y mariadb-devel
修改配置文件
sudo vim /etc/my.cnf.d/client.cnf
# /etc/my.cnf.d/client.cnf# These two groups are read by the client library# Use it for options that affect all clients, but not the server#[client]# 新增下边一行配置,设置客户端默认字符集为utf8default-character-set = utf8# This group is not read by mysql client library,# If you use the same .cnf file for MySQL and MariaDB,# use it for MariaDB-only client options[client-mariadb]
sudo vim /etc/my.cnf.d/mysql-clients.cnf
# /etc/my.cnf.d/mysql-clients.cnf# These groups are read by MariaDB command-line tools# Use it for options that affect only one utility#[mysql]# 新增配置default-character-set = utf8[mysql_upgrade][mysqladmin][mysqlbinlog][mysqlcheck][mysqldump][mysqlimport][mysqlshow][mysqlslap]
sudo vim /etc/my.cnf.d/server.cnf
# /etc/my.cnf.d/server.cnf# These groups are read by MariaDB server.# Use it for options that only the server (but not clients) should see## See the examples of server my.cnf files in /usr/share/mysql/## this is read by the standalone daemon and embedded servers[server]# this is only for the mysqld standalone daemon[mysqld]# 新增以下配置collation-server = utf8_general_ciinit-connect = \'SET NAMES utf8\'character-set-server = utf8sql-mode = TRADITIONAL# this is only for embedded server[embedded]# This group is only read by MariaDB-5.5 servers.# If you use the same .cnf file for MariaDB of different versions,# use this group for options that older servers don\'t understand[mysqld-5.5]# These two groups are only read by MariaDB servers, not by MySQL.# If you use the same .cnf file for MySQL and MariaDB,# you can put MariaDB-only options here[mariadb][mariadb-5.5]
启动mysql服务
systemctl start mariadb
设置开机自启动
systemctl enable mariadb
进入数据库
mysql -uroot
设置字符集支持中文格式
create database demo_db charset utf8mb4;
认识第三方库
json库
json用来实现网络传输时数据的序列化与反序列化
//Json数据对象类class Json::Value {Value& operator=(const Value& other); //Value重载了[]和=,因此所有的赋值和获取数据都可以通过Value& operator[](const std::string& key);//简单的方式完成 val[\"姓名\"] = \"小明\";Value& operator[](const char* key);Value removeMember(const char* key);//移除元素const Value& operator[](ArrayIndex index) const; //val[\"成绩\"][0]Value& append(const Value& value);//添加数组元素val[\"成绩\"].append(88);ArrayIndex size() const;//获取数组元素个数 val[\"成绩\"].size();std::string asString() const;//转string string name = val[\"name\"].asString();const char* asCString() const;//转char* char *name = val[\"name\"].asCString();Int asInt() const;//转int int age = val[\"age\"].asInt();float asFloat() const;//转floatbool asBool() const;//转 bool};class JSON_API StreamWriter {virtual int write(Value const& root, std::ostream* sout) = 0;}//使用该设计模式来构造StreamWriterclass JSON_API StreamWriterBuilder : public StreamWriter::Factory {virtual StreamWriter* newStreamWriter() const;}class JSON_API CharReader {virtual bool parse(char const* beginDoc, char const* endDoc,Value* root, std::string* errs) = 0;}class JSON_API CharReaderBuilder : public CharReader::Factory {virtual CharReader* newCharReader() const;}
测试代码
#include #include#include#include#includeint main(){ const std::string name=\"张三\"; int age=22; float score[3]={77.5,88,99.5}; Json::Value v; v[\"名字\"]=name; v[\"年龄\"]=age; v[\"成绩\"].append(score[0]); v[\"成绩\"].append(score[1]); v[\"成绩\"].append(score[2]); //序列化 Json::StreamWriterBuilder swr;//使用设计模式 std::unique_ptr sw(swr.newStreamWriter()); std::stringstream ss; int ret=sw->write(v,&ss);//ss是输出参数 if(ret==0) std::cout<<ss.str()<<std::endl; //反序列化 const std::string s=ss.str(); std::string err; Json::Value v1; Json::CharReaderBuilder crb; std::unique_ptr cr(crb.newCharReader()); bool tmp=cr->parse(s.c_str(),s.c_str()+s.size(),&v1,&err); if(tmp) { std::cout<<v1[\"名字\"].asString()<<\' \'<<v1[\"年龄\"].asInt()<<std::endl; std::cout<<v1[\"成绩\"][0]<<\' \'<<v1[\"成绩\"][1]<<\' \'<<v1[\"成绩\"][2]<<std::endl; } return 0;}
运行代码
数据库APT
这里主要介绍 Mysql 的 C 语言 API 接口;Mysql 是 C/S 模式,其实编写代码访问数据库就是实现了一个 Mysql 客户端,实现专有功能
//Mysql操作句柄初始化MYSQL *mysql_init(MYSQL *mysql);//参数为空则动态申请句柄空间进行初始化//失败返回NULL//连接mysql服务器MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user, const char *passwd,const char *db, unsigned int port, const char *unix_socket, unsigned long client_flag);//mysql--初始化完成的句柄//host---连接的mysql服务器的地址//user---连接的服务器的用户名//passwd-连接的服务器的密码//db ----默认选择的数据库名称//port---连接的服务器的端口: 默认0是3306端口//unix_socket---通信管道文件或者socket文件,通常置NULL//client_flag---客户端标志位,通常置0//返回值:成功返回句柄,失败返回NULL//设置当前客户端的字符集int mysql_set_character_set(MYSQL *mysql, const char *csname)//mysql--初始化完成的句柄//csname--字符集名称,通常:\"utf8\"//返回值:成功返回0, 失败返回非0//执行sql语句int mysql_query(MYSQL *mysql, const char *stmt_str)//mysql--连接成功后返回的句柄//stmt_str--要执行的sql语句//返回值:成功返回0, 失败返回非0;//保存查询结果到本地MYSQL_RES *mysql_store_result(MYSQL *mysql)//返回值:成功返回结果集的指针, 失败返回NULL;//获取结果集中的行数uint64_t mysql_num_rows(MYSQL_RES *result);//result--保存到本地的结果集地址//返回值:结果集中数据的列数;//获取结果集的列数unsigned int mysql_num_fields(MYSQL_RES *result)//result--保存到本地的结果集地址//返回值:结果集中每一条数据的列数//获取属性集MYSQL_FIELD * mysql_fetch_fields(MYSQL_RES *result)//遍历结果集MYSQL_ROW mysql_fetch_row(MYSQL_RES *result)//result--保存到本地的结果集地址//返回值:实际上是一个char **的指针,//将每一条数据做成了字符串指针数组 row[0]-第0列 row[1]-第1列//并且这个接口会保存当前读取结果位置,每次获取的都是下一条数据//释放结果集void mysql_free_result(MYSQL_RES *result)//result--保存到本地的结果集地址//关闭数据库客户端连接,销毁句柄:void mysql_close(MYSQL *mysql)//获取mysql接口执行错误原因(一般用不到)const char *mysql_error(MYSQL *mysql)
首先先准备一个库和创建一个普通用户,给普通用户库的所有权限
测试代码
#include #include const char*host=\"192.168.109.148\";const char*user=\"zzj\";const char*password=\"0628\";const char*db=\"test_conn\";unsigned int port=3306;int main(){ //创建句柄 MYSQL* my=mysql_init(nullptr); if(my==nullptr) return 1; //连接数据库 MYSQL* mys=mysql_real_connect(my,host,user,password,db,port,nullptr,0); if(mys==nullptr) return 2; //设置字符集使得操作支持中文 int ret=mysql_set_character_set(mys,\"utf8\"); if(ret!=0) return 3; //选择要操作的数据库 //ret=mysql_select_db(mys,db); //if(ret!=0) return 4; //mysql指令 const char* mysql=\"select *from people\"; ret=mysql_query(mys,mysql); if(ret!=0) return 5; //获取结果集 行数与列数 MYSQL_RES* res=mysql_store_result(mys); int rows=mysql_num_rows(res); int files=mysql_num_fields(res); //打印属性 MYSQL_FIELD* files_at=mysql_fetch_field(res); for(int i=0;i<files;i++) { std::cout<<files_at[i].name<<\" \"; } std::cout<<std::endl; //打印内容 for(int i=0;i<rows;i++) { MYSQL_ROW content=mysql_fetch_row(res); for(int j=0;j<files;j++) { std::cout<<content[j]<<\" \"; } std::cout<<std::endl; } //释放结果集 mysql_free_result(res); //关闭数据库连接 mysql_close(mys); return 0;}
把查询结果打印出来
httplib库
在httplib库中对应了两个对象:request 和 reponse
struct Request {std::string method;//存放请求方法std::string path;//存放请求资源路径Headers headers;//存放头部字段的键值对mapstd::string body;//存放请求正文// for serverstd::string version;//存放协议版本Params params;//存放url中查询字符串 key=val&key=val的 键值对mapMultipartFormDataMap files;//存放文件上传时,正文中的文件信息Ranges ranges;bool has_header(const char* key) const;//判断是否有某个头部字段std::string get_header_value(const char* key, size_t id = 0) const;//获取头部字段值void set_header(const char* key, const char* val);//设置头部字段bool has_file(const char* key) const;//文件上传中判断是否有某个文件的信息MultipartFormData get_file_value(const char* key) const;//获取指定的文件信息};struct Response {std::string version;//存放协议版本int status = -1;//存放响应状态码std::string reason;Headers headers;//存放响应头部字段键值对的mapstd::string body;//存放响应正文std::string location; // Redirect location重定向位置void set_header(const char* key, const char* val);//添加头部字段到headers中void set_content(const std::string& s, const char* content_type);//添加正文到body中void set_redirect(const std::string& url, int status = 302);//设置全套的重定向信息};
此外还有请求到来时的request对象经过出来后生成reponse对象的各种函数方法
class Server {using Handler = std::function;//函数指针类型using Handlers = std::vector<std::pair>;//存放请求-处理函数映射std::function new_task_queue;//线程池Server& Get(const std::string& pattern, Handler handler);//添加指定GET方法的处理映射Server& Post(const std::string& pattern, Handler handler);Server& Put(const std::string& pattern, Handler handler);Server& Patch(const std::string& pattern, Handler handler);Server& Delete(const std::string& pattern, Handler handler);Server& Options(const std::string& pattern, Handler handler);bool listen(const char* host, int port, int socket_flags = 0);//开始服务器监听bool set_mount_point(const std::string& mount_point, const std::string& dir,Headers headers = Headers());//设置http服务器静态资源根目录};
一个一个分析:
std::vector<std::pair>;//存放请求-处理函数映射
std::function new_task_queue;//线程池bool listen(const char *host, int port, int socket_flags = 0);
客户端发送请求后,tcp服务器通过监听listen接收到请求后,在线程池中找一个线程出来处理请求(这就是下面我们要处理的业务逻辑):先进行请求解析得到Request对象,根据请求方法与资源路径根据上面映射表来找处理函数:有就调用处理,并将空reponse对象传入;处理后就得到填充完毕的reponse对象,(httplib)根据它组成http响应后发送给客户端(不用我们关心),短连接就关闭套接字,长连接就等待超时后关闭,之后等待请求的到来...
bool set_mount_point(const std::string &mount_point, const std::string &dir,Headers headers = Headers());//设置http服务器静态资源根目录
可能请求的资源是静态资源,这时就不用我们自己处理,对请求路径进行拼接后再指定路径下找该资源,找到后自己返回即可
测试代码
//test_httplib.cc#include\"httplib.h\" #include using namespace httplib; int main() { Server sr; //设置静态资源 sr.set_mount_point(\"/\",\"./www\"); //设置方法 正则表达式匹配 ()->捕捉 \\d匹配数字 +匹配多个字符 sr.Get(R\"(/number/(\\d+))\",[](const Request& req,Response& res) { res.set_content(req.matches[1],\"text/html\");//mathces[0]=\"/number/12345\" }); sr.Post(\"/multipart\",[](const Request& req,Response& res) { auto file=req.get_file_value(\"file1\"); std::cout<<file.content<<std::endl; res.status=200; }); //监听 sr.listen(\"0.0.0.0\",8080); return 0; }
// www/index.html Hello Bit
服务器
文件工具类
编写一个文件工具类,实现:
- 文件是否存在;
- 文件大小;
- 文件内容读取;
- 文件内容写数据;
- 创建目录(文件不存在时(目录也是文件))
主要是来熟悉使用系统调用与C++读写文件iofstream的写法
#pragma once #include #include #include #include #include #include namespace util{ class FileUtil { public: FileUtil(const std::string&name): _FileName(name){} bool FileExit() { //access(F_OK)判断文件是否存在--存在返回0 int ret=access(_FileName.c_str(),F_OK); if(ret<0) { std::cout<<\"file is no exit\"<<std::endl; return false; } return true; } size_t FileSize() { //stat获取文件所有属性 struct stat buf; int ret=stat(_FileName.c_str(),&buf); if(ret<0) { std::cout<<\"stat fail\"<<std::endl; } return buf.st_size; } //读文件 bool GetContent(std::string& content) { std::ifstream ifs; ifs.open(_FileName.c_str(),std::ios::binary); if(!ifs.is_open()) { std::cout<<\"read open file fail\"<<std::endl; return false; } size_t size=FileSize(); content.resize(size); ifs.read(&(content[0]),size); ifs.close(); if(!ifs.good()) return false; return true; } //写文件 bool SetContent(const std::string& content) { std::ofstream ofs; ofs.open(_FileName.c_str(),std::ios::binary); if(!ofs.is_open()) { std::cout<<\"write open file fail\"<<std::endl; return false; } ofs.write(content.c_str(),content.size()); ofs.close(); if(!ofs.good()) return false; return true; } bool CreateDir() { if(FileExit()) { std::cout<<\"file exit\"<<std::endl; return false; } int ret=mkdir(_FileName.c_str(),0777); if(ret<0) { std::cout<<\"CreateDir fail\"<<std::endl; return false; } return true; } private: std::string _FileName; };}; //功能测试void test_util() { util::FileUtil(\"./www\").CreateDir(); std::string content; util::FileUtil fu(\"./www/index.html\"); fu.SetContent(\"\"); fu.GetContent(content); std::cout<<content.c_str()<<\" \"<<fu.FileSize()<<std::endl; }
json工具类
实现通信时使用json库进行序列化与反序列化(上面已经简单使用过到这里没难度~)
//util.hpp#include #include #include #include #include #include name util{ class FileUtil{}... class JsonUtil { public: static bool Serialize(Json::Value& root,std::string& content) { Json::StreamWriterBuilder swr; std::unique_ptr sw(swr.newStreamWriter()); std::stringstream ss; int ret=sw->write(root,&ss); if(ret<0) { std::cout<<\"Serialize Fail\"<<std::endl; return false; } content=ss.str(); return true; } static bool Unserialize(Json::Value& root,std::string& content) { Json::CharReaderBuilder crb; std::unique_ptr cr(crb.newCharReader()); std::string err; int ret=cr->parse(content.c_str(),content.c_str()+content.size(),&root,&err); if(ret<0) { std::cout<<\"Unserialize fail \"<<err<<std::endl; return false; } return true; } };};//功能测试void test_json() { Json::Value root; root[\"姓名\"]=\"小王\"; root[\"年龄\"]=18; root[\"成绩\"].append(77.5); root[\"成绩\"].append(88.5); root[\"成绩\"].append(99.5); std::string s; util::JsonUtil::Serialize(root,s); std::cout<<s<<std::endl; Json::Value root1; util::JsonUtil::Unserialize(root1,s); std::cout<<root1[\"姓名\"].asString()<<\" \"<<root1[\"年龄\"].asInt()<<std::endl; for(auto& a:root1[\"成绩\"]) std::cout<<a<<\" \"; std::cout<<std::endl; }
视频数据表设计
创建一个数据库aod_system,创建一张表用来保存视频的各种资源数据
视频数据类管理
写一个VideoData类:创建时自动进行mysql句柄初始化,连接数据库的操作;析构时自动关闭连接;其中我们实现各种接口,让外部简单调用实现对数据库的增删查改;
#pragma once#include #include \"util.hpp\"#include \"mysql/mysql.h\"namespace data{const char*host=\"192.168.109.148\";const char*user=\"zzj\";const char*password=\"0628\";const char*db=\"aod_system\";unsigned int port=3306; static MYSQL* Mysql_Init() { //创建句柄 MYSQL* my=mysql_init(nullptr); if(my==NULL) { std::cout<<\"mysql init fail\"<<std::endl; return nullptr; } //连接数据库 MYSQL* mys=mysql_real_connect(my,host,user,password,db,port,nullptr,0); if(mys==nullptr) { std::cout<<\"link mysql fail\"<<std::endl; return nullptr; } //设置字符集使得操作支持中文 int ret=mysql_set_character_set(mys,\"utf8\"); if(ret!=0) { std::cout<<\"set character fail\"<<std::endl; return nullptr; } return mys; } static void Mysql_Destory(MYSQL* my) { mysql_close(my); } static bool Mysql_Query(MYSQL* my,const std::string& mysql) { int ret=mysql_query(my,mysql.c_str()); if(ret!=0) { std::cout<<mysql.c_str()<<std::endl; std::cout<<mysql_error(my)<<std::endl; return false; } return true; } class VideoData { public: VideoData() { _mysql=Mysql_Init(); if(_mysql==nullptr) exit(-1); } ~VideoData() { Mysql_Destory(_mysql); } bool InsertMysql(const Json::Value& root); bool UpdateMysql(int video_id,const Json::Value& root); bool DeleteMysql(int video_id); bool SelectOne(int video_id,Json::Value& root); bool SelectAll(Json::Value& root); bool SelectLike(const std::string& s,Json::Value& root); private: MYSQL* _mysql; std::mutex _mutex; };};
增删改的操作需要让外部传id和json对象进来,我们好通过这些数据拼接sql语句;查的操作就需要传一个空的json对象,把select查询到的数据填充到json对象中以输出型参数的形式返回(注意拼接sql语句的各种符号问题,如果用string来拼接很容易出错,推荐使用宏与sprintf进行格式化输出)
bool InsertMysql(const Json::Value &root) { // id name info video image std::string sql = \"insert tb_video values (null,\'\" + root[\"name\"].asString() + \"\',\'\" + root[\"info\"].asString() + \"\',\'\" + root[\"video\"].asString() + \"\',\'\" + root[\"image\"].asString() + \"\')\"; // #define INSERT_MYSQL \"insert tb_video values(null,\'%s\',\'%s\',\'%s\',\'%s\');\" // char sql[4096+root[\"info\"].size()]; // sprintf(sql,INSERT_MYSQL,root[\"name\"].asString().c_str(),root[\"info\"].asString().c_str(),root[\"video\"].asString().c_str(),root[\"image\"].asString().c_str()); return Mysql_Query(_mysql, sql); } bool UpdateMysql(int video_id, const Json::Value &root) { std::string sql = \"update tb_video set name=\'\" + root[\"name\"].asString() + \"\',info=\'\" + root[\"info\"].asString() + \"\',video=\'\" + root[\"video\"].asString() + \"\',image=\'\" + root[\"image\"].asString() + \"\'where id=\" + std::to_string(video_id); return Mysql_Query(_mysql, sql); } bool DeleteMysql(int video_id) { std::string sql = \"delete from tb_video where id=\" + std::to_string(video_id); return Mysql_Query(_mysql, sql); }
查询结果与创建结果集的操作一定是原子的:查询之后的结果保存在结果集,立刻就要通过结果集函数进行获取,但因为最终实现时是多线程的方式有线程安全,所以要加锁
bool SelectAll(Json::Value& root) { std::string sql=\"select *from tb_video\"; //查询sql与结果集之间要是原子的 _mutex.lock(); bool ret=Mysql_Query(_mysql,sql); if(ret==false) { _mutex.unlock(); return false; } //保存select内容到root中 MYSQL_RES* res=mysql_store_result(_mysql); if(res==nullptr) { std::cout<<\"gain result fail\"<<std::endl; _mutex.unlock(); return false; } _mutex.unlock(); int row=mysql_num_rows(res); for(int i=0;i<row;i++) { MYSQL_ROW content=mysql_fetch_row(res); Json::Value tmp; tmp[\"id\"]=content[0]; tmp[\"name\"]=content[1]; tmp[\"info\"]=content[2]; tmp[\"video\"]=content[3]; tmp[\"image\"]=content[4]; root.append(tmp); } mysql_free_result(res); return true; } bool SelectOne(int video_id,Json::Value& root) { std::string sql=\"select *from tb_video where id=\"+std::to_string(video_id); //查询sql与结果集之间要是原子的 _mutex.lock(); bool ret=Mysql_Query(_mysql,sql); if(ret==false) { _mutex.unlock(); return false; } //保存select内容到root中 MYSQL_RES* res=mysql_store_result(_mysql); if(res==nullptr) { std::cout<<\"gain result fail\"<<std::endl; _mutex.unlock(); return false; } _mutex.unlock(); MYSQL_ROW content=mysql_fetch_row(res); root[\"id\"]=content[0]; root[\"name\"]=content[1]; root[\"info\"]=content[2]; root[\"video\"]=content[3]; root[\"image\"]=content[4]; mysql_free_result(res); return true; } bool SelectLike(const std::string& s,Json::Value& root) { std::string sql=\"select *from tb_video where name like \'%%\"+s+\"%%\'\"; //查询sql与结果集之间要是原子的 _mutex.lock(); bool ret=Mysql_Query(_mysql,sql); if(ret==false) { _mutex.unlock(); return false; } //保存select内容到root中 MYSQL_RES* res=mysql_store_result(_mysql); if(res==nullptr) { std::cout<<\"gain result fail\"<<std::endl; _mutex.unlock(); return false; } _mutex.unlock(); int row=mysql_num_rows(res); for(int i=0;i<row;i++) { MYSQL_ROW content=mysql_fetch_row(res); Json::Value tmp; tmp[\"id\"]=content[0]; tmp[\"name\"]=content[1]; tmp[\"info\"]=content[2]; tmp[\"video\"]=content[3]; tmp[\"image\"]=content[4]; root.append(tmp); } mysql_free_result(res); return true; }
通信接口设计
所谓的http协议,本质上是tcp协议传输时应用层所规定的一种数据格式,通信接口设计就是要知道什么样的请求对应数据库上增删查改的那个操作,这个过程我们不用自己实现,在restful中就定义好了:GET方法表示查询,POST方法表示新增,PUT方法表示修改,DELETE方法表示删除,正文数据格式通常采用的是json或者xml的数据格式;
通信接口类设计
主要实现的是:面对不同请求时设计出不同的接口来处理;而通信过程中的一系列搭建如创建套接字,监听,连接...都有httplib库提供的Server对象和对应接口来使用,不用我们自己来搭建,这是为了让我们有更多精力放在业务处理逻辑上来
#pragma once#include \"httplib.h\"#include \"data.hpp\"namespace aod{const std::string wwwroot=\"./www\";const std::string video_root=\"/video/\";const std::string image_root=\"/image/\";const std::string ip=\"0.0.0.0\";// httplib是基于多线程要设计为全局的data::VideoData* vd=nullptr; class server { private: unsigned int _port; httplib::Server _sr;//httplib库为我们搭建通信处理的桥梁 private: //各种不同的请求对应的处理 static void Insert(const httplib::Request& req,httplib::Response& res); static void Delete(const httplib::Request& req,httplib::Response& res); static void Select(const httplib::Request& req,httplib::Response& res); static void Update(const httplib::Request& req,httplib::Response& res); public: server(unsigned int port):_port(port){} //服务器整体设计 bool RunServer() };};
RunServer 函数,连接前的准备工作:
- 初始化视频数据类VideoDate;
- 创建出资源存放的目录;
- 建立httplib接口提供的路由表:不同的资源请求(不同的请求方法和资源路径)到来是传入我们自己写的方法来处理
bool RunServer() { //初始化视频资源类 vd=new data::VideoData(); //设置资源路径 util::FileUtil(wwwroot).CreateDir(); util::FileUtil(wwwroot+video_root).CreateDir(); util::FileUtil(wwwroot+image_root).CreateDir(); _sr.set_mount_point(\"/\",wwwroot); //访问资源对应的处理方法 _sr.Post(\"/video\",Insert); _sr.Delete(\"/video/(\\\\d+)\",Delete); _sr.Put(\"/video/(\\\\d+)\",Update); _sr.Get(\"/video/(\\\\d+)\",SelectOne); _sr.Get(\"/video/\",Select);// 这样路由 /video -> postman 404 Bug? //监听 _sr.listen(ip,_port); return true; }
Insert 函数,当资源请求:POST /video 时就调用它来处理:
- 通过request提供的接口获取客户端提交的视频信息,也就是name,info,video_url(视频信息),image_url(图片信息)
- 通过上面的信息拼接成视频绝对路径,图片绝对路径保存客户端提交的视频资源,图片资源;
- 数据库也要新增有关这个客户端提交的视频信息;
static void Insert(const httplib::Request& req,httplib::Response& res) { if(req.has_file(\"name\")==false|| req.has_file(\"info\")==false|| req.has_file(\"video\")==false|| req.has_file(\"image\")==false) { res.status=400; res.set_content(R\"({\"result\":false,\"content\":\"输入参数缺失\"})\",\"application/json\"); return; } //httplib::MultipartFormData中有{name,content_type,content,filename,} //将请求获取到的资源进行保存 auto name_source=req.get_file_value(\"name\"); auto info_source=req.get_file_value(\"info\"); auto video_source=req.get_file_value(\"video\"); auto image_source=req.get_file_value(\"image\"); //拼接资源所要保存的绝对路径 ./wwwroot/video/XXXX //拼接name避免命名重复 std::string video_path=wwwroot+video_root+name_source.content+video_source.filename; std::string image_path=wwwroot+image_root+name_source.content+image_source.filename; if(util::FileUtil(video_path).SetContent(video_source.content)==false) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"保存视频失败\"})\",\"application/json\"); return; } if(util::FileUtil(image_path).SetContent(image_source.content)==false) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"保存image失败\"})\",\"application/json\"); return; } //把相关信息保存到数据库中 Json::Value v; v[\"name\"]=name_source.content; v[\"info\"]=info_source.content; //拼接相对路径 std::string vo=video_root+name_source.content+video_source.filename; std::string ie=image_root+name_source.content+image_source.filename; v[\"video\"]=vo; v[\"image\"]=ie; if(vd->InsertMysql(v)==false) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"保存数据库失败\"})\",\"application/json\"); return; } }
Delete 函数,当资源请求:DELETE /video/number 时就调用它来处理:
- 通过req的mathes捕获正则表达式匹配的数字,也就是视频id;
- 通过视频id在数据库中查询它的相关信息;
- 通过查询到的信息拼接视频,图片资源所在的路径并进行删除;
- 使用视频id删除在数据库的相关数据
static void Delete(const httplib::Request& req,httplib::Response& res) { // /video/(\\\\d+)捕获的数字 matches[0]得到完整字符串 int id=std::stoi(req.matches[1]); // 查询数据 Json::Value v; vd->SelectOne(id,v); //删除保存的资源文件 std::string video_file=wwwroot+v[\"video\"].asString(); std::string image_file=wwwroot+v[\"image\"].asString(); if(remove(video_file.c_str())!=0) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"删除视频资源失败\"})\",\"application/json\"); return; } if(remove(image_file.c_str())!=0) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"删除图片资源失败\"})\",\"application/json\"); return; } //删除数据库信息 if(vd->DeleteMysql(id)==false) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"删除数据库数据失败\"})\",\"application/json\"); return; } }
Modify函数,当资源请求:PUT /video/number {修改后的数据} 时就调用它来处理:
- 获取视频id;
- 通过request的body函数获取修改后的数据字符串;
- 进行反序列化处理成Json格式;
- 通过视频id和Json格式在数据库中进行修改
static void Update(const httplib::Request& req,httplib::Response& res) { int id=std::stoi(req.matches[1]); std::string s=req.body; //反序列化才能修改 Json::Value v; if(util::JsonUtil::Unserialize(v,s)==false) { res.status=400; res.set_content(R\"({\"result\":false,\"content\":\"反序列化失败\"})\",\"application/json\"); return; } if(vd->UpdateMysql(id,v)==false) { std::cout<<-1<<std::endl; res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"更新数据库数据失败\"})\",\"application/json\"); return; } }
SelectOne函数,当资源请求:Get /video/number 时就调用它来处理:
- 获取视频id;
- 数据库中查询该id下的相关信息Json;
- Json反序列化后填写填写响应正文与格式
static void SelectOne(const httplib::Request& req,httplib::Response& res) { int id=std::stoi(req.matches[1]); Json::Value v; if(vd->SelectOne(id,v)==false) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"查询数据库数据失败\"})\",\"application/json\"); return; } //组织响应正文给客户端 std::string result; if(util::JsonUtil::Serialize(v,result)==false) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"序列化失败\"})\",\"application/json\"); return; } res.set_content(result,\"application/json\"); }
SelectALL函数,当资源请求:Get /video/ 时就调用它来处理:
- 先判断是否为模糊匹配,使用:request的has_param;
- 是模糊匹配的话要进行标记,还要获取模糊匹配关键字,使用模糊匹配查询接口
- 不是就使用查询接口
- Json反序列化后填写填写响应正文与格式
static void Select(const httplib::Request& req,httplib::Response& res) { bool tmp=false; std::string key; if(req.has_param(\"search\")) { tmp=true; key=req.get_param_value(\"search\");// /video?search=\"key\" } Json::Value v; //模糊匹配 if(tmp) { if(vd->SelectLike(key,v)==false) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"模糊查询数据库数据失败\"})\",\"application/json\"); return; } } else { if(vd->SelectAll(v)==false) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"更新数据库数据失败\"})\",\"application/json\"); return; } } std::string result; if(util::JsonUtil::Serialize(v,result)==false) { res.status=500; res.set_content(R\"({\"result\":false,\"content\":\"序列化失败\"})\",\"application/json\"); return; } res.set_content(result,\"application/json\"); }
测试使用postman进行各个请求接口测试,要严格按照设计的请求方法与url来进行测试
新增视频(同时检测服务器的指定路径与数据库是否新增客户端提交的视频信息)
获取所有视频信息
通过id获取视频信息
通过name的模糊匹配获取视频信息
通过id与修改后的视频信息(Json格式)更新指定视频的信息(查看数据库是否同步更新了)
通过id删除指定视频信息(查看指定路径与数据库是否没有了指定视频信息)
认识前端
HTML
HTML 代码是由标签构成的。我们可以理解不同的标签代表不同的控件元素,前端浏览器拿到 html 代码之后,根据标签之间的关系进行解析,得到一棵 DOM(Document Object Mode - 文档对象模型的缩写) 树 ,然后它根据 DOM 树渲染出不同的控件元素,得到我们所看到的页面
编写一个简单的HTML代码
第一个html页面 hello html
编写HTML代码首先写 :表面这是一个HTML代码;通常是一前一后双标签组成,少部分是单标签;head标签通常是页面的信息,如标题,字符集格式等;body标签通常是页面的内容,图片,表格,表单等,通常是head先加载;id表明给该标签一个特殊的标识符
基础标签
标题段落
标题标签:h1~h6,段落标签:p,还有单标签br:br使用把前后内容进行换行;而在p中的内容单独形成段落
你好
段落标签
第一行
第二行 hello html
图片

超链接
网站点击内容
表格
- 表格:table
- tr:表格的一行
- th:表头单元格,加粗
- td:判断单元格
- colspan:一行n列合并成1列
- align:(表格)所在区域
- border:(表格)边框是否有,默认为0
- celladding:字与表格的间距
- cellspacing:表格的间距
- width:(表格)宽度
- height:(表格)高度
菜名 单价 折扣 红烧茄子 10 8折 金额:8¥
顺序/无序
无序列表 ul,配合 li 使用;有序列表 ol,配合 li 使用;自定义列表 dl,配合 dt 和 dd,dt 是标题,dd 是标题下的内容
- order list
- unorder list
- 标题
- 段落
表单
- form:表单标签
- input:输入标签
- action:服务器端程序地址
- method:提交方式
- enctype:编码类型,其中 multipart/form-data 常用于文件上传
- type:输入的类型(默认是text类型)
- name:表单上传时输入类型的字段名
- placeholder:输入文本框的提示词
- value:在file类型时无效,在submit中显示按钮提示词;不同的类型有不同的功能
选择
- selected:默认选择
选择年份 2019 2020
文本框
盒子
无语义标签:div/span:用来布置页面布局
占一行不占一行真的
div 没有语义. 对于搜索引擎来说没有意义. 为了让搜索引擎能够更好的识别和分析页面(SEO 优化), HTML 引入了更多的 \"语义化\" 标签. 但是这些标签其实本质上都和 div 一样(对于前端开发来说). 然而对于搜索引擎来说, 见到 header 和 article 这种标签就会重点进行解析
- header:头部
- nav:导航
- article:内容
视频标签:video,音频标签:audio
- src:资源所在位置;
- controls:控制视频/音频界面播放控制
CSS
CSS 能够对网页中元素位置的排版进行像素级精确控制, 实现美化页面的效果. 能够做到页面的样式和结构分离
选择器 {n条说明};常见的选择器:通配符*,标签选择器,子类选择器,id选择器
/*通配选择器*-对所有的标签产生效果,页面布局重置靠左*/ *{ margin: 0; padding: 0; } a{ color:red; size: 20px; } .green { color:green; } #blue { color:blue; } 雷猴啊 雷猴啊 雷猴啊
vue
安装
使用vue
{{message}} var app = new Vue({ el: \'#app\', data: { message: \'hello\', }, });
- {{XXX}} 里面{}是插值操作
- 创建了一个app对象,一个d选择器
- new Vue 后面跟的是json格式的字段
- el:选择器类型
- data:数据字段
引用app对象后使用message字段,会自动替换成message在data定义的数据,在页面可以根据需要修改
v-cloak
v-cloak:遮罩,message变成数据需要通过vue加载出来,可能其中会有一小段时间{{message}}显示,我们不想让用户看到就可以使用v-cloak让它数据没出来之前不显示
[v-cloak] { display: none; } {{str}} {{str1}}
v-bind
v-bind:使用标签通过data里面的url动态调整网站链接需要使用v-bind绑定
var app = new Vue({ el: \'#app\', data: { str: \'hello\', url:\"https://www.baidu.com\" }, });
v-on
v-on:
var app = new Vue({ el: \'#app\', methods:{ dialog:function(s){ alert(s); } }, });
v-show
v-show,后面=true是为真,显示下面的内容;为假就不显示
var app = new Vue({ el: \'#app\', data: { tmp:false }, methods:{ dialog:function(s){ alert(s); } } });
相应地还有v-if,v-else:它们解析是才是真正的判断,而v-show为假只是把内容隐藏了
{{hero}} var app = new Vue({ el: \'#app\', data: { heros:[\'小乔\',\'二乔\',\'大乔\'] }, });
v-model
可以将一个 vue 数据与标签数据关联起来,实现一荣俱荣一损俱损的效果
{{tmp}} var app = new Vue({ el: \'#app\', data: { tmp:\'hello world\' }, });
v-for
v-for:循环可以绑定数据到数组来渲染一个标签
{{hero}} let app = new Vue({ el: \'#app\', data: { heros: [\'小乔\', \'曹操\', \'李白\'], }});
使用ajax异步请求服务器资源
let app = new Vue({el: \'#app\',data: {numbers: 0,videos: []},methods: {myclick: function () {$.ajax({url: \"http://192.168.122.137:9090/video\",type: \"get\",context: this,//这里是将vue对象传入ajax作为this对象success: function (result, status, xhr) {//请求成功后的处理函数this.videos = result;alert(result);}})}}});
客户端
如果从0到1写页面很费时间,可以选择视频页面模板进行修改成我们想要的页面
视频展示页面信息
- 在vue的methods中:添加GetVideo方法;
- 实现:ajx发送 GET /video请求所有视频数据后保存在data数组videos中;
- 视频展示页面以v-for的方式得到每个视频数据并进行填写,包括:视频名,视频图片,以及点击视频跳转后的url中,把视频id给携带上(后面要用到)
视频展示页面新增视频按钮
- 找到页面的弹窗按钮(点击后会弹出弹窗要我们填写信息,本质上是form);
- 用户信息填写的内容修改成:视频名,视频内容,视频文件,图片文件;
- form表单中添加:action = \"/video\",method = \"post\",enctype=\"multipart/form-data\";
- 提交form表单后就给服务器发送 POST /video请求新增视频;
- 在代码中处理新增视频的业务处理后的响应添加上重定向,跳转到当前页面
播放页面视频播放
- 跳转到播放页面中的url携带了要播放的视频id,要将id值进行截取;
- 使用ajx方法发送 Put /video/3 请求视频id为3的视频数据保存在video中;
- 在播放的src链接给上video.video值(video是视频保存路径),给上下面视频内容video.info
播放页面视频修改
- 找到页面弹窗按钮,修改弹窗内的信息:视频名,视频内容;
- 使用ajx方法发送 Put /video/3 请求更新视频id为3的视频数据(原视频数据转成字符串格式一起发送),新视频数据保存在video中;
- 刷新当前页面:window.location.reload()
播放页面视频删除
- 新增页面弹窗按钮(拷贝上面按钮代码)
- 绑定v-on一个ajx方法:发送 Delete /video/3 请求删除视频id为3的视频数据
- 重定向当前页面到视频展示页面: window.location.href=\"/index.html\"
测试报告
项目背景
视频共享系统采用前端 + 后端实现,使用数据库 + 文件路径的储存用户提交的视频与图片;前端实现了两个页面:视频列表页面与视频播放页面;后端实现了:新增视频,修改视频,查询视频,删除视频的功能;数据库储存了视频,图片的相对路径,前端通过它来找到视频,图片的储存路径;
项目功能
主要实现了:新删查改视频:
新增视频:用户填写完视频相关数据后,后端使用数据库储存的同时,把视频和图片资源放在指定的文件中,下次用户点击视频想要观看时就可以通过路径找到指定的文件;
删除视频:在视频播放页面上面实现了点击视频删除按钮,点击后页面播放的视频相关视频将被删除,同时重定向到视频列表页面;
查找视频:在视频列表页面上可以根据关键词搜索到相关视频页面,用户就可以找到想要的视频点击进行观看;
修改视频:在视频播放页面上实现了视频修改按钮,用户可以修改当前的视频名,视频内容,修改完成后当前视频数据即可生效
项目计划
功能测试
先整理出思维导图
新增视频:填写有效的视频数据与视频,图片文件
结果正确显示在视频列表中
什么都不填写直接提交,此时返回Json格式的报错
查找视频:在搜索框中填写有效的关键词,在当前视频列表能够找到的视频名
结果能够正确返回相关视频
搜索框中什么也不填写直接提交,返回的是全部视频列表
点击视频列表中的任意一个视频播放按钮,可以正常播放
点击没有提交过视频文件的视频办法按钮,则视频播放异常
修该视频:点击播放页面修改视频按钮,提交新的视频名,内容
结果正确显示出修改后的新视频名,内容
删除视频:点击删除视频按钮删除当前播放的视频数据,返回的视频列表中没有显示删除视频的相关数据
自动化测试
以页面为单位,使用selenium库来编写自动化测试脚本进行测试
实现一个类Driver用来初始化driver并生成一个BlogDriver对象 ,每次测试时使用BlogDriver获取driver;在类Driver中设计出屏幕截图方法函数
//Ultils.pyimport datetimeimport osimport sysimport timefrom selenium import webdriverfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.edge.options import Optionsfrom selenium.webdriver.edge.service import Servicefrom webdriver_manager.microsoft import EdgeChromiumDriverManager#使用Driver对象进行测试class Driver: driver=\"\" def __init__(self): option =Options() self.driver=webdriver.Edge(service=Service(EdgeChromiumDriverManager().install()),options=option) self.driver.implicitly_wait(5) def SaveScreem(self): #每天截图放在同一个文件夹上 dirname=datetime.datetime.now().strftime(\"%Y-%m-%d\") if not os.path.exists(\"../image/\"+dirname): os.mkdir(\"../image/\"+dirname) # 测试方法-日期.png filename=sys._getframe().f_back.f_code.co_name+\"-\" +datetime.datetime.now().strftime(\"%Y-%m-%d-%H.%M.%S\")+\".png\" # ../image/日期文件夹/测试方法-日期.png self.driver.save_screenshot(\"../image/\"+dirname+\"/\"+filename) def SwitchPage(self): BeforeHandle = BlogDriver.driver.current_window_handle BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#home-main > div > div.col-lg-9.col-md-12.col-sm-12 > div.row.auto-clear > article > div > div.thumbr > a > span > i\").click() # 切换页面 Allhandle = BlogDriver.driver.window_handles for handle in Allhandle: if handle != BeforeHandle: BlogDriver.driver.switch_to.window(handle)#单例模式BlogDriver=Driver()
视频列表页面设计三个测试:新增视频测试,视频搜索测试,视频播放测试;视频播放测试要进行driver句柄的跳转,这个可以在Driver类中设计成函数,因为后面页面的测试也要用到;视频播放在html设计时放在ifname标签页,也是要先进行ifname切换后,才能定位元素并点击视频播放(注意:很多元素使用隐式等待无法正常截图到需要显示等待才能正确截图)
from cryptography.hazmat.primitives.asymmetric import ecfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support import expected_conditions as ecfrom selenium.webdriver.support.ui import WebDriverWaitfrom common.Ultils import BlogDriverfrom selenium.webdriver.common.keys import Keysclass VideoLists: url=\"\" driver=\"\" def __init__(self): self.url=\"http://192.168.109.148:8888/\"#测试网址 self.driver=BlogDriver.driver self.driver.get(self.url) def TestInserVideo(self): BlogDriver.SaveScreem() BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#home1 > div.row.header-top > div.col-lg-3.col-md-6.col-sm-7.hidden-xs > div > button\").click() #填写视频数据 BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#name\").send_keys(\"抖音视频\") BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#info\").send_keys(\"这时一个抖音视频\") BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#video\").send_keys(r\"C:\\Users\\29096\\Desktop\\1.mp4\") BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#image\").send_keys(r\"C:\\Users\\29096\\Desktop\\1.png\") BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#enquirypopup > div > div > div.modal-body > form > div:nth-child(5) > button\").click() #等待视频列表刷新新视频数据 WebDriverWait(self.driver,6).until(ec.visibility_of_element_located((By.CSS_SELECTOR,\"#home-main > div > div.col-lg-9.col-md-12.col-sm-12 > div.row.auto-clear > article > div > div.thumbr > a > img\"))) BlogDriver.SaveScreem() # BlogDriver.driver.quit() def TestSelect(self): WebDriverWait(self.driver,6).until(ec.visibility_of_element_located((By.CSS_SELECTOR,\"#home-main > div > div.col-lg-9.col-md-12.col-sm-12 > div.row.auto-clear > article > div > div.thumbr > a > img\"))) BlogDriver.SaveScreem() input_element=BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#search\") input_element.send_keys(\"抖音\") input_element.send_keys(Keys.RETURN) #等待弹窗 WebDriverWait(BlogDriver.driver, 6).until(ec.alert_is_present()) alert=BlogDriver.driver.switch_to.alert alert.accept() BlogDriver.SaveScreem() def TestVideo(self): #句柄切换 BlogDriver.SwitchPage() # 1. 等待 iframe 加载并切换 iframe = WebDriverWait(BlogDriver.driver, 6).until( ec.presence_of_element_located((By.TAG_NAME, \"iframe\")) ) BlogDriver.driver.switch_to.frame(iframe) # 切换到 iframe 内部 # 2. 定位视频元素 video = WebDriverWait(BlogDriver.driver, 6).until( ec.presence_of_element_located((By.TAG_NAME, \"video\")) ) # 3. 等待视频播放 WebDriverWait(BlogDriver.driver, 6).until( lambda driver: driver.execute_script(\"return arguments[0].currentTime > 0\", video) ) BlogDriver.SaveScreem() # 5. 切回主页面 BlogDriver.driver.switch_to.default_content() # BlogDriver.driver.quit()#调用测试函数#VideoList=VideoLists()#VideoList.TestInserVideo()#VideoList.TestVideo()#VideoList.TestSelect()
视频播放页面就简单些,实现时一定要先进行句柄切换
from selenium.webdriver.support import expected_conditions as ecfrom selenium.webdriver.common.by import Byfrom selenium.webdriver.support.wait import WebDriverWaitfrom common.Ultils import BlogDriverclass VideoWatch: url=\"\" driver=\"\" def __init__(self): self.url=\"http://192.168.109.148:8888\" self.driver=BlogDriver.driver self.driver.get(self.url) def TestModifyVideo(self): BlogDriver.SwitchPage() WebDriverWait(self.driver, 6).until(ec.visibility_of_element_located((By.CSS_SELECTOR, \"#single-video-wrapper > div:nth-child(2) > div > article > div.video-content\"))) BlogDriver.SaveScreem() BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#single-video > div.row.header-top > div.col-lg-3.col-md-6.col-sm-7.hidden-xs > div > button:nth-child(2)\").click() BlogDriver.driver.find_element(By.CSS_SELECTOR, \"#name\").clear() BlogDriver.driver.find_element(By.CSS_SELECTOR, \"#name\").send_keys(\"好看视频\") BlogDriver.driver.find_element(By.CSS_SELECTOR, \"#info\").clear() BlogDriver.driver.find_element(By.CSS_SELECTOR, \"#info\").send_keys(\"这是一个好看视频\") BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#enquirypopup > div > div > div.modal-body > form > div:nth-child(3) > button\").click() WebDriverWait(self.driver, 6).until(ec.visibility_of_element_located((By.CSS_SELECTOR, \"#single-video-wrapper > div:nth-child(2) > div > article > div.video-content\"))) BlogDriver.SaveScreem() def TestDeleteVideo(self): BlogDriver.SwitchPage() BlogDriver.driver.find_element(By.CSS_SELECTOR,\"#single-video > div.row.header-top > div.col-lg-3.col-md-6.col-sm-7.hidden-xs > div > button:nth-child(1)\").click() WebDriverWait(self.driver,6).until(ec.visibility_of_element_located((By.CSS_SELECTOR,\"#home-main > h2\"))) BlogDriver.SaveScreem()#测试函数#VideoWatch=VideoWatch()#VideoWatch.TestModifyVideo()#VideoWatch.TestDeleteVideo()
一个一个测试没问题后一同放在Main中进行测试
from common.Ultils import BlogDriverfrom tests.VideoLists import VideoListsfrom tests.VideoWatch import VideoWatchif __name__ == \"__main__\": VideoLists().TestInserVideo() VideoLists().TestSelect() VideoLists().TestVideo() VideoWatch().TestModifyVideo() VideoWatch().TestDeleteVideo() BlogDriver.driver.quit()
Bug:测试是发现相同的视频数据插入时如果一个删除后另一个就删除不掉了;原因是:新增相同视频数据后服务器只保留一份;解决:服务器保存时在文件名后加入时间戳防止只保留一份(也可以在处理新增视频业务模块上进行判断,相同视频数据插入就不做新增)
性能测试
使用jmeter进行按照增删查改的顺序为一个事务,进行梯度压测;单线程时所有请求成功,多线程并发请求新增视频数据,前几秒成功,后面请求全部失败,原因是mysql服务器断开连接后没有进行重新连接
修改Bug:在每次处理业务前先判断当前数据库连接是否断开,如果断开了就需要重新连接,此时再继续测试就可以了;测试到4个线程进行并发时程序崩溃概率很高,来不及重新进行连接:这也说明了当前服务器最大允许为3个线程同时并发请求
以上测试文件都提交到项目文件中,有需要点击链接查看