Unit Test 和 gtest 介绍

单元测试Unit Test ,模块测试)是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确,通过编写单元测试可以在编码阶段发现程序编码错误,甚至是程序设计错误。

单元测试不但可以增加开发者对于所完成代码的自信,同时,好的单元测试用例往往可以在 回归测试 的过程中,很好地保证之前所发生的修改没有破坏已有的程序逻辑。因此,单元测试不但不会成为开发者的负担,反而可以在保证开发质量的情况下,加速迭代开发的过程。

GoogleTest是一个跨平台的(LiunxMac OS XWindowsCygwinWindows CE and Symbian) C++ 单元测试框架,GoogleTestgoogle 公司发布, 且遵循 New BSD License(可用作商业用途)的开源项目, 为当前比较主流的 C++ 单元测试框架,目前所在公司也在使用。

gtest 安装、导入项目(Linux系统)

下载源码

我本地使用的系统参数:

bash-4.2$ uname -a
Linux yejy 3.10.0-514.el7.x86_64 #1 SMP Tue Nov 22 16:42:41 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux

gtest github 地址:

下载源码:

bash-4.2$ git clone https://github.com/google/googletest

导入项目

简单测试

下载源码后,接着就是将其导入到我们的项目中使用,如果你只是想简单测试一下,可以直接编译 gtest 源码,生成相应的静态库,将库和头文件拷贝到系统的头文件和库中,然后就可以直接写代码进行测试了,步骤如下:

bash-4.2$ cd googletest
bash-4.2$ cmake
bash-4.2$ make
bash-4.2$ cp libgtest*.a /usr/lib
bash-4.2$ cp –a include/gtest /usr/include

写一个简单的测试程序:

#include<gtest/gtest.h>

int add(int a, int b){
return a+b;
} TEST(testCase, test0){
EXPECT_EQ(add(4,3), 7); // 断言检测两参数是否相等
} int main(int argc, char **argv)
{
testing::InitGoogleTest(&argc, argv); // 初始化,所有测试都是这里启动的
return RUN_ALL_TESTS(); // 运行所有测试用例
}

编译代码,当然你可以用 make 或者 cmake 编译都可以,具体输出:

[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from testCase
[ RUN ] testCase.test0
[ OK ] testCase.test0 (0 ms)
[----------] 1 test from testCase (0 ms total)

工业生产

上面这种测试方法比较特殊,等于是把 gtest 库和 gnu c 库一样使用了,正常工作项目中,肯定不会这样用的。

正确的做法是 以第三方库的形式直接将源码引入进项目。可能有人就会说了,为什么一定要将源代码引入其中,而不先编译出静态库,然后导入其中呢,这样编译自己项目的时候不就不用再重新编译了吗? 这里主要是考虑 跨平台,编译环境会有多种,需要多次编译,因此需要源码导入,同宿主项目一起编译。

我比较熟悉的编译工具是 cmake, 工作中使用的也是这个,该工具也是跨平台的,在编译大型跨平台项目时,很有优势,那这边就大致讲一下引入步骤,如果你对 cmake 很熟悉,那这边就很轻松了。

首先看一下引入后的代码结构,如下图:

重点是这个文件 unit_test/CMakeLists.txt

file(GLOB SRC_FILES ./*.cpp)
file(GLOB HEADER_FILES ./*.h) # 将给定目录添加到编译器用于搜索包含文件的目录中。相对路径被解释为相对于当前源目录。
# 相当于gcc命令的-I,告诉编译器到该目录中查找头文件
include_directories(${CMAKE_SOURCE_DIR}/src)
if(ENABLE_TEST)
include_directories(
${CMAKE_SOURCE_DIR}/3rdlib/googletest/googlemock/include
${CMAKE_SOURCE_DIR}/3rdlib/googletest/googletest/include
)
endif() # 生成可执行文件 posix_thread_test.exx
add_executable(posix_thread_test.exx
${SRC_FILES}
) # 引入 gtest 库,posixthread 为源代码库
target_link_libraries(posix_thread_test.exx
gtest
posixthread
) target_install(posix_thread_test.exx)

导入项目,主要就是看 unit_test/CMakeLists.txt 这个文件了,其他基本变化不大,如果你熟悉 cmake 很容易就能看懂。 至于图中的源码,是最近在封装 Posix-thread 时写的,源码大部分引用了陈硕老师的 muduo 网络库中的线程相关代码。

gtest 具体使用

介绍一下断言,断言主要用来做一些逻辑判断,主要有以下两类接口:

  • ASSERT_XXX(): 如果断言失败,则测试处理终止。
  • EXPECT_XXX(): 非致命性失败,允许继续处理。
Test Fatal NonFatal
condition 为真 ASSERT_TRUE(condition) EXPECT_TRUE(condition)
condition 为假 ASSERT_FALSE(condition) EXPECT_FALSE(condition)
Equal ASSERT_EQ(arg1,arg2) EXPECT_EQ(arg1,arg2)
Not Equal ASSERT_NE(arg1,arg2) EXPECT_NE(arg1,arg2)
Less Than ASSERT_LT(arg1,arg2) EXPECT_LT(arg1,arg2)
Less Than or Equal ASSERT_LE(arg1,arg2) EXPECT_LE(arg1,arg2)
Greater Than ASSERT_GT(arg1,arg2) EXPECT_GT(arg1,arg2)
Greater Than or Equal ASSERT_GE(arg1,arg2) EXPECT_GE(arg1,arg2)
C String Equal ASSERT_STREQ(str1,str2) EXPECT_STREQ(str1,str2)
C String Not Equal ASSERT_STRNE(str1,str2) EXPECT_STRNE(str1,str2)
C String Case Equal ASSERT_STRCASEEQ(str1,str2) EXPECT_STRCASEEQ(str1,str2)
C String Case Not Equal ASSERT_STRCASENE(str1,str2) EXPECT_STRCASENE(str1,str2)
Verify that exception is thrown ASSERT_THROW(statement,exception_type) EXPECT_THROW(statement,exception_type)
Verify that exception is thrown ASSERT_ANY_THROW(statement) EXPECT_ANY_THROW(statement)
Verify that exception is NOT thrown ASSERT_NO_THROW(statement) EXPECT_NO_THROW(statement)

测试代码如下:

#include <gtest/gtest.h>
#include <posix_thread.h> void threadFunc()
{
std::cout << "tid= "<< PosixThread::CurrentThread::tid() << std::endl;
} TEST(PosixThreadTest, CreateThread)
{
std::cout << "pid= " << ::getpid() << " tid= " <<PosixThread::CurrentThread::tid() << std::endl; PosixThread::Thread t1(threadFunc);
t1.start();
ASSERT_TRUE(t1.started());
EXPECT_FALSE(t1.started()); // 故意失败
ASSERT_FALSE(t1.started()); // 故意失败
std::cout << "t1.tid: " << t1.tid() << std::endl;
std::cout << "thread name: " << t1.name().c_str() << std::endl; t1.join(); std::cout << "CreateThread end !\n"
<< std::endl;
} TEST(AtomicTest, AtomicInt64)
{
std::cout << "pid= " << ::getpid() << " tid= " <<PosixThread::CurrentThread::tid() << std::endl; PosixThread::AtomicInt64 a0;
ASSERT_EQ(a0.get(), 0);
ASSERT_EQ(a0.getAndAdd(1), 0);
ASSERT_EQ(a0.get(), 1);
ASSERT_EQ(a0.addAndGet(2), 3);
ASSERT_EQ(a0.get(), 3);
ASSERT_EQ(a0.incrementAndGet(), 4);
ASSERT_EQ(a0.get(), 4);
a0.increment();
ASSERT_EQ(a0.get(), 5);
ASSERT_EQ(a0.addAndGet(-3), 2);
ASSERT_EQ(a0.getAndSet(100), 2);
ASSERT_EQ(a0.get(), 100);
}

执行结果:

bash-4.2$ ./output/bin/posix_thread_test.exx
[==========] Running 2 tests from 2 test cases.
[----------] Global test environment set-up.
[----------] 1 test from PosixThreadTest
[ RUN ] PosixThreadTest.CreateThread
pid= 5297 tid= 5297
tid= 5298
/home/willy/myshare/thread-pool/unit_test/thread_test.cpp:16: Failure
Value of: t1.started()
Actual: true
Expected: false
/home/willy/myshare/thread-pool/unit_test/thread_test.cpp:17: Failure
Value of: t1.started()
Actual: true
Expected: false
[ FAILED ] PosixThreadTest.CreateThread (0 ms)
[----------] 1 test from PosixThreadTest (0 ms total) [----------] 1 test from AtomicTest
[ RUN ] AtomicTest.AtomicInt64
pid= 5297 tid= 5297
[ OK ] AtomicTest.AtomicInt64 (0 ms)
[----------] 1 test from AtomicTest (0 ms total) [----------] Global test environment tear-down
[==========] 2 tests from 2 test cases ran. (0 ms total)
[ PASSED ] 1 test.
[ FAILED ] 1 test, listed below:
[ FAILED ] PosixThreadTest.CreateThread 1 FAILED TEST

从执行结果,我们可以很清楚的知道测试用例具体执行到哪一步,如果失败了,我们可以看到具体是哪一行代码出问题了,程序预期结果是什么,但是实际结果又是什么,输出十分详细。

我们还可以将测试结果导出到 xml 文件,通过参数:--gtest_output 实现。

bash-4.2$ ./output/bin/posix_thread_test.exx --gtest_output="xml:./test.xml"
bash-4.2$ cat test.xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuites tests="2" failures="1" disabled="0" errors="0" timestamp="2019-01-04T21:36:40" time="0" name="AllTests">
<testsuite name="PosixThreadTest" tests="1" failures="1" disabled="0" errors="0" time="0">
<testcase name="CreateThread" status="run" time="0" classname="PosixThreadTest">
<failure message="/home/willy/myshare/thread-pool/unit_test/thread_test.cpp:16 Value of: t1.started() Actual: true Expected: false" type=""><![CDATA[/home/willy/myshare/thread-pool/unit_test/thread_test.cpp:16
Value of: t1.started()
Actual: true
Expected: false]]></failure>
<failure message="/home/willy/myshare/thread-pool/unit_test/thread_test.cpp:17 Value of: t1.started() Actual: true Expected: false" type=""><![CDATA[/home/willy/myshare/thread-pool/unit_test/thread_test.cpp:17
Value of: t1.started()
Actual: true
Expected: false]]></failure>
</testcase>
</testsuite>
<testsuite name="AtomicTest" tests="1" failures="0" disabled="0" errors="0" time="0">
<testcase name="AtomicInt64" status="run" time="0" classname="AtomicTest" />
</testsuite>
</testsuites>

此外,在运行可执行目标程序时,可以使用 --gtest_filter 来指定要执行的测试用例,支持字符串正则匹配,主要如下几种常用情况:

./output/bin/posix_thread_test.exx 没有指定filter,运行所有测试;
./output/bin/posix_thread_test.exx --gtest_filter=* 指定filter为*,运行所有测试;
./output/bin/posix_thread_test.exx --gtest_filter=PosixThreadTest.* 运行测试用例FooTest的所有测试;
./output/bin/posix_thread_test.exx --gtest_filter=*Null*:*Thread* 运行所有全名;
./output/bin/posix_thread_test.exx --gtest_filter=PosixThreadTest.*-PosixThreadTest.CreateThread
运行测试用例FooTest的所有测试,但不包括PosixThreadTest.CreateThread。

gtest 还有很多方便你测试的功能,包括 事件机制, 参数化, 死亡测试, 运行参数等,我们点到为止,如果想继续深入,可以参考这位博主的 gtest 系列, 很详细:

玩转Google开源C++单元测试框架Google Test系列

googlemock 使用

googlemock,是用于编写和使用C++ 模拟类的框架,在我们工作中,主要用来模拟应用程序的一部分,在单元测试用例编写过程中,常常需要编写模拟对象来隔离被测试单元的“下游”或“上游”程序逻辑或环境,从而达到对需要测试的部分进行隔离测试的目的,它可以帮助我们获得更好的系统设计并编写更好的测试。googlemock 同样遵循 New BSD License(可用作商业用途)的开源项目。

在开发过程中,经常出现各联调模块间,进度不一的情况;测试环境非常不稳定,易导致测试失败,导致达不到单元测试的目的,模仿对象提供了解决这些问题的方法:模仿对象符合实际对象的接口,但只包含用来“欺骗”测试对象并跟踪其行为的必要代码。因此,其实现往往比实际实现类简单很多。

官方教程:

官方的 Tutorial 讲的很详细,我在github上也找了一个使用例子,很简洁,但是能很好的说明问题,大致代码如下:

mail_service.h文件:

#ifndef MAIL_SERVICE_HPP
#define MAIL_SERVICE_HPP /** \brief Mail service. This represents one of the collaborators of the SUT.
* \author David Stutz
*/
// 邮件服务
class MailService
{
public:
/** \brief Send a mial.
* \param[in] message message to send
*/
virtual void send(std::string message) = 0; }; #endif /* MAIL_SERVICE_HPP */

order.h文件:

#ifndef ORDER_HPP
#define ORDER_HPP #include <string>
#include <memory>
#include "warehouse.h"
#include "mail_service.h" /** \brief An order of a product with quantity. */
// 订单
class Order
{
public:
/** \brief Constructor.
* \param[in] quantity quantity requested
* \param[in] product product name requested
*/
Order(int quantity, std::string product)
{
this->quantity = quantity;
this->product = product;
} /** \brief Set the mail service to use.
* \param[in] mailService the mail service to attach
*/
// 设置邮件服务
void setMailService(std::shared_ptr<MailService> mailService)
{
this->mailService = mailService;
} /** \brief Fill the order given the warehouse.
* \param[in] warehouse the warehouse to use
* \return whether the operation was successful
*/
// 判断产品是否有库存,发送邮件通知
bool fill(Warehouse &warehouse)
{
if (warehouse.hasInventory(quantity, product))
{
// ...
warehouse.remove(quantity, product);
this->mailService->send("Order filled."); return true;
}
else
{
// ...
this->mailService->send("Order not filled."); return false;
}
} private: /** \brief Product name. */
std::string product; /** \brief Quantity requested. */
int quantity; /** \brief Mail service to use. */
std::shared_ptr<MailService> mailService;
}; #endif /* ORDER_HPP */

warehouse.h文件:

#ifndef WAREHOUSE_HPP
#define WAREHOUSE_HPP #include <string> /** \brief Warehouse interface. This interface is one of the collaborators of our SUT.
* \author David Stutz
*/
class Warehouse
{
public:
/** \brief Check whether the product in the given quantity is on stock.
* \param[in] quantity quantity requested
* \param[in] product product name
* \return whether the warehouse has the product on stock for the given quantity
*/
// 是否有库存
virtual bool hasInventory(int quantity, std::string product) const = 0; /** \brief Remove the given quantity of the product from the warehouse.
* \param[in] quantity quantity to remove
* \param[in] product product name to remove
*/
// 从库存中删除
virtual void remove(int quantity, std::string product) = 0; }; #endif /* WAREHOUSE_HPP */

主要场景就是处理产品订单,其中库存Warehouse类和邮件服务MailService类,我们只声明一下虚基类,不实现,然后通过模拟对象的方式mock一下Warehouse和MailService,来达到订单类接口测试的正常开展,具体测试代码:

#include <gmock/gmock.h>
#include "lib/mail_service.h"
#include "lib/order.h"
#include "lib/warehouse.h" using ::testing::Return;
using ::testing::_; // Matcher for parameters class MockWarehouse : public Warehouse
{
public: // see https://github.com/google/googletest/blob/master/googlemock/docs/ForDummies.md
MOCK_CONST_METHOD2(hasInventory, bool(int, std::string));
MOCK_METHOD2(remove, void(int, std::string));
}; class MockMailService : public MailService
{
public:
MockMailService()
{ } MOCK_METHOD1(send, void(std::string));
}; TEST(OrderTest, Fill)
{
MockWarehouse warehouse;
std::shared_ptr<MockMailService> mailService = std::make_shared<MockMailService>(); Order order(50, "Talisker");
order.setMailService(mailService); EXPECT_CALL(warehouse, hasInventory(50, "Talisker"))
.Times(1)
.WillOnce(Return(true)); EXPECT_CALL(warehouse, remove(50, "Talisker"))
.Times(1); EXPECT_CALL(*mailService, send(_)) // Not making assumptions on the message send ...
.Times(1); ASSERT_TRUE(order.fill(warehouse));
} int main(int argc, char **argv)
{
testing::InitGoogleMock(&argc, argv); // Runs all tests using Google Test.
return RUN_ALL_TESTS();
}

测试结果:

bash-4.2$ ./output/bin/order.exx
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from OrderTest
[ RUN ] OrderTest.Fill
[ OK ] OrderTest.Fill (0 ms)
[----------] 1 test from OrderTest (0 ms total) [----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (1 ms total)
[ PASSED ] 1 test.

其中main函数和gtest差不多,只是初始化的是googlemock,我们着重了解的是几个宏的含义:

MOCK_METHOD

MOCK_METHOD#1(#2, #3(#4) )

MOCK_CONST_METHOD2(hasInventory, bool(int, std::string));
MOCK_METHOD2(remove, void(int, std::string));

其中#1表示你要mock的方法共有几个参数,#2是你要mock的方法名称,#3表示这个方法的返回值类型,#4是这个方法具体的参数。

EXPECT_CALL

using ::testing::Return;
EXPECT_CALL(warehouse, hasInventory(50, "Talisker"))
.Times(1)
.WillOnce(Return(true));

设定期望对象被访问的方式及其响应,其中warehouse为对象,希望hasInventory在传递参数为(50, “Talisker”)时,被调用且仅被调用一次,第一次返回true。

ON_CALL

ON_CALL(#1, #2(#3)).WillByDefault(Return(#4));

ON_CALL(foo, GetSize())
.WillByDefault(Return(1));
// ... other default actions ...

其中#1表示mock对象,#2表示个方法名称,#3表示方法的参数,#4表示参数为#1, #2,#3情况下返回结果。

ON_CALLEXPECT_CALL的区别? ON_CALL定义了调用mock方法时会发生什么,但并不意味着对被调用方法的任何期望。 EXPECT_CALL不仅定义了行为,还设置了对给定次数(以及在指定顺序时按给定顺序)使用给定参数调用方法的期望。

GoogleMock 为开发者设定 Mock 类行为,跟踪程序运行过程及结果,提供了丰富的支持。但与此同时,应用程序也应该尽量降低应用代码间的耦合度,使得单元测试可以很容易对被测试单元进行隔离。(尽量做到高内聚,低耦合)

总结

Googletest 与 GoogleMock,很好的简化了我们的C++单元测试工作,本篇文章对此做了一个总结,让自己对gtest有了一个系统的认识。测试并不只是测试工程师的责任,对于开发工程师,为了保证发布给测试环节的代码具有足够好的质量( Quality ),为所编写的功能代码编写适量的单元测试是十分必要的。

如果还想更加深入的了解,可查阅官方文档

参考链接

https://www.ibm.com/developerworks/cn/linux/l-cn-cppunittest/?mhq=gtest&mhsrc=ibmsearch_a

https://blog.csdn.net/russell_tao/article/details/7344739

http://www.cnblogs.com/coderzh/archive/2009/03/31/1426758.html

https://github.com/davidstutz/googlemock-example

【3rd_Party】Cpp 单元测试框架-gtest的更多相关文章

  1. Google单元测试框架gtest之官方sample笔记2--类型参数测试

    gtest 提供了类型参数化测试方案,可以测试不同类型的数据接口,比如模板测试.可以定义参数类型列表,按照列表定义的类型,每个测试case都执行一遍. 本例中,定义了2种计算素数的类,一个是实时计算, ...

  2. Google单元测试框架gtest之官方sample笔记3--值参数化测试

    1.7 sample7--接口测试 值参数不限定类型,也可以是类的引用,这就可以实现对类接口的测试,一个基类可以有多个继承类,那么可以测试不同的子类功能,但是只需要写一个测试用例,然后使用参数列表实现 ...

  3. Google单元测试框架gtest之官方sample笔记4--事件监控之内存泄漏测试

    sample 10 使用event listener监控Water类的创建和销毁.在Water类中,有一个静态变量allocated,创建一次值加一,销毁一次值减一.为了实现这个功能,重载了new和d ...

  4. 简单易懂的单元测试框架-gtest(一)

    简介     gtest是google开源的一个单元测试框架,以其简单易学的特点被广泛使用.该框架以第三方库的方式插入被测代码中.同其他单元测试框架相似,gtest也通过制作测试样例来进行代码测试.同 ...

  5. C++单元测试框架gtest使用

    作用 作为代码编码人员,写完代码,不仅要保证编译通过和运行,还要保证逻辑尽量正确.单元测试是对软件可测试最小单元的检查和校验.单元测试与其他测试不同,单元测试可看作是编码工作的一部分,应该由程序员完成 ...

  6. Google C++单元测试框架---Gtest框架简介(译文)

    一.设置一个新的测试项目 在用google test写测试项目之前,需要先编译gtest到library库并将测试与其链接.我们为一些流行的构建系统提供了构建文件: msvc/ for Visual ...

  7. 简单易懂的单元测试框架-gtest(二)

    简介     事件机制用于在案例运行前后添加一些操作(相当于挂钩函数).目前,gtest提供了三种等级的事件,分别: 全局级,所有案例执行的前后 TestSuite级,某一个案例集的前后 TestCa ...

  8. Google单元测试框架gtest之官方sample笔记1--简单用例

    1.0 通用部分 和常见的测试工具一样,gtest提供了单体测试常见的工具和组件.比如判断各种类型的值相等,大于,小于等,管理多个测试的测试组如testsuit下辖testcase,为了方便处理初始化 ...

  9. Google C++单元测试框架GoogleTest(总)

    之前一个月都在学习googletest框架,对googletest的文档都翻译了一遍,也都发在了之前的博客里,另外其实还有一部分的文档我没有发,就是GMock的CookBook部分:https://g ...

  10. Google C++单元测试框架GoogleTest---GTest的Sample1和编写单元测试的步骤

    如果你还没有搭建gtest框架,可以参考我之前的博客:http://www.cnblogs.com/jycboy/p/6001153.html.. 1.The first sample: sample ...

随机推荐

  1. 计算机网络之防火墙和Wlan配置

    一.防火墙 防火墙(firewall)是一种安全设备,它的位置一般处于企业网络边界与外网交界的地方,用于隔离不信任的数据包 准确点讲,它就是隔离外网和内网的一道屏障,用于保护内部资源信息安全的一种策略 ...

  2. #ifndef、#define、#ifdef等命令在C#中的应用

    1.#ifndef指令的理解 #ifndef是一个预处理指令,用于判断一个表示符是否被定义.如果该标识符未被定义,则执行条件编译中的代码块.否则,将跳过该代码块.下面是一个简单的粒子. 1 #ifnd ...

  3. HelloJs

    JS 轻量级脚本语言,也是嵌入式语言,是一种对啊想模型语言,简称JS 想要实现复杂的效果,得依靠宿主环境提供API,最常见的是浏览器,还有服务器环境(操作系统) 语言机构+宿主环境提供的API 写js ...

  4. h5移动端使用video实现拍照、上传文件对象、选择相册,做手机兼容。

    html部分 <template> <div class="views"> <video style="width: 100vw; heig ...

  5. [ABC262C] Min Max Pair

    Problem Statement You are given a sequence $a = (a_1, \dots, a_N)$ of length $N$ consisting of integ ...

  6. Spring Cache + Caffeine的整合与使用

    前言 对于一些项目里需要对数据库里的某些数据一直重复请求的,且这些数据基本是固定的,在这种情况下,可以借助简单使用本地缓存来缓存这些数据.这些介绍一下Spring Cache和Caffeine的使用. ...

  7. Helm Chart 部署 Redis 的完美指南

    目录 一.Helm介绍 二.安装Helm 三.配置Helm的repository 四.部署chart(以部署redis为例) 1. 搜索chart 2. 拉取chart 3. 修改values.yam ...

  8. Illegal overloaded getter method with ambiguous type for property......

    问题原因 建议 1.boolean类型的属性值不建议设置为is开头,否则会引起rpc框架的序列化异常. 2.如果强行将IDE自动生成的isSuccess()方法修改成getSuccess(),也能获取 ...

  9. 基于Docker Desktop搭建Kafka集群并使用Java编程开发

    一.引言 前段时间因课业要求使用Docker Desktop 部署Kafka集群并编写生产者消费者程序,折磨了我好几天,在查找大量资料后终于是把整个集群搭建完成了.现在我想要分享其中搭建的历程,希望能 ...

  10. 【UniApp】-uni-app-网络请求

    前言 经过上个章节的介绍,大家可以了解到 uni-app-pinia存储数据的基本使用方法 那本章节来给大家介绍一下 uni-app-网络请求 的基本使用方法 步入正题 首先我们打开官方文档,我先带着 ...