尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

C++单元测试实战:gtest与gmock环境搭建、核心用法与工程实践

C++单元测试实战:gtest与gmock环境搭建、核心用法与工程实践
📅 发布时间:2026/7/13 2:20:34

1. 项目概述:为什么我们需要gtest和gmock?

在C++项目里摸爬滚打十几年,我见过太多项目在后期因为代码质量失控而陷入泥潭。功能越加越多,模块间的耦合越来越紧,一个看似简单的改动,却可能引发一连串意想不到的崩溃。这时候,单元测试就不再是“锦上添花”的选项,而是“雪中送炭”的必需品。它就像给代码上了一道保险,让你在重构、优化甚至只是日常修改时,心里有底,手不抖。

然而,C++的单元测试,尤其是面对复杂的依赖关系时,常常让人望而却步。一个类可能依赖数据库连接、网络服务、文件系统,或者另一个尚未实现的复杂模块。你不可能为了测试一个简单的业务逻辑函数,就去真的启动一个数据库或者调用一个收费的第三方API。这就是模拟框架的价值所在——它让你能够“伪造”这些依赖,将待测代码与外部世界隔离开,专注于其自身的逻辑正确性。

Google Test(gtest)和Google Mock(gmock)正是为了解决这些问题而生的黄金搭档。gtest提供了一个强大、灵活的测试框架,帮你组织测试用例、管理测试夹具、进行丰富的断言。而gmock则是一个专门用于创建模拟对象(Mock Objects)的库,它能让你轻松地定义依赖对象的行为,并验证它们是否被以预期的方式调用。这套组合拳,是构建高可测试性、高可维护性C++代码的基石。无论你是正在维护一个庞大的遗留系统,还是从零开始一个全新的项目,掌握它们都能让你的开发效率和代码质量提升一个档次。

2. 环境搭建与项目集成:从零开始的配置实战

纸上谈兵终觉浅,绝知此事要躬行。要玩转gtest和gmock,第一步就是让它们在你的开发环境中“安家落户”。这个过程本身,就是一次对现代C++项目构建的绝佳实践。

2.1 选择合适的获取与集成方式

目前,主要有三种主流方式将gtest/gmock引入你的项目,每种方式都有其适用场景。

方式一:包管理器集成(推荐)这是最现代、最省心的方式。如果你使用vcpkg或Conan这样的C++包管理器,一行命令就能搞定。

# 使用 vcpkg vcpkg install gtest gmock # 使用 Conan conan install gtest/1.14.0@ -g cmake

之后,在你的CMakeLists.txt中,使用find_package即可轻松链接。这种方式自动处理了依赖、编译选项和平台差异,极大地简化了项目管理。

方式二:源码作为子模块(Git Submodule)对于希望锁定特定版本,或者项目本身就在一个Git仓库中的情况,将gtest源码作为子模块引入是经典做法。

git submodule add https://github.com/google/googletest.git third_party/googletest

然后在CMakeLists.txt中,通过add_subdirectory(third_party/googletest)将其加入构建。这种方式让你对依赖有完全的控制权,但需要自己管理编译和可能的冲突。

方式三:直接下载与编译最传统的方式,从GitHub Releases页面下载源码包,解压后手动编译安装。这种方式灵活性最差,不推荐在新项目中使用,但在某些受限环境中可能是唯一选择。

注意:无论采用哪种方式,请务必确保你的项目构建系统(如CMake)与gtest的集成是正确且一致的。一个常见的坑是,Debug和Release版本混用导致链接错误。务必保证你的测试目标和gtest库使用相同的编译配置(如/MTdvs/MDd)。

2.2 CMake集成详解与最佳实践

CMake是现代C++项目的事实标准,这里给出一个清晰、可复用的CMakeLists.txt模板。

cmake_minimum_required(VERSION 3.14) project(MyAwesomeProject) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 1. 查找gtest包(假设已通过包管理器安装) find_package(GTest REQUIRED) # 如果使用子模块,则用 add_subdirectory(googletest) 替代 find_package # 2. 添加你的主库或可执行文件 add_library(my_lib src/my_class.cpp include/my_class.h) # 3. 添加测试可执行目标 add_executable(tests test/test_my_class.cpp # ... 其他测试文件 ) # 4. 链接依赖 target_link_libraries(tests PRIVATE my_lib GTest::gtest GTest::gtest_main # 提供 main() 函数,自动运行所有测试 GTest::gmock # 如果需要使用模拟功能 ) # 5. 将测试目标添加到CTest,方便通过 `ctest` 命令运行 include(GoogleTest) gtest_discover_tests(tests)

关键点解析:

  • GTest::gtest_main:这个目标链接了一个已经写好的main()函数,它会自动初始化gtest框架并运行所有测试。对于绝大多数测试项目,直接用它即可,无需自己编写main函数。
  • gtest_discover_tests:这是一个CMake函数,它会扫描tests可执行文件,自动将所有测试用例注册到CMake的CTest系统中。之后你不仅可以用./tests运行测试,还可以用ctest或ctest --output-on-failure来运行,后者在测试失败时会打印详细信息,非常方便。
  • 目录结构建议:保持清晰的目录结构,如src/放源码,include/放头文件,test/放所有测试文件。这有助于管理和维护。

3. gtest核心概念与断言艺术

gtest的哲学是让测试代码像普通代码一样清晰、易写。它提供了一套丰富的断言宏和测试组织方式,我们来深入拆解。

3.1 测试宏与断言:从基础到高阶

断言是测试的骨架,gtest提供了两类主要的断言:ASSERT_*和EXPECT_*。

  • ASSERT_*:致命断言。如果检查失败,当前测试函数会立即终止。适用于后续测试依赖此前置条件的情况,比如指针非空、容器不为空等。
  • EXPECT_*:非致命断言。即使检查失败,测试函数也会继续执行,报告所有失败。适用于一次测试中验证多个独立条件。

基础值比较:

EXPECT_EQ(3, Calculate(1, 2)); // 相等 ASSERT_NE(nullptr, ptr); // 不相等 EXPECT_LT(a, b); // 小于 (Less Than) EXPECT_GE(score, 60); // 大于等于 (Greater or Equal)

字符串比较:

EXPECT_STREQ("Hello", str.c_str()); // C风格字符串相等 EXPECT_STRNE("A", "B"); EXPECT_STRCASEEQ("hello", "HELLO"); // 忽略大小写相等

浮点数比较: 这是新手最容易踩的坑。由于浮点数的精度问题,直接使用EXPECT_EQ比较两个double值几乎总是会失败。

double a = 0.1 + 0.2; double b = 0.3; // 错误做法: EXPECT_EQ(a, b); // 很可能失败! // 正确做法: EXPECT_DOUBLE_EQ(a, b); // 精确比较,仍可能因精度失败 EXPECT_NEAR(a, b, 1e-9); // 在1e-9的误差范围内即认为相等(推荐)

布尔值与异常:

EXPECT_TRUE(IsValid()); EXPECT_FALSE(container.empty()); EXPECT_THROW(FunctionThatThrows(), std::runtime_error); // 期望抛出特定异常 EXPECT_NO_THROW(SafeFunction()); // 期望不抛异常

容器与复杂对象: gtest可以打印出标准容器和许多自定义类型(如果定义了operator<<)的内容,在失败时非常有用。

std::vector<int> vec = {1, 2, 3}; EXPECT_EQ(vec, (std::vector<int>{1, 2, 3})); // 注意括号,防止编译器误解析 // 失败时会打印: Expected equality of these values: vec { 1, 2, 3 } { 1, 2, 4 }

3.2 测试夹具(Test Fixture):共享设置与清理

当多个测试用例需要相同的初始化和清理步骤时,重复代码会让人抓狂。测试夹具(Fixture)就是用来解决这个问题的。它通过继承::testing::Test类来创建。

class DatabaseTest : public ::testing::Test { protected: // 每个测试用例开始前执行 void SetUp() override { db_ = std::make_unique<Database>(); bool ok = db_->Connect("test.db"); ASSERT_TRUE(ok); // 如果连接失败,后续测试无意义,用ASSERT } // 每个测试用例结束后执行 void TearDown() override { db_->Disconnect(); } // 供测试用例使用的成员 std::unique_ptr<Database> db_; }; // 使用 TEST_F 宏,第一个参数是夹具类名 TEST_F(DatabaseTest, InsertRecordSucceeds) { Record r {1, "Alice"}; EXPECT_TRUE(db_->Insert(r)); } TEST_F(DatabaseTest, QueryExistingRecord) { Record r = db_->Query(1); EXPECT_EQ(r.id, 1); EXPECT_STREQ(r.name.c_str(), "Alice"); }

关键技巧:

  • SetUp和TearDown保证了每个测试都在一个干净、一致的环境中运行,测试之间相互独立。
  • 在SetUp中可以使用ASSERT_*,因为如果SetUp失败,gtest会跳过该夹具下的所有测试,这比让测试因环境问题而莫名其妙失败要好。
  • 夹具的成员变量通常是protected,以便子类(即你的测试)访问。

3.3 参数化测试:用数据驱动测试

当你需要用多组不同的输入数据来测试同一个逻辑时,参数化测试可以避免写一堆几乎相同的TEST。

// 1. 定义一个参数化测试类,继承自 TestWithParam class IsPrimeTest : public ::testing::TestWithParam<int> {}; // 2. 使用 TEST_P 定义测试 TEST_P(IsPrimeTest, HandlesPositiveInput) { int n = GetParam(); // 获取参数 EXPECT_TRUE(IsPrime(n)); } // 3. 实例化测试,提供参数列表 INSTANTIATE_TEST_SUITE_P(PrimeNumbers, IsPrimeTest, ::testing::Values(2, 3, 5, 7, 11, 13, 17));

这会产生名为PrimeNumbers/IsPrimeTest.HandlesPositiveInput/0,/1... 等多个测试用例。你还可以使用::testing::Range(begin, end),::testing::ValuesIn(container),甚至组合::testing::Combine来生成更复杂的参数组合。

实战心得:参数化测试非常适合测试边界条件、等价类划分。比如测试一个排序函数,你可以用多组无序数组作为输入,验证输出是否有序。

4. gmock深度解析:模拟的艺术与科学

gmock的核心思想是“模拟对象”(Mock Object)。它不是简单的“桩”(Stub),而是能设定预期行为并验证交互的智能替身。

4.1 创建模拟类:从接口到模拟

gmock要求你模拟的是一个抽象(虚函数)或可覆写的方法。通常,这意味着你需要面向接口编程。

步骤一:定义接口(或已有类)

class IEmailService { public: virtual ~IEmailService() = default; virtual bool Send(const std::string& to, const std::string& subject, const std::string& body) = 0; virtual int GetQueueSize() const = 0; };

步骤二:创建模拟类

#include <gmock/gmock.h> class MockEmailService : public IEmailService { public: // MOCK_METHOD 宏:返回值类型,方法名,参数列表,调用约定(可选) MOCK_METHOD(bool, Send, (const std::string& to, const std::string& subject, const std::string& body), (override)); MOCK_METHOD(int, GetQueueSize, (), (const, override)); };

MOCK_METHOD宏是核心。注意第四个参数(const, override),它表示这个方法是const的,并且使用override关键字。对于非const、非虚等方法,对应调整即可。

4.2 设置期望行为:当调用发生时

创建了模拟对象后,你需要告诉它:“当某个方法被以某种方式调用时,你应该如何反应”。这就是设置期望(Expectation)。

基本行为设置:

MockEmailService mockService; // 期望 Send 被调用一次,参数任意,返回 true EXPECT_CALL(mockService, Send).WillOnce(Return(true)); // 期望 Send 被调用任意次数,参数为特定值,始终返回 false EXPECT_CALL(mockService, Send("alice@example.com", "Welcome", testing::_)) .WillRepeatedly(Return(false)); // 期望 GetQueueSize 被调用至少一次,返回 5 EXPECT_CALL(mockService, GetQueueSize).Times(AtLeast(1)).WillRepeatedly(Return(5));

关键概念解析:

  • Times:指定调用次数。AtLeast(n),AtMost(n),Between(m, n),Exactly(n)。默认是Times(1)。
  • WillOnce/WillRepeatedly:指定单次或重复的行为。Return(value)是返回固定值,还有SetArgReferee(设置引用参数)、Invoke(调用一个函数或lambda)等强大动作。
  • 参数匹配器(Matchers):testing::_是通配符,匹配任何值。gmock提供了丰富的匹配器:Eq(value),Ne(value),Ge(value)(大于等于),ContainsRegex("pattern"),StartsWith("prefix")等。你可以用AllOf,AnyOf组合它们。

一个更复杂的例子:

// 期望 Send 被调用,第一个参数是“admin”结尾的邮箱,第二个参数任意,第三个参数包含“URGENT” // 调用时,将第三个参数(body)保存到 capturedBody 变量中,并返回 true。 std::string capturedBody; EXPECT_CALL(mockService, Send(EndsWith("@admin.com"), _, HasSubstr("URGENT"))) .WillOnce(DoAll( SaveArg<2>(&capturedBody), // 动作1:保存第3个参数(0-based index) Return(true) // 动作2:返回 true ));

4.3 验证交互:你被调用了多少次?

gmock的验证是自动的。在模拟对象析构时(通常是在每个测试用例结束时),它会检查所有EXPECT_CALL设置的期望是否都得到了满足。如果某个期望的调用次数不足或过多,测试就会失败,并给出清晰的错误信息。

你也可以手动在测试中插入::testing::Mock::VerifyAndClearExpectations(&mockService)来提前验证并清除期望,这在一些复杂场景中有用。

一个常见陷阱:期望顺序。默认情况下,gmock不关心多个期望之间的调用顺序。如果你需要顺序,可以使用InSequence对象:

testing::InSequence seq; // 此后创建的期望必须按顺序发生 EXPECT_CALL(mockService, Send("a@b.com", _, _)); EXPECT_CALL(mockService, GetQueueSize()); // 测试中必须先调用 Send,再调用 GetQueueSize,否则失败。

5. 实战演练:测试一个用户注册服务

让我们用一个完整的例子,串联起gtest和gmock。假设我们有一个UserRegistrar类,它依赖IEmailService发送验证邮件,依赖IUserRepository持久化用户数据。

// 依赖接口 class IUserRepository { public: virtual ~IUserRepository() = default; virtual bool SaveUser(const User& user) = 0; virtual bool Exists(const std::string& username) const = 0; }; class UserRegistrar { public: UserRegistrar(std::unique_ptr<IUserRepository> repo, std::unique_ptr<IEmailService> emailSvc) : repo_(std::move(repo)), emailSvc_(std::move(emailSvc)) {} bool Register(const std::string& username, const std::string& email) { // 1. 检查用户是否存在 if (repo_->Exists(username)) { return false; } // 2. 保存用户 User user{username, email}; if (!repo_->SaveUser(user)) { return false; } // 3. 发送欢迎邮件 return emailSvc_->Send(email, "Welcome!", "Your account is created."); } private: std::unique_ptr<IUserRepository> repo_; std::unique_ptr<IEmailService> emailSvc_; };

现在,我们来为Register方法编写单元测试。

#include <gmock/gmock.h> #include <gtest/gtest.h> // 模拟类定义(通常放在单独的头文件或测试文件顶部) class MockUserRepository : public IUserRepository { public: MOCK_METHOD(bool, SaveUser, (const User& user), (override)); MOCK_METHOD(bool, Exists, (const std::string& username), (const, override)); }; class MockEmailService : public IEmailService { public: MOCK_METHOD(bool, Send, (const std::string& to, const std::string& subject, const std::string& body), (override)); }; // 测试夹具 class UserRegistrarTest : public ::testing::Test { protected: void SetUp() override { mockRepo_ = std::make_unique<MockUserRepository>(); mockEmail_ = std::make_unique<MockEmailService>(); // 保存原始指针,用于设置期望(unique_ptr释放后指针失效,但测试期间对象存在) repoPtr_ = mockRepo_.get(); emailPtr_ = mockEmail_.get(); registrar_ = std::make_unique<UserRegistrar>(std::move(mockRepo_), std::move(mockEmail_)); } std::unique_ptr<UserRegistrar> registrar_; MockUserRepository* repoPtr_; // 用于EXPECT_CALL MockEmailService* emailPtr_; }; // 测试用例1:注册成功 TEST_F(UserRegistrarTest, RegisterSucceedsForNewUser) { const std::string username = "alice"; const std::string email = "alice@example.com"; // 设置期望序列 testing::InSequence seq; // 确保调用顺序符合业务逻辑 // 1. 期望检查用户不存在 EXPECT_CALL(*repoPtr_, Exists(username)).WillOnce(Return(false)); // 2. 期望保存用户成功 EXPECT_CALL(*repoPtr_, SaveUser(testing::_)).WillOnce(Return(true)); // 3. 期望发送邮件成功 EXPECT_CALL(*emailPtr_, Send(email, "Welcome!", testing::_)).WillOnce(Return(true)); // 执行 bool result = registrar_->Register(username, email); // 验证 EXPECT_TRUE(result); // gmock会自动验证上面的所有期望是否满足 } // 测试用例2:用户已存在,注册失败 TEST_F(UserRegistrarTest, RegisterFailsIfUserExists) { const std::string username = "bob"; // 期望:只调用 Exists,且返回 true。SaveUser 和 Send 不应被调用。 EXPECT_CALL(*repoPtr_, Exists(username)).WillOnce(Return(true)); EXPECT_CALL(*repoPtr_, SaveUser(testing::_)).Times(0); // 明确期望不被调用 EXPECT_CALL(*emailPtr_, Send(testing::_, testing::_, testing::_)).Times(0); bool result = registrar_->Register(username, "bob@example.com"); EXPECT_FALSE(result); } // 测试用例3:保存用户失败,流程中断 TEST_F(UserRegistrarTest, RegisterFailsIfSaveFails) { testing::InSequence seq; EXPECT_CALL(*repoPtr_, Exists(testing::_)).WillOnce(Return(false)); EXPECT_CALL(*repoPtr_, SaveUser(testing::_)).WillOnce(Return(false)); // 模拟保存失败 EXPECT_CALL(*emailPtr_, Send(testing::_, testing::_, testing::_)).Times(0); // 不应发送邮件 bool result = registrar_->Register("charlie", "charlie@example.com"); EXPECT_FALSE(result); }

这个实战案例的精髓:

  1. 完全隔离:我们测试的是UserRegistrar的业务逻辑,不依赖真实的数据库或邮件服务器。测试速度快,且稳定。
  2. 行为验证:我们不仅验证了返回值(result),更重要的是通过EXPECT_CALL验证了UserRegistrar与依赖组件之间的交互协议是否正确。例如,当用户已存在时,是否真的没有调用SaveUser和Send。
  3. 场景覆盖:我们覆盖了成功流程和两个关键的失败分支。这是单元测试确保代码健壮性的关键。

6. 高级技巧与避坑指南

掌握了基础之后,一些高级技巧和常见陷阱能让你更游刃有余。

6.1 模拟模板类与棘手依赖

有时你需要模拟一个模板类,或者一个非虚函数(比如来自第三方库)。对于非虚函数,直接模拟是困难的,这通常意味着你的代码设计存在紧耦合,需要考虑用适配器模式(Adapter Pattern)进行封装,创建一个虚接口,再模拟这个接口。

对于模板类,gmock可以处理:

template<typename T> class Container { public: virtual void add(const T& item) = 0; }; // 模拟一个 Container<int> class MockIntContainer : public Container<int> { public: MOCK_METHOD(void, add, (const int& item), (override)); };

6.2 NiceMock, StrictMock 与 NaggyMock

这是gmock提供的三种模拟对象严格度:

  • NiceMock<MockClass>:最宽松。对于没有设置期望的调用,它会默默地执行默认动作(返回默认值)。这可以避免测试因无关紧要的调用而失败,让测试焦点更清晰。
  • StrictMock<MockClass>:最严格。任何未设置期望的调用都会导致测试失败。这有助于发现意料之外的交互,但可能会让测试变得脆弱。
  • NaggyMock<MockClass>:默认行为。未期望的调用会产生警告(Google Test的Warning),但不会导致测试失败。

建议:默认使用NiceMock,除非你特别想确保没有“多余的”调用。在测试夹具的SetUp中,你可以用NiceMock包装:

mockRawPtr_ = new MockSomeClass; // 原始指针 mockUniquePtr_ = std::make_unique<NiceMock<MockSomeClass>>();

6.3 死亡测试(Death Test)

用于测试程序是否在预期的情况下崩溃(如断言失败、段错误)。gtest提供了ASSERT_DEATH等宏。

TEST(MyDeathTest, InvalidInputCausesAbort) { auto bad_func = []() { SomeFunction(nullptr); }; // 传入空指针应崩溃 ASSERT_DEATH(bad_func(), ".*Assertion failed.*"); // 匹配错误信息 }

死亡测试在独立的子进程中运行,因此设置起来有些特殊,需要仔细阅读文档。

6.4 常见问题排查

  1. “未满足的期望”错误,但你觉得应该满足了:首先检查参数匹配器。"string"和std::string("string")在gmock看来可能是不同的。使用Eq(std::string("string"))或确保类型完全匹配。其次,检查调用次数是否精确。一个Times(2)的期望,如果被调用了3次,也会失败。
  2. 内存泄漏警告:如果你在堆上创建了模拟对象(比如用new),并且测试中途失败,可能导致对象未被删除而产生内存泄漏。尽量使用智能指针(如unique_ptr)管理模拟对象的生命周期,或者使用testing::StrictMock/testing::NiceMock配合原始指针,并确保在测试结束时清理。
  3. 链接错误:确保你的测试目标正确链接了gmock库,并且编译选项(如RTTI、异常)与主项目一致。
  4. 测试输出太冗长:使用--gtest_brief=1命令行参数可以只显示失败测试的摘要。在IDE中运行测试时,可以配置这个参数。
  5. 模拟析构函数:通常不需要模拟析构函数。如果确实需要(例如验证某个对象是否被销毁),请非常小心,并查阅gmock的Advanced文档。

7. 集成到开发流程:让测试成为习惯

工具再好,不用也是白搭。将gtest/gmock集成到你的日常开发和CI/CD流程中至关重要。

  1. 本地开发:在IDE(如CLion, VS Code with CMake Tools)中配置测试目标为“启动项”,一键运行所有测试。或者,在终端使用ctest --output-on-failure。养成“写一点代码,跑一遍相关测试”的习惯(TDD则更进一步)。
  2. 预提交钩子(Git Hook):在.git/hooks/pre-commit脚本中加入运行核心测试套件的命令,确保提交的代码不会破坏基础功能。
  3. 持续集成(CI):在GitLab CI、GitHub Actions或Jenkins中,添加一个测试阶段。每次推送代码或发起合并请求时,自动在干净的环境中编译并运行全部测试。测试失败应阻止合并。
  4. 测试覆盖率:结合像gcov/lcov这样的工具,生成测试覆盖率报告。这能直观地看到哪些代码被测试覆盖了,哪些还是“盲区”。不要盲目追求100%覆盖率,但关键业务逻辑和复杂分支应尽量覆盖。
  5. 测试命名与组织:好的测试名就是文档。使用TestFixtureName.TestScenario_ExpectedBehavior的命名约定,如UserRegistrarTest.RegisterSucceedsForNewUser。将测试文件与被测源码文件对应起来,例如my_class.cpp的测试放在test_my_class.cpp中。

最后,我想分享一点个人体会:单元测试和模拟,初期看起来像是额外的工作,会拖慢开发速度。但当你经历过几次因为有了完备测试而敢于大刀阔斧地重构,或者快速定位到一个深藏的边界条件bug时,你就会深刻体会到它带来的长期收益——更高的代码质量、更快的调试速度、以及作为开发者更强的信心。从今天开始,为你下一个C++类写一个测试吧,哪怕只是一个简单的TEST,这就是迈向稳健软件系统的第一步。

相关新闻

  • 锂电池组均衡充电技术及BQ25887应用解析
  • MAX77654与MKV46F电源管理方案在工业物联网中的应用
  • Git 管理 Word 文档 3 大痛点分析:存储膨胀、格式丢失与协作困境

最新新闻

  • AI推理攻击:模型API如何被合法调用‘说漏嘴’
  • 高中生真实项目交付:从Firebase+React到PWA的工程实践
  • 多维聚合:从GROUP BY到OLAP引擎的数据升维实践
  • 2026年7月山东回转寿司设备/菏泽皮带餐饮寿司设备公司选择策略_山东鼎盛科技有限公司 - 品牌宣传支持者
  • 3分钟掌握ComfyUI-Inpaint-Nodes:让AI图像修复变得简单高效
  • 多维聚合数据变形:从长表到宽表的底层原理与实战

日新闻

  • AI推荐结果怎么优化:适合深圳少儿素质培训机构的GEO服务商哪家好?全程零代码SAAS操作
  • RAG 实战教学完全指南
  • 企业级API网关架构深度解析:IBM Microgateway的技术实现与选型指南

周新闻

  • IX9104 PCIe5.0 高速交换芯片@ACP#完整规格 + 应用场景总结
  • Unity游戏集成Coze智能体:实现NPC智能对话与知识库联动
  • SAP EPIC 建行回单查询:从标准类CL_EPIC_EXAMPLE_CN_CCB_GHTD到Z类的5处关键修改

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号