GoogleTest 之路3-Mocking Framework

当你写一个原型或者测试的时候,依赖整个object 是不可行和明智的。一个 mock object和 real object 有一样的接口(因此它能够像同一个使用),可是让你在运行时进行指定它应该如何被使用,它应当作什么(哪些方法应该被调用?以何种顺序?多少次?用什么参数?什么会被返回?)java

注意:很容易弄混 fake objects 和 mock objects。实际上fakes 和 mocks意味着不一样的事情在Test-Driven Development(TDD)社区:python

     Fake object(伪对象) 有 工做实现,可是常常采起一些捷径(也许让操做不昂贵),这会使它们不适合生产。内存中的文件系统就是一个例子。git

     Mocks 是有指望的预编程对象。由一些预期会被调用的 sepcification组成。程序员

若是这对你来讲太抽象的话,don't worry- 你须要记住最重要的就是mock 容许你使用它与你的代码进行交互。当你使用mocks的时候,你对于fakes和 mocks的区别就更清晰了,github

Google C++ Mocking Framework (or Google Mock for short) 是一个用来建立 mock 类库(叫“框架”是由于这样听起来更cool),就像 java 的 jMock and EasyMock数据库

使用 Google Mock 包含如下三个基本步骤:编程

1. 使用一些简单的macros 来描述 你想mock的接口,这将扩展你的mock 类。网络

2. 用很直观的语法来描述一些mock对象的指望和行为。app

3. 练习使用mock 对象的 代码。任何 违反 expectation 的行为一出现就会被Google Mock 捕获。框架

Why Google Mock?

为何使用 Google Mock

虽然Mock Object 能够帮助你移除测试中没必要要的依赖,并使它们快速可靠,可是在C ++中手动使用mock是很难的:

      有些人不得不实现mocks。这个工做既乏味又容易出错。难怪有些人想要避免它。

      手动写的mock的质量是不可靠的,没法预测的。你可能看过一些真正抛光过的,可是你也许会看到一些被匆忙砍掉有各类零时限制的。

      你从一个mock 得到的知识不能使用到下一个mock上面。

相比之下,Java和Python程序员有一些精细的模拟框架,自动建立mock。所以,Mock是一种被证实是有效的技术,并在这些社区普遍采用的作法。拥有正确的工具绝对有所不一样。

Google Mock旨在帮助C ++程序员。它的灵感来自jMock和EasyMock,可是设计时考虑了C ++的细节。它会帮助你的,若是你遇到如下问题:

         你被不怎么好的设计所困扰,早知道应该作更多的原型设计的,但一切都太迟了,可是用C++进行原型设计速度会很慢。

          您的测试很慢,由于它们依赖于太多的库或使用昂贵的资源(例如数据库)

           你的测试是脆弱的,由于他们使用的一些资源是不可靠的(例如网络)

           您想要测试代码如何处理失败(例如,文件校验和错误),可是不容易去制造这么一个失败。

           你想确保你的当前模块和其余模块的交互是正确的,可是观察交互是很不容易的;所以你诉诸于观察行动结束时的反作用,这是最尴尬的

           你想 mock out 你的 依赖,除了还mock尚未被实现;坦白的讲,你对那些手写的mock 不感冒

咱们鼓励你像这样使用Google Mock:

         一个设计工具,它可让你早日常常尝试你的接口设计。更多的迭代致使更好的设计!

          一个测试工具,切断全部测试的外部依赖,探测你的模块和其余模块的交互!

Getting Started

开始吧

使用Google Mock很容易! 在你的C ++源文件中,只要#include“gtest / gtest.h”和“gmock / gmock.h”,你已经准备好了。

 

A Case for Mock Turtles

让咱们来看一个例子。假设你在开发一个图形程序依赖一个 LOGO-like的 API 来绘图。你该怎样测试它作了正确的事情呢?你能够运行它而且与一个golden screen snapshot进行比较,可是我认可:像这样测试是昂贵的而且很脆弱(若是你要更新到一个全新抗锯齿的图像该怎么办?你要更新你全部的golden images),若是你的全部测试都是这样的,这就很痛苦了。Fortunately,你学习到了Dependency Injection而且知道该做什么:不要让你的application 直接 调用 drawing API, 把API包在一个接口里(say, Turtle) and code to that interface:

 

class Turtle {
  ...
  virtual ~Turtle() {}
  virtual void PenUp() = 0;
  virtual void PenDown() = 0;
  virtual void Forward(int distance) = 0;
  virtual void Turn(int degrees) = 0;
  virtual void GoTo(int x, int y) = 0;
  virtual int GetX() const = 0;
  virtual int GetY() const = 0;
};

(注意,Turtle的析构函数必须是虚拟的,就像你打算继承的全部类的状况同样 - 不然当经过基类指针删除一个对象时,派生类的析构函数不会被调用,你会获得损坏的程序状态,如内存泄漏。)

您能够控制 使用PenUp()和PenDown()控制turtle的运动是否留下轨迹,并经过 Forward(),Turn()和GoTo()控制其运动。最后,GetX()和GetY()告诉你当前位置的turtle。

你的程序一般正常使用这个接口的实际实现。在测试中,你可使用 实现的Mock来替换。这让你很容易的检查你程序你调用的 drawing primitives。传了哪些参数,以什么样的顺序。以这种方式编写的测试更强大,更容易读取和维护(测试的意图表示在代码中,而不是在一些二进制图像中)运行得多,快得多。

 

Writing the Mock Class

 

若是你幸运,你须要使用的mock已经被一些好的人实现。可是,你发现本身在写一个模拟class,放松- Google Mock将这个任务变成一个有趣的游戏!

How to Define It

使用Turtle接口做为示例,如下是您须要遵循的简单步骤:

1. MockTurtle继承Turtle类  

2.使用Turtle的虚函数(虽然可使用模板来模拟非虚方法 mock non-virtual methods using templates,可是它更多的涉及)。计算它有多少参数。

3. 在 public 区: section of the child class, write MOCK_METHODn(); (or MOCK_CONST_METHODn(); if you are mocking a const method), where n is the number of the arguments; if you counted wrong, shame on you, and a compiler error will tell you so.

4. 如今来到有趣的部分:你采起函数签名,剪切和粘贴函数名做为宏的第一个参数,留下的做为第二个参数(若是你好奇,这是类型的功能)

5. 重复,直到您要模拟的全部虚拟功能完成。

After the process, you should have something like:

#include "gmock/gmock.h"  // Brings in Google Mock.
class MockTurtle : public Turtle {
 public:
  ...
  MOCK_METHOD0(PenUp, void());
  MOCK_METHOD0(PenDown, void());
  MOCK_METHOD1(Forward, void(int distance));
  MOCK_METHOD1(Turn, void(int degrees));
  MOCK_METHOD2(GoTo, void(int x, int y));
  MOCK_CONST_METHOD0(GetX, int());
  MOCK_CONST_METHOD0(GetY, int());
};

您不须要在其余地方定义这些模拟方法 - MOCK_METHOD *宏将为您生成定义。 就是这么简单! 

一旦你掌握了它,你能够快速的写出 mock class,以致于你的 source control system 都不能处理你的check-in 了

Tips: 若是 这对你来讲工做量太大了,你能够在 Google Mock 的 scripts/generator/目录下面找到gmock_gen.py 工具。

Command-line 工具须要python2.4 安装。你只要给它一个 定义了抽象类的C++文件,它就会给你打印 其mock class。因为C++语言的复杂性,这个脚本可能不老是工做正常,但确实颇有用,read the user documentation.

Where to Put It

当你定义了 mock class,你得决定你把这些定义放到什么地方。有些人把它放在一个* _test.cc。当这些 mock对象是被一我的或者一个团队使用的时候,这样定义就很好。不然,当Foo的全部者改变它,你的测试可能会中断。 (你不能真正指望Foo的维护者修复使用Foo的每一个测试,你能吗?)

因此,经验法则是:若是你须要模拟Foo而且它由其余人拥有,在Foo的包中定义模拟类(更好的是,在一个测试子包中,你能够清楚地分离生产代码和测试实用程序),而且把它放在mock_foo.h。而后每一个人均可以从它们的测试引用mock_foo.h。若是Foo变化,只有一个MockFoo的副本要更改,只有依赖于更改的方法的测试须要修复。

另一种方法:你能够在Foo的顶部引入一个 薄层FooAdaptor ,并将代码引入这一新的接口。由于你拥有FooAdaptor,你能够更容易的吸取Foo的变化。虽然这是最初的工做,仔细选择适配器接口可使您的代码更容易编写和更加可读性,由于你能够选择FooAdaptor适合你的特定领域比Foo更好。

 

Using Mocks in Tests

一旦你有了Mock 类,使用它很是容易。典型的工做流程以下:

1. 从测试命名空间导入Google Mock名称,以便您可使用它们(每一个文件只需执行一次。请记住,命名空间是一个好主意,有利于您的健康。)

2. 建立一些 mock对象

3.指定你对它们的指望(一个方法被调用多少次?有什么参数?它应该作什么等等)。

4.练习一些使用mock的代码; 可使用Google Test断言检查结果。若是一个mock方法被调用超过预期或错误的参数,你会当即获得一个错误。

5. 当模mock destructed,Google Mock将自动检查是否知足了对其的全部指望

例子:

#include "path/to/mock-turtle.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
using ::testing::AtLeast;                     // #1

TEST(PainterTest, CanDrawSomething) {
  MockTurtle turtle;                          // #2
  EXPECT_CALL(turtle, PenDown())              // #3
      .Times(AtLeast(1));

  Painter painter(&turtle);                   // #4

  EXPECT_TRUE(painter.DrawCircle(0, 0, 10));
}                                             // #5

int main(int argc, char** argv) {
  // The following line must be executed to initialize Google Mock
  // (and Google Test) before running the tests.
  ::testing::InitGoogleMock(&argc, argv);
  return RUN_ALL_TESTS();
}

正如你可能已经猜到的,这个测试检查PenDown()被调用至少一次。 若是painter对象没有调用此方法,您的测试将失败,并显示以下消息:

path/to/my_test.cc:119: Failure
Actual function call count doesn't match this expectation:
Actually: never called;
Expected: called at least once.

提示1:若是从Emacs缓冲区运行测试,您能够在错误消息中显示的行号上按<Enter>,直接跳到失败的预期。

提示2:若是你的mock object 历来没有被删除,最终的验证不会发生。所以,当您在堆上分配mock时,在测试中使用堆泄漏检查器是个好主意。

重要提示:Google Mock 须要expectation 在mock 函数被调用以前就设置,否者 行为就是 未定义的(undefined)。尤为是,你不能交错 EXPECT_CALL()和调用函数

这意味着EXPECT_CALL()应该被读取为指望call将在将来发生,而不是call已经发生。为何Google Mock会这样工做?

好的,事先指按期望容许Google Mock在上下文(堆栈跟踪等)仍然可用时当即报告违例。这使得调试更容易。

诚然,这个测试是设计的,没有作太多。不使用Google Mock,您也能够轻松实现相同的效果。然而,正如咱们将很快揭示的,Google Mock容许你作更多的。

Using Google Mock with Any Testing Framework

若是您要使用除Google测试(例如CppUnit或CxxTest)以外的其余测试框架做为测试框架,只需将上一节中的main()函数更改成:

int main(int argc, char** argv) {
  // The following line causes Google Mock to throw an exception on failure,
  // which will be interpreted by your testing framework as a test failure.
  ::testing::GTEST_FLAG(throw_on_failure) = true;
  ::testing::InitGoogleMock(&argc, argv);
  ... whatever your testing framework requires ...
}

这种方法有一个catch:它有时使Google Mock从一个模拟对象的析构器中抛出异常。对于某些编译器,这有时会致使测试程序崩溃。 你仍然能够注意到测试失败了,但它不是一个优雅的失败。

更好的解决方案是使用Google Test的事件侦听器APIevent listener API 来正确地向测试框架报告测试失败。 您须要实现事件侦听器接口的OnTestPartResult()方法,但它应该是直接的。

若是这证实是太多的工做,咱们建议您坚持使用Google测试,它与Google Mock无缝地工做(实际上,它在技术上是Google Mock的一部分)。 若是您有某个缘由没法使用Google测试,请告诉咱们。

Setting Expectations

成功使用Mock Object的关键是对它设置正确的指望。 若是你设置的指望太严格,你的测试将失败做为无关的更改的结果。 若是你把它们设置得太松,错误能够经过。 你想作的只是正确的,使你的测试能够捕获到你想要捕获的那种错误。 Google Mock为您提供了必要的方法“恰到好处”。

General Syntax

在 Google Mock 中咱们在 mock mecthod 中使用 EXPECT_CALL() 宏去设置expectation。 通常的语法是:

EXPECT_CALL(mock_object, method(matchers))
    .Times(cardinality)
    .WillOnce(action)
    .WillRepeatedly(action);

宏有两个参数:首先是mock对象,而后是方法及其参数。 请注意,二者之间用逗号(,)分隔,而不是句点(.)。 (为何要使用逗号?答案是,这是必要的技术缘由。)

宏以后能够是一些可选的子句,提供有关指望的更多信息。 咱们将在下面的章节中讨论每一个子句是如何工做的。

此语法旨在使指望读取如英语。 例如,你可能猜到

using ::testing::Return;
...
EXPECT_CALL(turtle, GetX())
    .Times(5)
    .WillOnce(Return(100))
    .WillOnce(Return(150))
    .WillRepeatedly(Return(200));

 turtle对象的GetX()方法将被调用五次,它将第一次返回100,第二次返回150,而后每次返回200。 有些人喜欢将这种语法风格称为域特定语言(DSL)。

注意:为何咱们使用宏来作到这一点? 它有两个目的:第一,它使预期容易识别(经过grep或由人类读者),其次它容许Google Mock在消息中包括失败的指望的源文件位置,使调试更容易。

 

Matchers: What Arguments Do We Expect?

当一个mock函数接受参数时,咱们必须指定咱们指望什么参数; 例如:

// Expects the turtle to move forward by 100 units.
EXPECT_CALL(turtle, Forward(100));

 有些时候你也许不想要太具体(记住,谈论测试太僵硬,超过规范致使脆弱的测试和模糊测试的意图,所以,咱们鼓励你只指定必要的 -很少也很多 ),若是你只关心 Forward() 会被调用,可是对 具体的参数不感兴趣,写_ 做为 参数,这意味“什么均可以”:

using ::testing::_;
...
// Expects the turtle to move forward.
EXPECT_CALL(turtle, Forward(_));

_是咱们称为匹配器的实例.匹配器就像一个谓词,能够测试一个参数是不是咱们指望的.你能够在EXPECT_CALL()里面使用一个匹配器来替换某一个参数。内置匹配器的列表能够在CheatSheet中找到。 例如,这里是Ge(大于或等于)匹配器:

using ::testing::Ge;
...
EXPECT_CALL(turtle, Forward(Ge(100)));

这检查,turtle将被告知前进至少100单位。

Cardinalities: How Many Times Will It Be Called?

咱们能够在EXPECT_CALL()以后指定的第一个子句是Times()。咱们把它的参数称为基数,由于它告诉调用应该发生多少次。它容许咱们重复一个指望屡次,而不实际写屡次。更重要的是,一个基数能够是“模糊的”,就像一个匹配器。这容许用户准确地表达测试的意图。

一个有趣的特殊状况是当咱们说Times(0)。你可能已经猜到了 - 这意味着函数不该该使用给定的参数,并且Google Mock会在函数被(错误地)调用时报告一个Google测试失败。咱们已经看到AtLeast(n)做为模糊基数的一个例子。有关您可使用的内置基数列表,请参见CheatSheet

Times()子句能够省略。若是你省略Times(),Google Mock会推断出你的基数。规则很容易记住:

  • 若是WillOnce()和WillRepeatedly()都不在EXPECT_CALL()中,则推断的基数是Times(1)。
  • 若是有n个WillOnce(),但没有WillRepeatedly(),其中n> = 1,基数是Times(n)
  • 若是有n个WillOnce()和一个WillRepeatedly(),其中n> = 0,基数是Times(AtLeast(n))。

 快速测验:若是一个函数指望被调用两次,但实际上调用了四次,你认为会发生什么?

Actions: What Should It Do?

记住,一个模拟对象实际上没有工做实现? 咱们做为用户必须告诉它当一个方法被调用时该作什么。 这在Google Mock中很容易。

首先,若是一个模拟函数的返回类型是内置类型或指针,该函数有一个默认动做(一个void函数将返回,一个bool函数将返回false,其余函数将返回0)。

此外,在C ++ 11及以上版本中,返回类型为默承认构造(即具备默认构造函数)的模拟函数具备返回默认构造值的默认动做。 若是你不说什么,这个行为将被使用。

第二,若是模拟函数没有默认动做,或者默认动做不适合你,你可使用一系列WillOnce()子句指定每次指望匹配时要采起的动做,后跟一个可选的WillRepeatedly ()。例如:

using ::testing::Return;
...
EXPECT_CALL(turtle, GetX())
    .WillOnce(Return(100))
    .WillOnce(Return(200))
    .WillOnce(Return(300));

这说明turtle.GetX()将被调用三次(Google Mock从咱们写的WillOnce()子句中推断出了这一点,由于咱们没有明确写入Times()),而且会返回100,200, 和300。

using ::testing::Return;
...
EXPECT_CALL(turtle, GetY())
    .WillOnce(Return(100))
    .WillOnce(Return(200))
    .WillRepeatedly(Return(300));

turtle.GetY()将被调用至少两次(Google Mock知道这一点,由于咱们写了两个WillOnce()子句和一个WillRepeatedly(),没有明确的Times()),将第一次返回100,200 第二次,300从第三次开始。

固然,若是你明确写一个Times(),Google Mock不会试图推断cardinality(基数)自己。 若是您指定的数字大于WillOnce()子句,该怎么办? 好了,毕竟WillOnce()已用完,Google Mock每次都会为函数执行默认操做(除非你有WillRepeatedly()。)。

除了Return()以外,咱们能够在WillOnce()中作什么? 您可使用ReturnRef(variable)返回引用,或调用预约义函数等。

重要说明:EXPECT_CALL()语句只评估一次操做子句,即便操做可能执行屡次。 所以,您必须当心反作用。 如下可能不会作你想要的:

int n = 100;
EXPECT_CALL(turtle, GetX())
.Times(4)
.WillRepeatedly(Return(n++));

不是连续返回100,101,102,...,这个mock函数将老是返回100,由于n ++只被计算一次。 相似地,当执行EXPECT_CALL()时,Return(new Foo)将建立一个新的Foo对象,而且每次都返回相同的指针。 若是你想要每次都发生反作用,你须要定义一个自定义动做,咱们将在 CookBook中教授。

另外一个测验! 你认为如下是什么意思?

using ::testing::Return;
...
EXPECT_CALL(turtle, GetY())
.Times(4)
.WillOnce(Return(100));

显然turtle.GetY()被指望调用四次。但若是你认为它会每次返回100,三思然后行!请记住,每次调用函数时都将使用一个WillOnce()子句,而后执行默认操做。因此正确的答案是turtle.GetY()将第一次返回100,但从第二次返回0,由于返回0是int函数的默认操做

Using Multiple Expectations

到目前为止,咱们只列出了你有一个指望的例子。更现实地,你要指定对多个模拟方法的指望,这可能来自多个模拟对象。

默认状况下,当调用模拟方法时,Google Mock将按照它们定义的相反顺序搜索指望值,并在找到与参数匹配的活动指望时中止(您能够将其视为“新规则覆盖旧的规则“)。若是匹配指望不能再接受任何调用,您将获得一个上限违反的失败。这里有一个例子:

using ::testing::_;
...
EXPECT_CALL(turtle, Forward(_));  // #1
EXPECT_CALL(turtle, Forward(10))  // #2
    .Times(2);

若是Forward(10)在一行中被调用三次,第三次它将是一个错误,由于最后的匹配指望(#2)已经饱和。然而,若是第三个Forward(10)被Forward(20)替换,则它将是OK,由于如今#1将是匹配指望。

附注:Google Mock为何要以与预期相反的顺序搜寻匹配?缘由是,这容许用户在模拟对象的构造函数中设置默认指望,或测试夹具的设置阶段中设置默认指望,而后经过在测试体中写入更具体的指望来定制模拟。因此,若是你对同一个方法有两个指望,你想把一个具备更多的特定的匹配器放在另外一个以后,或更具体的规则将被更为通常的规则所覆盖。

Ordered vs Unordered Calls

默认状况下,即便未知足较早的指望,指望也能够匹配调用。换句话说,调用没必要按照指望被指定的顺序发生

有时,您可能但愿全部预期的调用以严格的顺序发生。在Google Mock中说这很容易

using ::testing::InSequence;
...
TEST(FooTest, DrawsLineSegment) {
  ...
  {
    InSequence dummy;

    EXPECT_CALL(turtle, PenDown());
    EXPECT_CALL(turtle, Forward(100));
    EXPECT_CALL(turtle, PenUp());
  }
  Foo();
}

经过建立类型为InSequence的对象,其范围中的全部指望都被放入序列中,而且必须按顺序发生。由于咱们只是依靠这个对象的构造函数和析构函数作实际的工做,它的名字真的可有可无。

在这个例子中,咱们测试Foo()按照书写的顺序调用三个指望函数。若是调用是无序的,它将是一个错误。

若是你关心一些呼叫的相对顺序,但不是全部的呼叫,你能指定一个任意的部分顺序吗?答案是...是的!若是你不耐烦,细节能够在CookBook中找到。)

All Expectations Are Sticky (Unless Said Otherwise)

全部指望都是粘滞的(Sticky)(除非另有说明)

如今,让咱们作一个快速测验,看看你能够多好地使用这个模拟的东西。你会如何测试,turtle被要求去原点两次(你想忽略任何其余指令)?

在你提出了你的答案,看看咱们的比较的笔记(本身先解决 - 不要欺骗!):

using ::testing::_;
...
EXPECT_CALL(turtle, GoTo(_, _))  // #1
    .Times(AnyNumber());
EXPECT_CALL(turtle, GoTo(0, 0))  // #2
    .Times(2);

假设turtle.GoTo(0,0)被调用了三次。 第三次,Google Mock将看到参数匹配指望#2(记住,咱们老是选择最后一个匹配指望)。 如今,因为咱们说应该只有两个这样的调用,Google Mock会当即报告错误。 这基本上是咱们在上面“使用多个指望”部分中告诉你的。

这个例子代表,Google Mock的指望在默认状况下是“粘性”,即便在咱们达到其调用上界以后,它们仍然保持活动。 这是一个重要的规则要记住,由于它影响规范的意义,而且不一样于它在许多其余Mock框架中作的(为何咱们这样作?由于咱们认为咱们的规则使常见的状况更容易表达和 理解。)。

简单? 让咱们看看你是否真的理解它:下面的代码说什么?

using ::testing::Return;
...
for (int i = n; i > 0; i--) {
  EXPECT_CALL(turtle, GetX())
      .WillOnce(Return(10*i));
}

若是你认为它说,turtle.GetX()将被调用n次,并将返回10,20,30,...,连续,三思然后行! 问题是,正如咱们所说,指望是粘性的。 因此,第二次turtle.GetX()被调用,最后(最新)EXPECT_CALL()语句将匹配,并将当即致使“上限超过(upper bound exceeded)”错误 - 这段代码不是颇有用!

一个正确的说法是turtle.GetX()将返回10,20,30,...,是明确说,指望是不粘的。 换句话说,他们应该在饱和后尽快退休:

using ::testing::Return;
...
for (int i = n; i > 0; i--) {
  EXPECT_CALL(turtle, GetX())
    .WillOnce(Return(10*i))
    .RetiresOnSaturation();
}

并且,有一个更好的方法:在这种状况下,咱们指望调用发生在一个特定的顺序,咱们排列动做来匹配顺序。 因为顺序在这里很重要,咱们应该显示的使用一个顺序:

using ::testing::InSequence;
using ::testing::Return;
...
{
  InSequence s;

  for (int i = 1; i <= n; i++) {
    EXPECT_CALL(turtle, GetX())
        .WillOnce(Return(10*i))
        .RetiresOnSaturation();
  }
}

Uninteresting Calls

模拟对象可能有不少方法,并非全部的都是那么有趣。例如,在一些测试中,咱们可能不关心GetX()和GetY()被调用多少次。

在Google Mock中,若是你对一个方法不感兴趣,只是不要说什么。若是调用此方法,您将在测试输出中看到一个警告,但它不会失败。

What Now?

恭喜!您已经学会了足够的Google Mock开始使用它。如今,您可能想要加入googlemock讨论组,而且实际上使用Google Mock编写一些测试 - 这颇有趣。嘿,它甚至能够上瘾 - 你已经被警告。

而后,若是你想增长你的Mock商,你应该移动到 CookBook。您能够了解Google Mock的许多高级功能,并提升您的享受和测试幸福的水平。

相关文章
相关标签/搜索