TDD测试驱动学习

1 篇文章 0 订阅

gtest 和 gmock 安装

在这里插入图片描述

在这里插入图片描述


//如果不知道对应库名字可以执行这个命令查找对应库,如果没找到要去跟新对应的源sudo apt update
sudo apt-cache search gtest 
sudo apt-cache search gmock 

测试例子


#include <string>
using std::string;

// 定义 Soundex 类
class Soundex {
  public:
   // 对给定字符串进行编码
   string encode(const string& str) const { return zeroPad(str); }

  private:
   // 在字符串末尾填充零,使其达到一定长度
   string zeroPad(const string& word) const { return word + "000"; }
};

#include "gmock/gmock.h"
#include "gtest/gtest.h"

// 定义 SoundexEncoding 测试套件,用于测试 Soundex 类的编码功能
TEST(SoundexEncoding, RetainSoleLetterOfOneLetterWord) {
   // 创建一个 Soundex 对象
   Soundex soundex;
   // 对单个字母进行编码
   auto encoded = soundex.encode("A");
   // 断言编码后的结果与期望值相等
   ASSERT_THAT(encoded, testing::Eq("A"));
}

// 主函数
int main() {
   // 初始化 Google Test 框架
   testing::InitGoogleTest();
   // 运行所有测试
   return RUN_ALL_TESTS();
}


TDD 的三条规则是:

  1. 在编写新功能之前,先编写测试。
  2. 仅编写足以使测试 Pass 的代码。当测试已经通过时,不要写更多的代码。
  3. 重构现有的代码以消除重复和提高可读性

MATCHER_P 宏定义自定义匹配器


// 定义一个名为 IsEven 的自定义匹配器
MATCHER_P(IsEven, "", "") { // 匹配器名称为 "IsEven",没有自定义描述和失败消息
    return (arg % 2) == 0; // 如果 arg 是偶数,则匹配成功
}

// 创建测试用例,验证偶数是否匹配自定义匹配器
TEST(MyTest, TestEvenNumbers) {
    int num = 4;
    EXPECT_THAT(num, IsEven()); // 使用 EXPECT_THAT 宏验证 num 是否匹配 IsEven 匹配器
}

// 创建测试用例,验证奇数是否不匹配自定义匹配器
TEST(MyTest, TestOddNumbers) {
    int num = 7;
    EXPECT_THAT(num, Not(IsEven())); // 使用 EXPECT_THAT 和 Not 宏,验证 num 是否与 IsEven 匹配器的结果相反
}

–gtest_filter

是该 --gtest_filter 是该工具中的一个参数选项,用于过滤 Google Mock 测试用例。工具中的一个参数选项,用于过滤 Google Mock 测试用例。
假设我们有一个测试用例集 TestSuite,其中包含以下测试用例:


TEST(TestSuite, Test1) {}
TEST(TestSuite, Test2) {}
TEST(TestSuite, AnotherTest) {}

如果我们只想运行 Test1 和 AnotherTest 这两个测试用例,可以使用以下命令:


./mytest --gtest_filter="TestSuite.Test1|TestSuite.AnotherTest"

这个命令会运行 Test1 和 AnotherTest,而忽略 Test2。

ASSERT_THAT


string actual = string("al") + "pha";
//判断实际值actual是否与期望值“alpha”相等
//如果实际值等于期望值,则通过测试 
ASSERT_THAT(actual, Eq("alpha"));

EXPECT_CALL

用法:

  1. Match 期望的参数值 可以使用MATCHER宏定义一个匹配器,用于期望特定的参数值。例如

MATCHER_P(IsEqualToString, expected, "") { return arg == expected; }
EXPECT_CALL(mockObject, someMethod(IsEqualToString("expectedValue")));

在这个例子中,我们定义了一个IsEqualToString匹配器,用于期望someMethod方法的参数等于"expectedValue"。然后,我们将这个期望传递给EXPECT_CALL宏。

  1. Times 期望方法调用的次数 可以使用Times宏指定期望方法被调用的精确次数,例如:

EXPECT_CALL(mockObject, someMethod()).Times(3);

在这个例子中,我们期望someMethod方法被调用3次

  1. WillOnce / WillRepeatedly 模拟返回值可以使用WillOnce宏指定一次期望调用的返回值,例如:

EXPECT_CALL(mockObject, someMethod()).WillOnce(Return(1));

在这个例子中,我们期望someMethod方法被调用一次,并返回1。可以使用WillRepeatedly宏指定多次期望调用的返回值,例如:


EXPECT_CALL(mockObject, someMethod()).WillRepeatedly(Return(1));

在这个例子中,我们期望someMethod方法被多次调用,并返回1

  1. InSequence 指定方法调用顺序 可以使用InSequence宏指定期望方法调用的顺序,例如:

testing::InSequence sequence;
EXPECT_CALL(mockObject, someMethod1());
EXPECT_CALL(mockObject, someMethod2());

在这个例子中,我们期望someMethod1方法在someMethod2方法之前被调用。
这些是其中的一些用法,EXPECT_CALL还有其他用法,可以根据需要选择适合的方式。

是Google Mock测试框架提供的一个宏,指定这些函数在被调用时的动作。


#include <gmock/gmock.h>
#include <gtest/gtest.h>

class HttpClient {
  public:
   // 基类HttpClient定义了虚析构函数,以使子类在释放内存时能够正确析构
   virtual ~HttpClient() {}
   // 纯虚函数sendRequest,用于在子类中实现发送HTTP请求的具体逻辑
   virtual void sendRequest(const std::string& url) = 0;
};
// 模拟HttpClient,实现了sendRequest方法,用于测试MyHttpClient
class MockHttpClient : public HttpClient {
  public:
   // 使用MOCK_METHOD宏定义sendRequest方法
   MOCK_METHOD(void, sendRequest, (const std::string& url), (override));
};

class MyHttpClient {
  public:
   // 构造函数,将HttpClient对象作为依赖项传递给MyHttpClient,存储在httpClient_成员变量中
   MyHttpClient(HttpClient* httpClient)
       : httpClient_(httpClient) {}
   // 发送请求到期望的URL的方法
   void sendHttpRequestToExpectedURL(const std::string& url) {
      // 通过httpClient_成员变量调用HttpClient对象的sendRequest方法,将url作为参数,发送HTTP请求
      httpClient_->sendRequest(url);
   }

  private:
   // HttpClient对象指针
   HttpClient* httpClient_;
};

// 测试用例开始
TEST(MyHttpClientTest, SendsHttpRequestToCorrectUrl) {
   // 定义期望的URL
   std::string expectedURL = "https://example.com/test";
   // 创建MockHttpClient实例,用于模拟HttpClient对象
   MockHttpClient mockHttpClient;
   // 创建使用HttpClient对象的实例,将其传递给MyHttpClient的构造函数
   MyHttpClient httpClient(&mockHttpClient);

   // 设置期望:MockHttpClient的sendRequest方法将以expectedURL作为参数被调用
   EXPECT_CALL(mockHttpClient, sendRequest(expectedURL));

   // 调用应该发送请求到expectedURL的方法
   // 调用httpClient对象的sendHttpRequestToExpectedURL方法,将expectedURL作为参数
   httpClient.sendHttpRequestToExpectedURL(expectedURL);
}

// 定义 main 函数
int main(int argc, char** argv) {
   testing::InitGoogleMock(&argc, argv);  // 初始化 Google Mock 并运行所有测试
   return RUN_ALL_TESTS();
}

浮点数的比较

ULP 指Unit in the Last Place, 即表示连续浮点数之间的最小差异量,如果两个浮点数a和b,如果他们的差小于ULP
|a-b| < ULP
我们认为他们相等


double expected = 0.1;
double actual = 0.2 * 0.5;
// 使用ULP差异比较浮点数
EXPECT_FLOAT_EQ(expected, actual);          // 这里会失败,因为ULP差异大于0

// 使用相对/绝对误差范围比较浮点数
EXPECT_NEAR(expected, actual, 0.0001);      // 这里会通过,因为绝对误差和相对误差均在0.0001之内

//使用ASSERT_THAT()和DoubleEq()宏对变量x和y的和进行浮点数比较
//如果x+y与期望值4.56之间的ULP差异小于某个值,则测试通过,否则测试失败
ASSERT_THAT(x + y, DoubleEq(4.56));

基于异常测试


#include <gmock/gmock.h>
#include <gtest/gtest.h>

using namespace testing;

class Calculator {
  public:
   virtual int Divide(int dividend, int divisor) = 0;
};

// 定义一个 MockCalculator 类,继承自 Calculator 类,
// 并使用 MOCK_METHOD 宏定义了一个名为 Divide 的虚方法
class MockCalculator : public Calculator {
  public:
   MOCK_METHOD(int, Divide, (int, int), (override));
};

// 定义测试用例,名称为 CalculatorTest,测试除数为零的情况
TEST(CalculatorTest, DivideByZero) {
   // 创建一个 MockCalculator 实例
   MockCalculator mockCalculator;

   // 定义一个期望,表示调用 Divide 方法并传入除数为0的参数时,
   // 应该抛出 std::logic_error 异常,并将异常信息设置为 "Division by zero"
   EXPECT_CALL(mockCalculator, Divide(_, 0))
       .WillOnce(Throw(std::logic_error("Division by zero")));

   // 使用 ASSERT_THROW 宏检查调用 Divide 方法并传入参数 10 和 0 时
   // 是否抛出 std::logic_error 异常,并将异常信息与 "Division by zero" 进行比较
   ASSERT_THROW(mockCalculator.Divide(10, 0), std::logic_error);
}

// 主函数
int main() {
   // 初始化 Google Test 框架
   InitGoogleTest();
   // 运行所有测试
   return RUN_ALL_TESTS();
}

ASSERT_ANY_THROW 这个会检查是不是剖出异常如果抛出,测试通过


#include <gmock/gmock.h>
#include <gtest/gtest.h>

// 定义一个函数 Divide,接受两个整数参数 x 和 y,将它们相除的结果保存到 result 中
void Divide(int x, int y) {
   // 如果除数 y 等于 0,则抛出 std::logic_error 异常
   if (y == 0) {
      throw std::logic_error("Division by zero");
   }
   int result = x / y;
   // ...
}

// 定义一个测试用例 ExceptionTest,测试 Divide 函数是否能够正确地抛出异常
TEST(ExceptionTest, AnyException) {
   // 使用 ASSERT_ANY_THROW 宏检查 Divide(10, 0) 是否抛出任何类型的异常
   ASSERT_ANY_THROW(Divide(10, 0));
   try {
      // 如果 Divide 函数抛出异常,会被 catch 块捕获
      Divide(10, 0);
      FAIL() << "Expected std::logic_error";
   } catch (const std::logic_error& e) {
      EXPECT_STREQ("Division by zero", e.what());
   } catch (...) {
      FAIL() << "Expected std::logic_error";
   }
}

// 主函数
int main() {
   // 初始化 Google Test 框架
   testing::InitGoogleTest();
   // 运行所有测试
   return RUN_ALL_TESTS();
}


参数化测试

在软件开发过程中,测试(testing)和测试驱动开发(test-driving)都可以使用参数化测试(parameterized tests)等工具来增强测试的效果。测试(testing)是在开发完成后进行的测试,而测试驱动开发(test-driving)则是在开发过程中通过测试来驱动代码的开发。


#include <gtest/gtest.h>
#include <vector>
#include <tuple>
#include "calculator.hpp"

using namespace std;

// 定义一个测试用例,测试 Add() 方法是否能正确计算加法
class AddTest : public testing::TestWithParam<tuple<int, int, int>> {
};

// 使用 TEST_P 宏定义一个参数化测试,测试 Add() 方法是否能正确计算加法
TEST_P(AddTest, TestAdd) {
   // 获取传入的参数
   int x = get<0>(GetParam());
   int y = get<1>(GetParam());
   int expected_result = get<2>(GetParam());

   // 创建一个 Calculator 类对象,并调用其 Add() 方法计算加法
   Calculator calc;
   int actual_result = calc.Add(x, y);

   // 断言计算结果是否与预期结果一致
   ASSERT_EQ(expected_result, actual_result);
}

// 在 INSTANTIATE_TEST_CASE_P 宏中传递参数,为参数化测试提供测试数据
INSTANTIATE_TEST_CASE_P(TestAddInstantiation, AddTest, testing::Values(
   // 测试用例1:两个正整数相加
   make_tuple(2, 3, 5),
   // 测试用例2:两个负整数相加
   make_tuple(-2, -3, -5),
   // 测试用例3:一个正整数和一个负整数相加
   make_tuple(2, -3, -1),
   // 测试用例4:一个正整数和零相加
   make_tuple(2, 0, 2),
   // 测试用例5:两个零相加
   make_tuple(0, 0, 0)
));

int main(int argc, char **argv) {
   testing::InitGoogleTest(&argc, argv);
   return RUN_ALL_TESTS();
}

Test Doubles

是一个测试技术、概念,用于在单元测试环境中替代软件系统的依赖组件,以便更容易地测试软件系统。Test Doubles 旨在消除测试过程中的不确定性,使测试的结果更可靠。
Test Doubles 有多种类型,包括:
Dummy Object:没有实际实现的占位符对象,仅用于填充方法和参数。
Test Stubs: 测试桩它用于在测试期间提供预定义的数据或行为,以便测试系统的某些行为。


// 假设我们有一个 User 类,依赖于一个 Database 对象
class Database {
public:
    bool save(int id, std::string name) {
        std::cout << "Save data to the database" << std::endl;
        return true;
    }
};

class User {
private:
    int id;
    std::string name;
    Database* db;

public:
    User(int id, std::string name, Database* db) {
        this->id = id;
        this->name = name;
        this->db = db;
    }

    bool save() {
        std::cout << "Save User data" << std::endl;
        // 使用依赖的 Database 对象完成持久化操作
        bool result = db->save(id, name);
        return result;
    }
};

// Test Stub
class StubDatabase : public Database {
public:
    // 重写 save 函数以提供预定义行为
    bool save(int id, std::string name) override {
        std::cout << "Save data to the test database" << std::endl;
        return true;
    }
};

// 测试 User 的 save 函数
int main() {
    StubDatabase stubDb;
    User user(1, "testUser", &stubDb);
    bool result = user.save();
    std::cout << "Save operation result: " << result << std::endl;
    return 0;
}

Test Spy 可以用于在测试环境中记录被调用的函数或方法的信息,以便在之后验证测试对象的行为。


#include <iostream>
#include <string>
#include <vector>

class Logger {
  public:
   virtual void write_message(const std::string& message) {
      // 记录系统事件的写入操作
      std::cout << "[Logger] " << message << std::endl;
   }

   void log_event(const std::string& event) {
      std::string message = "Event: " + event;
      write_message(message);
   }
};

// Test Spy
class LoggerSpy : public Logger {
  public:
   std::vector<std::string> messages;

   void write_message(const std::string& message) override {
      messages.push_back(message);
   }
};

// 测试 log_event 函数是否能够在 write_message 被调用时记录正确的消息
int main() {
   LoggerSpy spy;

   // 调用 Logger 类的方法
   spy.log_event("test event");

   // 验证消息是否被正确记录
   if (spy.messages.size() != 1 || spy.messages[0] != "Event: test event") {
      std::cout << "Test failed: incorrect message recorded" << std::endl;
      return 1;
   }

   std::cout << "Test passed" << std::endl;
   return 0;
}

Mock Objects 是模拟对象,用于在测试环境中代替依赖对象。它们是具有预定义行为的测试 double 类型,用于在测试中代替或模拟依赖项以便测试对象。Mock Objects 通常用于测试高度耦合的对象,它们提供了强大的控制和验证测试对象的方式。


// PaymentGateway 类定义
class PaymentGateway {
public:
    virtual bool process_payment(int amount) = 0;
};

// ShoppingCart 类定义
class ShoppingCart {
public:
    ShoppingCart(PaymentGateway& gateway) : gateway(gateway) {}

    bool checkout(int amount) {
        // 调用外部依赖 PaymentGateway
        return gateway.process_payment(amount);
    }

private:
    PaymentGateway& gateway;
};

// PaymentGatewayMock 类定义
class PaymentGatewayMock : public PaymentGateway {
public:
    bool process_payment(int amount) override {
        // 返回失败状态
        return false;
    }
};

// 测试购物车在付款失败的情况下的行为
int main() {
    PaymentGatewayMock mock;
    ShoppingCart cart(mock);
    bool checkout_result = cart.checkout(100);

    // 验证购物车应该返回 false
    if (checkout_result != false) {
        std::cout << "Test failed: incorrect checkout result" << std::endl;
        return 1;
    }

    std::cout << "Test passed" << std::endl;
    return 0;
}

下面创建一个SoundexEncoding fixture 类


#include <string>
#include "gmock/gmock.h"
#include "gtest/gtest.h"

using std::string;

// 定义 Soundex 类
class Soundex {
  public:
   // 对给定字符串进行编码
   string encode(const string& str) const { return zeroPad(str); }

  private:
   // 在字符串末尾填充零,使其达到一定长度
   string zeroPad(const string& word) const { return word + "000"; }
};

// 定义一个名为SoundexEncoding的测试夹具,派生自::testing::Test类
class SoundexEncoding : public testing::Test {
  public:
   Soundex soundex;  // 声明一个公共的Soundex类型的成员变量soundex
};

// 定义一个测试用例,名称为RetainsSoleLetterOfOneLetterWord,使用SoundexEncoding夹具
TEST_F(SoundexEncoding, RetainsSoleLetterOfOneLetterWord) {
   // 调用soundex实例的encode方法,并将结果保存到变量encoded中
   auto encoded = soundex.encode("A");
   // 验证encoded是否与"A000"相等,如果不相等则测试失败
   ASSERT_THAT(encoded, testing::Eq("A000"));
}

// 定义一个测试用例,名称为PadsWithZerosToEnsureThreeDigits,使用SoundexEncoding夹具
TEST_F(SoundexEncoding, PadsWithZerosToEnsureThreeDigits) {
   // 调用soundex实例的encode方法,并将结果保存到变量encoded中
   auto encoded = soundex.encode("I");
   // 验证encoded是否与"I000"相等,如果不相等则测试失败
   ASSERT_THAT(encoded, testing::Eq("I000"));
}

// 主函数
int main() {
   // 初始化 Google Test 框架
   testing::InitGoogleTest();
   // 运行所有测试
   return RUN_ALL_TESTS();
}

测试的结果都pass,在执行RetainsSoleLetterOfOneLetterWord和PadsWithZerosToEnsureThreeDigits 之前都会创建一个单独的SoundexEncoding 实例,为了自定义Fixture 需要将宏TEST改成TEST_F,其中的F 代表的是“Fixture”,如果没有使用TEST_F 会不能访问soundex 这个成员。
在这里插入图片描述

EXPECT_THAT 宏在断言失败时记录失败,并继续执行测试,因此如果存在多个断言,即使第一个断言失败,其他断言也将被执行。这使您能够查看测试失败时的完整情况。
SSERT_THAT 宏与 EXPECT_THAT 不同,如果断言失败,它将立即将测试标记为失败并停止执行,因此如果存在多个断言,当第一个断言失败时,后续断言将不会执行。
DISABLED_前缀禁用测试


TEST_F(SoundexEncoding, DISABLED_ReplacesMultipleConsonantsWithDigits) {
ASSERT_THAT(soundex.encode("Acdl"), Eq("A234"));
}

gtest_catch_exceptions 出现异常试工具会捕获这个异常,报告测试失败,并会继续运行任何后续测试


[ RUN      ] SoundexEncoding.LimitsLengthToFourCharacters
unknown file: Failure
C++ exception with description "basic_string::_M_create" thrown in the test body.
[  FAILED  ] SoundexEncoding.LimitsLengthToFourCharacters (31465 ms)
[----------] 5 tests from SoundexEncoding (31465 ms total)

Google Mock 会忽略未捕获的异常并继续运行其余的测试。如果您希望在未捕获的异常时终止测试,可以使用以下命令行选项来运行 Google Mock:


--gtest_catch_exceptions=0

Teardown()Test 类的一个虚函数。作为 Google Test 的扩展部分,gMock 的 Test 类提供了额外的特性和语法糖。在使用 gMock 进行单元测试时,可以通过重载 Teardown() 函数来进行某些清理工作。

test double 例子

代码需要安装对应jsoncpp curl库


sudo apt-cache search jsoncpp //如果不知道对应库名字可以执行这个命令查找对应库,如果没找到要去跟新对应的源,sudo apt update
sudo apt-install libjsoncpp-dev 
sudo apt-cache search curl
sudo apt-install libcurl4-gnutls-dev

实现一个从地图上获取对应地址信息的测试。

测试 double HTTP 实现!测试 double 的工作是支持测试的需求。当客户端向 HTTP 对象发送 GET 请求时 test double 可以返回一个预先定义的地图上对应的地址信息响应。
要实现这个下面定义几个class

  1. CurlHttp:使用cURL库进行 HTTP 请求的类。它派生自抽象基类 Http,该类定义了 get() 和 initialize() 两个函数。在调用 get() 函数之前,客户端必须先调用 initialize() 函数进行初始化。

// 使用 libcurl 定义一个具体的 Http 实现类
class CurlHttp : public Http {
  public:
   CurlHttp()
       : curl(NULL){};
   virtual ~CurlHttp() {
      curl_global_cleanup();  // 在使用 libcurl 后进行清理
   }

   void initialize() {
      curl_global_init(CURL_GLOBAL_ALL);                                        // 初始化 libcurl
      curl = curl_easy_init();                                                  // 创建一个 curl 句柄
      curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlHttp::writeCallback);  // 设置响应数据回调函数
   }
   virtual string get(const string& url) const {
      response_ = "invalid request";                     // 初始化响应字符串
      curl_easy_setopt(curl, CURLOPT_URL, url.c_str());  // 设置请求的
      curl_easy_perform(curl);                           // 发送请求
      curl_easy_cleanup(curl);                           // 清理 curl 句柄

      return CurlHttp::Response();  // 返回响应字符串
   }
   static string Response() {
      return response_;
   }
   static size_t writeCallback(const char* buf, size_t size, size_t nMemb, void*) {
      for (auto i = 0u; i < size * nMemb; i++)  // 将响应数据添加到响应字符串中
         response_.push_back(buf[i]);
      return size * nMemb;
   }

  private:
   CURL* curl;

   static string response_;
};


  1. Address: 一个结构体,包含几个字段。地图返回的地址信息。

// 定义一个用于保存 Address 的结构体
struct Address {
   string road;     // 街道
   string city;     // 城市
   string state;    // 州/省
   string country;  // 国家
};

  1. AddressExtractor:使用JsonCpp库从JSON地址信息字符串中填充Address结构体的类。

// 定义一个用于从 JSON 字符串中提取 Address 的类
class AddressExtractor {
  public:
   Address addressFrom(const string& json) const {
      Address address;
      Value jsonAddress{jsonAddressFrom(json)};  // 解析 JSON 并提取 address 字段
      populate(address, jsonAddress);            // 将 JSON 中的各个字段填充到 Address 结构体中
      return address;
   }

  private:
   Json::Value jsonAddressFrom(const string& json) const {
      auto location = parse(json);                  // 解析 JSON 字符串
      return location.get("address", Value::null);  // 返回 JSON 对象中的 address 字段,如果不存在则返回 null
   }
   void populate(Address& address, Json::Value& jsonAddress) const {
      address.road = getString(jsonAddress, "road");  // 分别将 Address 结构体的各个字段填充为 JSON 对象中对应的字段值
      address.city = getString(jsonAddress, "city");
      address.state = getString(jsonAddress, "state");
      address.country = getString(jsonAddress, "country");
   }
   Json::Value parse(const string& json) const {
      Value root;
      Reader reader;
      reader.parse(json, root);  // 解析 JSON 字符串并将结果存入 root 变量中
      return root;
   }
   string getString(Json::Value& result, const string& name) const {
      return result.get(name, "").asString();  // 返回指定字段的字符串值,如果不存在则返回空字符串
   }
};

  1. PlaceDescriptionService: createGetRequestUrl 函数通过经纬度去,创建get请求。get 通过URL去调用获取对应address 信息。 summaryDescription 通过返回的json 信息解析出对应的地址信息。

// 定义了一个 PlaceDescriptionService 类,提供了获取地点描述信息的服务
class PlaceDescriptionService {
  public:
   // 构造函数,初始化 http 对象
   PlaceDescriptionService(Http* http);
   // 获取某个经纬度对应地点的简要描述
   string summaryDescription(const string& latitude, const string& longitude) const {
      // 根据经纬度组成 GET 请求的 URL
      auto getRequestUrl = "lat=" + latitude + "&lon=" + longitude;

      // 发送 GET 请求并获取响应结果
      auto jsonResponse = http_->get(getRequestUrl);

      // 从响应结果中提取出地址信息
      AddressExtractor extractor;
      auto address = extractor.addressFrom(jsonResponse);

      // 组合成简要描述并返回
      return address.road + ", " + address.city + ", " +
             address.state + ", " + address.country;
   }
  private:
   // 创建 HTTP GET 请求的 URL
   string createGetRequestUrl(const string& latitude, const string& longitude) const;
   // 根据地址信息生成简要描述
   string summaryDescription(const Address& address) const;
   Http* http_;  // HTTP 请求处理对象
};

可以实现成为下面的代码


// 创建一个 CurlHttp 类的实例,并调用其 initialize() 函数进行初始化
CurlHttp http;
http.initialize();

// 发送 GET 请求,并将响应存储在 jsonResponse 变量中,根据经纬度获取对应的消息
auto jsonResponse = http.get(createGetRequestUrl(latitude, longitude));

// 创建一个 AddressExtractor 类的实例,并从 JSON 字符串中提取 Address
AddressExtractor extractor;
auto address = extractor.addressFrom(jsonResponse);

// 对提取出的 Address 进行处理,并返回一个字符串作为摘要
return summaryDescription(address);

测试AddressExtractor 地址提取器


// 定义一个测试类 AnAddressExtractor,继承自 testing::Test
class AnAddressExtractor : public testing::Test {
  public:
   // 定义一个 AddressExtractor 对象 extractor
   AddressExtractor extractor;
};

// 定义一个参数匹配器 IsEmpty,用于匹配 Address 对象是否为空
MATCHER(IsEmpty, "") {
   return arg.road.empty() &&   // 道路为空
          arg.city.empty() &&   // 城市为空
          arg.state.empty() &&  // 州/省/地区为空
          arg.country.empty();  // 国家为空
}

// 定义测试用例
TEST_F(AnAddressExtractor, ReturnsAnEmptyAddressOnAFailedParse) {
   // 调用 AddressExtractor 的 addressFrom 方法,传入一个非法的 json 字符串
   auto address = extractor.addressFrom("not valid json");
   // 断言:address 对象应该为空
   ASSERT_THAT(address, IsEmpty());
}

// 定义测试用例
TEST_F(AnAddressExtractor, ReturnsPopulatedAddressForValidJsonResult) {
   // 定义一个 json 字符串
   auto json = R"({
         "place_id":"15331615",
         "address":{
            "road":"War Eagle Court",                 // 道路
            "city":"Colorado Springs",                // 城市
            "state":"Colorado",                       // 州/省/地区
            "country":"United States of America",     // 国家
         }
      })";

   // 调用 AddressExtractor 的 addressFrom 方法,传入 json 字符串
   auto address = extractor.addressFrom(json);

   // 断言:Address 对象的属性应该和 json 字符串中的值一致
   ASSERT_THAT(address.road, testing::Eq("War Eagle Court"));
   ASSERT_THAT(address.city, testing::Eq("Colorado Springs"));
   ASSERT_THAT(address.state, testing::Eq("Colorado"));
   ASSERT_THAT(address.country, testing::Eq("United States of America"));
}

使用Mock 工具进行测试

用测试驱动测试summaryDescription(),模拟HTTP方法get()和initialize()


//http.h
virtual ~Http() {}
virtual void initialize() = 0;
virtual std::string get(const std::string& url) const = 0;

定义派生类


// 定义纯虚函数 get(),用于向指定的 url 发送 get 请求,并返回响应结果
class HttpStub : public Http {
  public:
   // 定义纯虚函数 get(),用于向指定的 url 发送 get 请求,并返回响应结果,返回值为 void
   MOCK_METHOD0(initialize, void());
   // 定义一个带有一个参数的 const 成员函数 ,传入参数为 url 字符串,返回值为响应字符串
   MOCK_CONST_METHOD1(get, string(const string&));
};

Google Mock 为该函数合成一种实现方式以管理与之交互的操作
ValidLatitude和ValidLongitude封装到APlaceDescriptionService的目的是为了方便测试,以及避免代码重复。将这些值封装到测试夹具类中,可以方便地在测试用例中使用,同时也避免了多次重复定义这些常量。此外,这样也使得修改这些常量值时更加方便,只需要在测试夹具类中修改即可。



// 这是一个 PlaceDescriptionService 的测试夹具类定义,继承了 testing::Test,由 Google Test 框架提供
class APlaceDescriptionService : public testing::Test {
  public:  // 定义静态常量字符串值,表示有效的纬度和经度
   static const string ValidLatitude;
   static const string ValidLongitude;
};

// 定义静态常量字符串值
const string APlaceDescriptionService::ValidLatitude("38.005");
const string APlaceDescriptionService::ValidLongitude("-104.44");

// 定义测试用例方法
// 它属于 APlaceDescriptionService 的测试夹具类
TEST_F(APlaceDescriptionService, MakesHttpRequestToObtainAddress) {
   // 实例化 HttpStub 对象,用于模拟
   HttpStub httpStub;
   // 定义我们期望的 URL 字符串的开头
   string urlStart{"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"};
   // 用静态常量值和 httpStub 拼接成期望的 URL 字符串
   auto expectedURL = urlStart +
                      "lat=" +
                      APlaceDescriptionService::ValidLatitude +
                      "&" +
                      "lon=" +
                      APlaceDescriptionService::ValidLongitude;
   // 断言 httpStub 的 get 方法将使用期望的 URL 作为参数调用
   EXPECT_CALL(httpStub, get(expectedURL));
   // 使用 httpStub 实例化 PlaceDescriptionService 对象
   PlaceDescriptionService service{&httpStub};
   // 使用静态常量值调用 PlaceDescriptionService 对象的 summaryDescription 方法
   string Addressinfo = service.summaryDescription(ValidLatitude, ValidLongitude);
}

EXPECT_CALL(httpStub, get(expectedURL)); 这宏配置Google Mock以验证是否将get()获取得消息发送给httpStub对象。
由于PlaceDescriptionService class 中的 http_->get(getRequestUrl); 的getRequestUrl与期望的expectedURL不一样测试会出现错误


// 获取某个经纬度对应地点的简要描述
string summaryDescription(const string& latitude, const string& longitude) const {
   // 根据经纬度组成 GET 请求的 URL
   auto getRequestUrl = "lat=" + latitude + "&lon=" + longitude; 

   // 发送 GET 请求并获取响应结果
   auto jsonResponse = http_->get(getRequestUrl); //getRequestUrl与期待值不同

   // 从响应结果中提取出地址信息
   AddressExtractor extractor;
   auto address = extractor.addressFrom(jsonResponse);

   // 组合成简要描述并返回
   return address.road + ", " + address.city + ", " +
          address.state + ", " + address.country;
}

在这里插入图片描述

现在重写一下summaryDescription代码


// 获取某个经纬度对应地点的简要描述
string summaryDescription(const string& latitude, const string& longitude) const {
   string server{"http://open.mapquestapi.com/"};
   string document{"nominatim/v1/reverse"};
   string url = server + document + "?" +
                keyValue("format", "json") + "&" +
                keyValue("lat", latitude) + "&" +
                keyValue("lon", longitude);
   http_->get(url);
   return "";
}
string keyValue(const string& key,
                const string& value) const {
   return key + "=" + value;
}

现在对get() 的返回值进行测试

测试中的EXPECT_CALL()调用告诉Google Mock调用get()时应该返回什么值。测试中的 EXPECT_CALL() 调用告诉 Google Mock 调用get()时应该返回什么值。应该返回一个特定的JSON字符串值。我们不关心 get() 中传递的参数是什么,因为我们已经在 MakesHttpRequestToObtainAddress 中验证了这个行为。我们的 EXPECT_CALL() 使用了通配符匹配器get() 的参数。


// 获取某个经纬度对应地点的简要描述
string summaryDescription(const string& latitude, const string& longitude) const {
   string server{"http://open.mapquestapi.com/"};
   string document{"nominatim/v1/reverse"};
   string url = server + document + "?" +
                keyValue("format", "json") + "&" +
                keyValue("lat", latitude) + "&" +
                keyValue("lon", longitude);
   //添加下面代码对get函数的返回值进行测试
   auto response = http_->get(url);
   AddressExtractor extractor;
   auto address = extractor.addressFrom(response);
   return address.summaryDescription();
}

编写测试的代码


// TEST_F 宏用来定义一个测试用例,并使用 APlaceDescriptionService 类作为测试的 Fixture
TEST_F(APlaceDescriptionService, FormatsRetrievedAddressIntoSummaryDescription) {
   // 创建 HttpStub 对象,并设置 EXPECT_CALL,告诉 Google Mock 当调用 get() 时应该返回什么值。
   HttpStub httpStub;
   EXPECT_CALL(httpStub, get(testing::_))
       .WillOnce(testing::Return(
           R"({ "address": {
              "road":"Drury Ln",
              "city":"Fountain",
              "state":"CO",
              "country":"US" }})"));
   // 用 HttpStub 对象创建 PlaceDescriptionService 对象。
   PlaceDescriptionService service(&httpStub);
   // 调用 service.summaryDescription(),获取经纬度对应的地址简述信息。
   auto description = service.summaryDescription(ValidLatitude, ValidLongitude);
   // 将获取到的地址简述信息与期望值 "Drury Ln, Fountain, CO, US" 进行比较。
   ASSERT_THAT(description, testing::Eq("Drury Ln, Fountain, CO, US"));
}

指定get 的返回值然后通过ASSERT_THAT 进行判断。

对initialize 函数测试

对initialize 函数测试,在MakesHttpRequestToObtainAddress,中的EXPECT_CALL(httpStub, get(expectedURL)); 之前添加一行EXPECT_CALL(httpStub, initialize()); 确保在测试 http_stub.get(address) 方法之前,httpStub.initialize() 方法被调用了


// 定义测试用例方法
// 它属于 APlaceDescriptionService 的测试夹具类
TEST_F(APlaceDescriptionService, MakesHttpRequestToObtainAddress) {
   // 实例化 HttpStub 对象,用于模拟
   HttpStub httpStub;
   // 定义我们期望的 URL 字符串的开头
   string urlStart{"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"};
   // 用静态常量值和 httpStub 拼接成期望的 URL 字符串
   auto expectedURL = urlStart +
                      "lat=" +
                      APlaceDescriptionService::ValidLatitude +
                      "&" +
                      "lon=" +
                      APlaceDescriptionService::ValidLongitude;

   EXPECT_CALL(httpStub, initialize());
   // 断言 httpStub 的 get 方法将使用期望的 URL 作为参数调用
   EXPECT_CALL(httpStub, get(expectedURL));
   // 使用 httpStub 实例化 PlaceDescriptionService 对象
   PlaceDescriptionService service{&httpStub};
   // 使用静态常量值调用 PlaceDescriptionService 对象的 summaryDescription 方法
   string addressinfo1 = service.summaryDescription(ValidLatitude, ValidLongitude);
   cout << addressinfo1 << endl;
}

在这里插入图片描述

出现这个问题我们需要在PlaceDescriptionService 中的summaryDescription 函数中auto response = http_->get(url); 之前对initialize() 进行调用


string summaryDescription(const string& latitude, const string& longitude) const {
   string server{"http://open.mapquestapi.com/"};
   string document{"nominatim/v1/reverse"};
   string url = server + document + "?" +
                keyValue("format", "json") + "&" +
                keyValue("lat", latitude) + "&" +
                keyValue("lon", longitude);
   http_->initialize();
   auto response = http_->get(url);
   AddressExtractor extractor;
   auto address = extractor.addressFrom(response);
   return address.summaryDescription();
}

mock中的顺序

当在测试代码中使用 Mock 对象时,可以使用 EXPECT_CALL 宏来设置期望的函数调用,以及配置 Mock 对象的行为。这些期望的函数调用会被存储在 Mock 对象中,并按照它们被创建的顺序保存在 Mock 对象中。这样,当测试运行时,Mock 对象会按照这个顺序检查函数调用,以确保它们按照预期执行。
所以,当使用 Mock 对象时,期望函数调用的顺序可能非常重要,因为 Mock 对象会按顺序检查它们是否被正确调用。如果期望函数调用的顺序不正确,Mock 对象可能会抛出错误或失败,或测试用例可能不能正确地检查期望的行为。
为了解决这个问题,Google Test 提供了 ON_CALL 和 EXPECT_CALL 宏,以便在 Mock 对象中为不同的函数调用设置期望,并确保它们按照正确的顺序执行。可以使用 EXPECT_CALL 宏来设置常规的函数调用,以及期望的执行顺序,还可以使用 ON_CALL 宏来设置其他的函数调用,以特定的条件执行。这些宏可以帮助测试代码更加灵活和高效的设置 Mock 对象和检查它们的行为。

如果我们无意中交换了 Http 类中 initialize() 和 get() 函数的调用顺序,这种情况可能会发生。


string summaryDescription(const string& latitude, const string& longitude) const {
   string server{"http://open.mapquestapi.com/"};
   string document{"nominatim/v1/reverse"};
   string url = server + document + "?" +
                keyValue("format", "json") + "&" +
                keyValue("lat", latitude) + "&" +
                keyValue("lon", longitude);
   //get 和initialize() 函数的调用顺序被交换
   auto response = http_->get(url); 
   http_->initialize();
   AddressExtractor extractor;
   auto address = extractor.addressFrom(response);
   return address.summaryDescription();
}

令人惊讶的是,尽管我们交换了 Http 类中 initialize() 和 get() 函数的调用顺序,但测试仍然通过了!Google Mock 默认并不关心期望的的匹配顺序。如果你关心顺序,你可以告诉 Google Mock(以及其他许多 C++ mocking 工具)来验证它。最简单的方法是在测试的顶部声明一个 InSequence 实例,并确保后续的 EXPECT_CALL 函数出现的顺序符合你的期望。



// 定义测试用例方法
// 它属于 APlaceDescriptionService 的测试夹具类
TEST_F(APlaceDescriptionService, MakesHttpRequestToObtainAddress) {
   //testing::InSequence 所有的调用都都需要按照严格EXPECT_CALL的顺序执行 并且如果有调用顺序不正确的情况,测试将会失败。
   testing::InSequence forceExpectationOrder;
   // 实例化 HttpStub 对象,用于模拟
   HttpStub httpStub;
   // 定义我们期望的 URL 字符串的开头
   string urlStart{"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"};
   // 用静态常量值和 httpStub 拼接成期望的 URL 字符串
   auto expectedURL = urlStart +
                      "lat=" +
                      APlaceDescriptionService::ValidLatitude +
                      "&" +
                      "lon=" +
                      APlaceDescriptionService::ValidLongitude;
   EXPECT_CALL(httpStub, initialize());
   // 断言 httpStub 的 get 方法将使用期望的 URL 作为参数调用
   EXPECT_CALL(httpStub, get(expectedURL));
   // 使用 httpStub 实例化 PlaceDescriptionService 对象
   PlaceDescriptionService service{&httpStub};
   // 使用静态常量值调用 PlaceDescriptionService 对象的 summaryDescription 方法
   string addressinfo1 = service.summaryDescription(ValidLatitude, ValidLongitude);
   cout << addressinfo1 << endl;
}

在这里插入图片描述

EXPECT_CALL

EXPECT_CALL 宏支持许多附加修饰符。这是它的语法:


EXPECT_CALL(mock_object, method(matchers))
    .With(multi_argument_matcher) ?
    .Times(cardinality) ?
    .InSequence(sequences) *
    .After(expectations) *
    .WillOnce(action) *
    .WillRepeatedly(action) ?
    .RetiresOnSaturation(); ?

最有用的修饰符可能是 Times(),它让你可以指定一个方法被调用的次数。即使你知道一个方法会被调用多次,但不知道具体多少次,那么你也可以使用WillRepeatedly()。下面示例中的接口是在DifficultCollaborator类中定义的,需要一个int* 参数,和一个bool 的返回值


virtual bool calculate(int* result);

你可以使用 WillOnce() 或 WillRepeatedly() 为 Google Mock 的期望指定一个操作。大多数情况下,该操作将是返回一个值。如果是需要进行更复杂操作的函数,你可以使用 DoAll(),它可以创建一个由两个或更多操作组成的复合操作。


#include <gmock/gmock.h>

// 定义 DifficultCollaborator 类,它是一个被测试对象的依赖关系
class DifficultCollaborator {
  public:
   virtual bool calculate(int* result) {
      // Do some difficult calculation
      *result = 0;
      return false;
   }
};

// 定义目标类 Target,它是我们需要进行测试的类
class Target {
  public:
   int execute(DifficultCollaborator* collaborator) {
      int result;
      if (collaborator->calculate(&result)) {
         return result;
      }
      return -1;
   }
};

// 定义 DifficultCollaborator 类的 Mock 对象 DifficultCollaboratorMock,用于模拟 DifficultCollaborator 类的行为
class DifficultCollaboratorMock : public DifficultCollaborator {
  public:
   MOCK_METHOD(bool, calculate, (int* result), (override));
};

// 定义测试用例 TargetTest,测试 Target 类的行为
TEST(TargetTest, executeTest) {
   // 创建 DifficultCollaboratorMock 实例
   DifficultCollaboratorMock difficult;
   // 创建 Target 类实例
   Target calc;
   // 断言 DifficultCollaboratorMock 实例中的 calculate 方法应该被调用,
   // 并指定此调用的期望行为:将输出参数设置为 3,并返回 true
   EXPECT_CALL(difficult, calculate(testing::_))
       .WillOnce(testing::DoAll(
           testing::SetArgPointee<0>(3),
           testing::Return(true)));
   // 调用 Target 类的 execute 方法,将 DifficultCollaboratorMock 实例传入,并将返回值保存到 result 变量中
   auto result = calc.execute(&difficult);
   // 断言返回的结果是否等于 3
   ASSERT_THAT(result, testing::Eq(3));
}

// main 函数,运行测试用例
int main(int argc, char** argv) {
   testing::InitGoogleMock(&argc, argv);
   return RUN_ALL_TESTS();
}

Getting Test Doubles in Place

引入test double时,你有两个任务。首先,编写test double。其次,让目标使用它的一个实例。这样做被称为依赖注入(dependency injection, DI)。

依赖注入,MyApp 提供依赖的接口,外面实现好对象把依赖的对象传进去



#include <iostream>
#include <vector>

// 定义数据处理接口
class IDataProcessor {
  public:
   virtual std::vector<int> processData(const std::vector<int>& data) = 0;
   // 定义数据处理方法
};

// 实现数据处理接口的类
class DataProcessor : public IDataProcessor {
  public:
   std::vector<int> processData(const std::vector<int>& data) override {
      // 实现数据处理方法
      std::vector<int> result;
      for (auto x : data) {
         result.push_back(x * x);
      }
      return result;
   }
};

// 依赖注入的类
class MyApp {
  public:
   MyApp(IDataProcessor* dataProcessor)
       : m_dataProcessor(dataProcessor) {}
   // 使用构造函数注入IDataProcessor实例

   void run() {  // 执行数据处理流程的方法
      std::vector<int> data = {1, 2, 3, 4, 5};
      std::vector<int> result = m_dataProcessor->processData(data);
      // 使用注入的IDataProcessor进行数据处理

      std::cout << "Processed data:\n";
      for (auto x : result) {
         std::cout << x << " ";
      }
      std::cout << std::endl;
   }

  private:
   IDataProcessor* m_dataProcessor;  // 保存注入的IDataProcessor实例
};

int main() {
   DataProcessor dataProcessor;  // 创建一个实现IDataProcessor接口的类
   MyApp app(&dataProcessor);    // 创建MyApp实例,并注入IDataProcessor实例
   app.run();                    // 执行数据处理流程

   return 0;
}

在PlaceDescriptionService的例子中,我们通过构造函数注入了测试替身。在某些情况下,你可能会发现更适合使用setter成员函数来传递测试double。这些注入测试替身的方法被称为构造函数注入或setter注入。

Override Factory Method and Override Getter

工厂方法模式(Factory Method):定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法将一个类的实例化延迟到其子类。 对每一个子类产品都分别对应一个工厂子类,用来创建相应的产品,若增加了新的产品,只需相应增加工厂子类即可。


#include <iostream>

// 工厂方法模式中的产品类
class Product {
  public:
   virtual void use() = 0;
};

// 具体的产品类A
class ProductA : public Product {
  public:
   void use() override {
      std::cout << "Product A used" << std::endl;
   }
};

// 具体的产品类B
class ProductB : public Product {
  public:
   void use() override {
      std::cout << "Product B used" << std::endl;
   }
};

// 抽象工厂类
class Factory {
  public:
   virtual Product* createProduct() = 0;
};

// 具体工厂类A,用于创建ProductA类的实例
class FactoryA : public Factory {
  public:
   Product* createProduct() override {
      return new ProductA();
   }
};

// 具体工厂类B,用于创建ProductB类的实例
class FactoryB : public Factory {
  public:
   Product* createProduct() override {
      return new ProductB();
   }
};

int main() {
   // 使用具体的工厂类A创建一个ProductA实例
   FactoryA factoryA;
   Product* productA = factoryA.createProduct();
   productA->use();

   // 使用具体的工厂类B创建一个ProductB实例
   FactoryB factoryB;
   Product* productB = factoryB.createProduct();
   productB->use();

   delete productA;
   delete productB;

   return 0;
}

现在回到之前的代码


// 包含必要的头文件
#include <curl/curl.h>            // 用于 HTTP 请求
#include <jsoncpp/json/reader.h>  // 用于 JSON 解析
#include <jsoncpp/json/value.h>
#include <string>
#include "gmock/gmock.h"  // 用于单元测试
#include "gtest/gtest.h"

using namespace std;
using namespace Json;

// 定义一个用于保存 Address 的结构体
struct Address {
   string road;     // 街道
   string city;     // 城市
   string state;    // 州/省
   string country;  // 国家
   std::string summaryDescription() const {
      return road + ", " + city + ", " + state + ", " + country;
   }
};

// 定义一个 HTTP 接口
class Http {
  public:
   virtual ~Http() {}
   // 定义纯虚函数 initialize(),用于初始化 Http 对象
   virtual void initialize() = 0;
   // 定义纯虚函数 get(),用于向指定的 url 发送 get 请求,并返回响应结果
   virtual string get(const string& url) const = 0;
};

// 定义纯虚函数 get(),用于向指定的 url 发送 get 请求,并返回响应结果
class HttpStub : public Http {
  public:
   // 定义纯虚函数 get(),用于向指定的 url 发送 get 请求,并返回响应结果,返回值为 void
   MOCK_METHOD0(initialize, void());
   // 定义一个带有一个参数的 const 成员函数 ,传入参数为 url 字符串,返回值为响应字符串
   MOCK_CONST_METHOD1(get, string(const string&));
};

// 使用 libcurl 定义一个具体的 Http 实现类
class CurlHttp : public Http {
  public:
   CurlHttp()
       : curl(NULL){};
   virtual ~CurlHttp() {
      curl_global_cleanup();  // 在使用 libcurl 后进行清理
   }

   void initialize() {
      curl_global_init(CURL_GLOBAL_ALL);                                        // 初始化 libcurl
      curl = curl_easy_init();                                                  // 创建一个 curl 句柄
      curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &CurlHttp::writeCallback);  // 设置响应数据回调函数
   }
   virtual string get(const string& url) const {
      response_ = "invalid request";                     // 初始化响应字符串
      curl_easy_setopt(curl, CURLOPT_URL, url.c_str());  // 设置请求的
      curl_easy_perform(curl);                           // 发送请求
      curl_easy_cleanup(curl);                           // 清理 curl 句柄

      return CurlHttp::Response();  // 返回响应字符串
   }
   static string Response() {
      return response_;
   }
   static size_t writeCallback(const char* buf, size_t size, size_t nMemb, void*) {
      for (auto i = 0u; i < size * nMemb; i++)  // 将响应数据添加到响应字符串中
         response_.push_back(buf[i]);
      return size * nMemb;
   }

  private:
   CURL* curl;

   static string response_;
};

// 定义一个用于从 JSON 字符串中提取 Address 的类
class AddressExtractor {
  public:
   Address addressFrom(const string& json) const {
      Address address;
      Value jsonAddress{jsonAddressFrom(json)};  // 解析 JSON 并提取 address 字段
      populate(address, jsonAddress);            // 将 JSON 中的各个字段填充到 Address 结构体中
      return address;
   }

  private:
   Json::Value jsonAddressFrom(const string& json) const {
      auto location = parse(json);                  // 解析 JSON 字符串
      return location.get("address", Value::null);  // 返回 JSON 对象中的 address 字段,如果不存在则返回 null
   }
   void populate(Address& address, Json::Value& jsonAddress) const {
      address.road = getString(jsonAddress, "road");  // 分别将 Address 结构体的各个字段填充为 JSON 对象中对应的字段值
      address.city = getString(jsonAddress, "city");
      address.state = getString(jsonAddress, "state");
      address.country = getString(jsonAddress, "country");
   }
   Json::Value parse(const string& json) const {
      Value root;
      Reader reader;
      reader.parse(json, root);  // 解析 JSON 字符串并将结果存入 root 变量中
      return root;
   }
   string getString(Json::Value& result, const string& name) const {
      return result.get(name, "").asString();  // 返回指定字段的字符串值,如果不存在则返回空字符串
   }
};

// 定义了一个 PlaceDescriptionService 类,提供了获取地点描述信息的服务
class PlaceDescriptionService {
  public:
   // 构造函数,初始化 http 对象
   PlaceDescriptionService(Http* http)
       : http_(http){};
   // 获取某个经纬度对应地点的简要描述
   string summaryDescription(const string& latitude, const string& longitude) const {
      string server{"http://open.mapquestapi.com/"};
      string document{"nominatim/v1/reverse"};
      string url = server + document + "?" +
                   keyValue("format", "json") + "&" +
                   keyValue("lat", latitude) + "&" +
                   keyValue("lon", longitude);
      http_->initialize();
      auto response = http_->get(url);
      AddressExtractor extractor;
      auto address = extractor.addressFrom(response);
      return address.summaryDescription();
   }

   string keyValue(const string& key,
                   const string& value) const {
      return key + "=" + value;
   }

  private:
   // 创建 HTTP GET 请求的 URL
   string createGetRequestUrl(const string& latitude, const string& longitude) const;
   // 根据地址信息生成简要描述
   string summaryDescription(const Address& address) const;
   Http* http_;  // HTTP 请求处理对象
};

// 这是一个 PlaceDescriptionService 的测试夹具类定义,继承了 testing::Test,由 Google Test 框架提供
class APlaceDescriptionService : public testing::Test {
  public:  // 定义静态常量字符串值,表示有效的纬度和经度
   static const string ValidLatitude;
   static const string ValidLongitude;
};

// 定义静态常量字符串值
const string APlaceDescriptionService::ValidLatitude("38.005");
const string APlaceDescriptionService::ValidLongitude("-104.44");

// 定义测试用例方法
// 它属于 APlaceDescriptionService 的测试夹具类
TEST_F(APlaceDescriptionService, MakesHttpRequestToObtainAddress) {
   testing::InSequence forceExpectationOrder;
   // 实例化 HttpStub 对象,用于模拟
   HttpStub httpStub;
   // 定义我们期望的 URL 字符串的开头
   string urlStart{"http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"};
   // 用静态常量值和 httpStub 拼接成期望的 URL 字符串
   auto expectedURL = urlStart +
                      "lat=" +
                      APlaceDescriptionService::ValidLatitude +
                      "&" +
                      "lon=" +
                      APlaceDescriptionService::ValidLongitude;
   EXPECT_CALL(httpStub, initialize());
   // 断言 httpStub 的 get 方法将使用期望的 URL 作为参数调用
   EXPECT_CALL(httpStub, get(expectedURL));
   // 使用 httpStub 实例化 PlaceDescriptionService 对象
   PlaceDescriptionService service{&httpStub};
   // 使用静态常量值调用 PlaceDescriptionService 对象的 summaryDescription 方法
   string addressinfo1 = service.summaryDescription(ValidLatitude, ValidLongitude);
   cout << addressinfo1 << endl;
}

// TEST_F 宏用来定义一个测试用例,并使用 APlaceDescriptionService 类作为测试的 Fixture
TEST_F(APlaceDescriptionService, FormatsRetrievedAddressIntoSummaryDescription) {
   // 创建 HttpStub 对象,并设置 EXPECT_CALL,告诉 Google Mock 当调用 get() 时应该返回什么值。
   HttpStub httpStub;
   EXPECT_CALL(httpStub, get(testing::_))
       .WillOnce(testing::Return(
           R"({ "address": {
              "road":"Drury Ln",
              "city":"Fountain",
              "state":"CO",
              "country":"US" }})"));
   // 用 HttpStub 对象创建 PlaceDescriptionService 对象。
   PlaceDescriptionService service(&httpStub);
   // 调用 service.summaryDescription(),获取经纬度对应的地址简述信息。
   auto description = service.summaryDescription(ValidLatitude, ValidLongitude);
   // 将获取到的地址简述信息与期望值 "Drury Ln, Fountain, CO, US" 进行比较。
   ASSERT_THAT(description, testing::Eq("Drury Ln, Fountain, CO, US"));
}

// 定义 main 函数
int main(int argc, char** argv) {
   testing::InitGoogleMock(&argc, argv);  // 初始化 Google Mock 并运行所有测试
   return RUN_ALL_TESTS();
}

还有其他获得测试替身的技术。使用最适合你的情况的一个。要应用覆盖工厂方法,您必须更改生产代码,以便在任何需要collaborator实例的时候使用工厂方法。下面是在PlaceDescriptionService中实现更改的一种方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值