C++ 測試框架 GoogleTest 初學者入門篇 丙

来源:https://www.cnblogs.com/englyf/archive/2023/04/15/17320950.html
-Advertisement-
Play Games

theme: channing-cyan *以下內容為本人的學習筆記,如需要轉載,請聲明原文鏈接 微信公眾號「ENG八戒」https://mp.weixin.qq.com/s/RIztusI3uKRnoHVf0sloeg 開發者雖然主要負責工程里的開發任務,但是每個開發完畢的功能都是需要開發者自測通 ...


theme: channing-cyan

*以下內容為本人的學習筆記,如需要轉載,請聲明原文鏈接 微信公眾號「ENG八戒」https://mp.weixin.qq.com/s/RIztusI3uKRnoHVf0sloeg

開發者雖然主要負責工程里的開發任務,但是每個開發完畢的功能都是需要開發者自測通過的,所以經常會聽到開發者提起單元測試的話題。那麼今天我就帶大伙一起來看看大名鼎鼎的谷歌 C++ 測試框架 GoogleTest。

本文上接《C++ 測試框架 GoogleTest 初學者入門篇 乙》,歡迎關註公眾號【ENG八戒】查看更多精彩內容。


斷言

什麼是斷言?斷言是用來對錶達式執行比較的代碼塊,調用時類似函數。當表達式一致時,斷言返回成功,否則失敗。

googletest 的斷言是一組巨集定義。分為 ASSERT_* 和 EXPECT_* 兩種。

比如

ASSERT_EQ(1, 2);

EXPECT_EQ(1, 2);

上面用到的兩個斷言都是比較輸入的數據是否相等。主要區別是,ASSERT_* 在失敗時終止程式運行,EXPECT_* 在失敗時不會終止程式運行,但是都會返回錯誤信息。因而測試使用 EXPECT_* 可以發現更多的問題而不會打斷測試流程。

那麼 ASSERT_* 斷言失敗時,跟在其後的語句會被忽略執行,如果其中包含對資源的釋放,那麼就有會出現資源泄漏的問題,斷言失敗報錯信息會附帶有堆檢查錯誤。這時出現的資源泄漏問題,真的有必要修複碼?看具體情況而定。

另外,googletest 在斷言失敗後除了可以返回標準錯誤信息,還可以附帶返回自定義錯誤信息,使用操作符 << 添加自定義錯誤信息。

ASSERT_EQ(1, 2) << "1 is not equal to 2";

EXPECT_EQ(1, 2) << "1 is not equal to 2";

任何可以傳遞給 ostream 的數據都可以作為自定義錯誤信息傳遞給斷言,比如 C 字元串、string對象。

那麼,測試的基本手段就是利用斷言,除了判斷型的斷言之外,googletest 還提供了其它類型的斷言用於協助測試,比如顯式成功或失敗、布爾類型斷言、字元串比較斷言等,詳情可以前往官網查看手冊。

https://google.github.io/googletest/reference/assertions.html

如何測試

前面提到在 googletest 中,測試的範圍分為測試套件和單個測試。測試程式可以包含多個測試套件,一個測試套件可以包含多個測試。

簡單的測試一般推薦使用 TEST 巨集來定義單個測試。

一般的使用方式如下

TEST(test_suite_name, test_name) {
  // test body
}

test_suite_name 是測試套件名,test_name 是單個測試的名稱,書寫時都應該符合 C++ 的標識符規範,而且不能包含有下劃線_。更詳細的命名規範可以查看下麵的鏈接

https://google.github.io/styleguide/cppguide.html#Function_Names

那麼 TEST 巨集到底代表著什麼?一起來看看 TEST 巨集定義的源代碼

#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )

#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
  test_suite_name##_##test_name##_Test

#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id)       \
  static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1,                 \
                "test_suite_name must not be empty");                          \
  static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1,                       \
                "test_name must not be empty");                                \
  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
      : public parent_class {                                                  \
   public:                                                                     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
    ~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default;  \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete;     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        const GTEST_TEST_CLASS_NAME_(test_suite_name,                          \
                                     test_name) &) = delete; /* NOLINT */      \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        GTEST_TEST_CLASS_NAME_(test_suite_name,                                \
                               test_name) &&) noexcept = delete; /* NOLINT */  \
                                                                               \
   private:                                                                    \
    void TestBody() override;                                                  \
    static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;      \
  };                                                                           \
                                                                               \
  ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name,           \
                                                    test_name)::test_info_ =   \
      ::testing::internal::MakeAndRegisterTestInfo(                            \
          #test_suite_name, #test_name, nullptr, nullptr,                      \
          ::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id),  \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),          \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),       \
          new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_(     \
              test_suite_name, test_name)>);                                   \
  void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()

#define GTEST_TEST(test_suite_name, test_name)             \
  GTEST_TEST_(test_suite_name, test_name, ::testing::Test, \
              ::testing::internal::GetTestTypeId())

#define TEST(test_suite_name, test_name) GTEST_TEST(test_suite_name, test_name)

這麼多預定義處理,不妨嘗試代入上面的一般使用方式,然後展開一下,展開如下

static_assert(sizeof("test_suite_name") > 1,
              "test_suite_name must not be empty");
static_assert(sizeof("test_name") > 1,
              "test_name must not be empty");
			  
class test_suite_name_test_name_Test : public ::testing::Test {
  public:
  test_suite_name_test_name_Test() = default;
  ~test_suite_name_test_name_Test() override = default;
  test_suite_name_test_name_Test(const test_suite_name_test_name_Test &) = delete;
  test_suite_name_test_name_Test & operator=(
      const test_suite_name_test_name_Test &) = delete; /* NOLINT */
  test_suite_name_test_name_Test
  (test_suite_name_test_name_Test &&) noexcept = delete;
  test_suite_name_test_name_Test & operator=(
      test_suite_name_test_name_Test &&) noexcept = delete; /* NOLINT */

  private:
  void TestBody() override;
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};

::testing::TestInfo* const test_suite_name_test_name_Test::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
        "test_suite_name", "test_name", nullptr, nullptr,
        ::testing::internal::CodeLocation(__FILE__, __LINE__),
        ::testing::internal::GetTestTypeId(),
        ::testing::internal::SuiteApiResolver<
            parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
        ::testing::internal::SuiteApiResolver<
            parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
        new ::testing::internal::TestFactoryImpl<test_suite_name_test_name_Test>);
		
void test_suite_name_test_name_Test::TestBody() {
  // test body
}

從展開後的代碼,可以看到有一堆代碼,最開始有兩個斷言 static_assert 用來判斷輸入的測試套件名和測試名長度是否大於1,所以要求 TEST 巨集定義輸入的測試套件名和測試名都不能為空。

然後基於 ::testing::Test 派生了一個類,類名是測試套件名和測試名串接後再在末尾加上 _Test。類內聲明重寫 TestBody() 方法。

TEST 巨集定義後面的 {} 用於定義派生類的成員方法 TestBody() 的函數體,內部填寫標準 C++ 的有效語句作為測試主體,當然也包含調用 googletest 提供的模塊內容,註意這個代碼塊是沒有返回值的。代碼塊執行的斷言失敗時,或者代碼崩潰,則測試 test_name 失敗,否則成功。

再來看個例子

int square(const int a)
{
  // ...
}

TEST(SquareTest, PositiveNos) { 
    ASSERT_EQ(0, square(0));
    ASSERT_EQ(36, square(6));
    ASSERT_EQ(324, square(18));
}
 
TEST(SquareTest, NegativeNos) {
    ASSERT_EQ(1, square(-1));
    ASSERT_EQ(100, square(-10));
}

上面定義了兩個測試 PositiveNos 和 NegativeNos,都屬於測試套件 SquareTest。

googletest 在設計時就指定通過測試套件來彙總測試結果,所以驗證同一個邏輯功能的測試應該定義在同一個測試套件內。

測試夾具

在 googletest 里什麼是測試夾具?

測試夾具這個概念是為瞭解決當你的同一個邏輯功能測試里,有多個測試共用測試數據或者配置的問題。

需要用到測試夾具的測試一般推薦使用 TEST_F 巨集來定義單個測試。

一般的使用方式如下

TEST_F(FixtureTest, test_name) {
  // test body
}

不過,TEST_F 巨集的第一個輸入參數不僅僅是測試套件名稱,同時也是測試夾具類名。這個測試夾具類需要自己基於類 ::testing::Test 派生實現。

class FixtureTest : public testing::Test {
protected:
void SetUp() override { ... }
void TearDown() override { ... }
// custom data
};

共用的測試數據或者配置就在這個派生類里添加即可。SetUp() 用於初始化數據和配置,TearDown() 用於卸載配置。

那麼 TEST_F 巨集到底代表著什麼,和 TEST 巨集的區別在哪?一起來看看 TEST_F 巨集定義的源代碼

#define GTEST_STRINGIFY_HELPER_(name, ...) #name
#define GTEST_STRINGIFY_(...) GTEST_STRINGIFY_HELPER_(__VA_ARGS__, )

#define GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) \
  test_suite_name##_##test_name##_Test

#define GTEST_TEST_(test_suite_name, test_name, parent_class, parent_id)       \
  static_assert(sizeof(GTEST_STRINGIFY_(test_suite_name)) > 1,                 \
                "test_suite_name must not be empty");                          \
  static_assert(sizeof(GTEST_STRINGIFY_(test_name)) > 1,                       \
                "test_name must not be empty");                                \
  class GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                     \
      : public parent_class {                                                  \
   public:                                                                     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() = default;            \
    ~GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)() override = default;  \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (const GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &) = delete;     \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        const GTEST_TEST_CLASS_NAME_(test_suite_name,                          \
                                     test_name) &) = delete; /* NOLINT */      \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)                         \
    (GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) &&) noexcept = delete; \
    GTEST_TEST_CLASS_NAME_(test_suite_name, test_name) & operator=(            \
        GTEST_TEST_CLASS_NAME_(test_suite_name,                                \
                               test_name) &&) noexcept = delete; /* NOLINT */  \
                                                                               \
   private:                                                                    \
    void TestBody() override;                                                  \
    static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;      \
  };                                                                           \
                                                                               \
  ::testing::TestInfo* const GTEST_TEST_CLASS_NAME_(test_suite_name,           \
                                                    test_name)::test_info_ =   \
      ::testing::internal::MakeAndRegisterTestInfo(                            \
          #test_suite_name, #test_name, nullptr, nullptr,                      \
          ::testing::internal::CodeLocation(__FILE__, __LINE__), (parent_id),  \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetSetUpCaseOrSuite(__FILE__, __LINE__),          \
          ::testing::internal::SuiteApiResolver<                               \
              parent_class>::GetTearDownCaseOrSuite(__FILE__, __LINE__),       \
          new ::testing::internal::TestFactoryImpl<GTEST_TEST_CLASS_NAME_(     \
              test_suite_name, test_name)>);                                   \
  void GTEST_TEST_CLASS_NAME_(test_suite_name, test_name)::TestBody()

#define GTEST_TEST_F(test_fixture, test_name)        \
  GTEST_TEST_(test_fixture, test_name, test_fixture, \
              ::testing::internal::GetTypeId<test_fixture>())

#define TEST_F(test_fixture, test_name) GTEST_TEST_F(test_fixture, test_name)

這麼多預定義處理,手癢代入一般的使用方式然後展開一下,展開如下

static_assert(sizeof("FixtureTest") > 1,
              "FixtureTest must not be empty");
static_assert(sizeof("test_name") > 1,
              "test_name must not be empty");
class FixtureTest_test_name_Test : public FixtureTest {
  public:
  FixtureTest_test_name_Test() = default;
  ~FixtureTest_test_name_Test() override = default;
  FixtureTest_test_name_Test(const FixtureTest_test_name_Test &) = delete;
  FixtureTest_test_name_Test & operator=(
      const FixtureTest_test_name_Test &) = delete; /* NOLINT */
  FixtureTest_test_name_Test
  (FixtureTest_test_name_Test &&) noexcept = delete;
  FixtureTest_test_name_Test & operator=(
      FixtureTest_test_name_Test &&) noexcept = delete; /* NOLINT */
  
  private:
  void TestBody() override;
  static ::testing::TestInfo* const test_info_ GTEST_ATTRIBUTE_UNUSED_;
};

::testing::TestInfo* const FixtureTest_test_name_Test::test_info_ =
    ::testing::internal::MakeAndRegisterTestInfo(
        #FixtureTest, #test_name, nullptr, nullptr,
        ::testing::internal::CodeLocation(__FILE__, __LINE__),
        ::testing::internal::GetTypeId<FixtureTest>(),
        ::testing::internal::SuiteApiResolver<
            FixtureTest>::GetSetUpCaseOrSuite(__FILE__, __LINE__),
        ::testing::internal::SuiteApiResolver<
            FixtureTest>::GetTearDownCaseOrSuite(__FILE__, __LINE__),
        new ::testing::internal::TestFactoryImpl<FixtureTest_test_name_Test>);
void FixtureTest_test_name_Test::TestBody() {
  // test body
}

從展開後的代碼來看,TEST_F 和 TEST 實現基本類似,那麼使用時要遵循的規則也是一樣的,除了需要傳入自定義的基於 ::testing::Test 派生類,並且測試套件名就是測試夾具類名。

舉個例子,有個模板類 Queue 的邏輯功能需要測試,它實現了 FIFO 的數據隊列管理。

template <typename E>  // E 是元素類型
class Queue {
 public:
  Queue();
  void Enqueue(const E& element); // 數據入隊
  E* Dequeue();  // 數據出隊,如果隊列為空則返回 NULL
  size_t size() const;  // 隊列數據長度
  ...
};

然後需要基於 ::testing::Test 派生一個測試夾具類 QueueTest

class QueueTest : public ::testing::Test {
 protected:
  void SetUp() override {
     q1_.Enqueue(1);
     q2_.Enqueue(2);
     q2_.Enqueue(3);
  }

  // void TearDown() override {}

  Queue<int> q0_;
  Queue<int> q1_;
  Queue<int> q2_;
};

夾具類 QueueTest 內定義了三個隊列數據對象。SetUp() 內對數據對象初始化,q0_ 保持為空,q1_ 入隊一個數據,q2_ 入隊兩個數據。

為什麼不實現 TearDown() 呢?TearDown() 本來的設計意圖是卸載配置,不是剛好可以用來清理數據嗎?是的,的確可以,不過這裡有個更好的選擇,就是使用類析構函數來對隊列清空。這裡有個建議就是,能用析構函數處理的,儘量用析構函數替代 TearDown()。因為用析構函數可以確保被調用而且調用的順序不會亂,但不是說所有情況都建議用析構函數替代 TearDown(),這裡不展開了。

接著調用 TEST_F 定義兩個測試,基於測試夾具類 QueueTest,測試套件名也是 QueueTest,兩個測試名分別為 IsEmptyInitially 和 DequeueWorks。

TEST_F(QueueTest, IsEmptyInitially) {
  EXPECT_EQ(q0_.size(), 0);
}

TEST_F(QueueTest, DequeueWorks) {
  int* n = q0_.Dequeue();
  EXPECT_EQ(n, nullptr);

  n = q1_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 1);
  EXPECT_EQ(q1_.size(), 0);
  delete n;

  n = q2_.Dequeue();
  ASSERT_NE(n, nullptr);
  EXPECT_EQ(*n, 2);
  EXPECT_EQ(q2_.size(), 1);
  delete n;
}

上面的這兩個測試定義,都會創建 QueueTest 類對象,分別創建而且不共用,所以數據不會相互影響。

第一個測試 IsEmptyInitially,googletest 框架會先創建 QueueTest 類對象 obj,調用 SetUp() 初始化數據和配置,執行測試。這裡只執行了一個 EXPECT_EQ 斷言,EXPECT_* 類型的斷言失敗後會返回失敗信息,不會終止測試程式,繼續下一步測試。然後調用 TearDown() 清理,最後執行對象 obj 的析構函數釋放資源並退出當前測試。

第二個測試 DequeueWorks,執行流程與上一個類似。其中測試內容包含有 ASSERT_* 類別的斷言,這種斷言在失敗後除了會返回失敗信息,還會終止測試程式。如果斷言失敗之後的測試已沒有意義,那麼適合使用 ASSERT_* 類別的斷言。

測試調用過程

其它 C++ 測試框架在測試開始前,需要你把測試排列出來,但是 googletest 不需要這麼麻煩。 在 googletest 框架中,定義好測試後,只需要在 main 部分執行如下代碼即可。

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

InitGoogleTest() 可以對程式的輸入命令執行解析,基於這點可以通過命令行的方式控制測試框架的運行。

繼續以上面的代碼為例,大致流程如下

  1. InitGoogleTest() 初始化測試框架。
  2. RUN_ALL_TESTS() 啟動測試。
  3. 查找測試套件內的測試。
  4. 保存配置標誌。
  5. 創建 QueueTest 實例。
  6. 調用 QueueTest 實例的 SetUp() 初始化數據配置。
  7. 執行測試。
  8. 調用 QueueTest 實例的 TearDown() 卸載數據配置。
  9. 恢復配置標誌。
  10. 重覆第 3 步,直到所有測試執行完畢,

RUN_ALL_TESTS() 返回 0 表示成功,否則失敗。只能在主線程里調用 RUN_ALL_TESTS()。

在一般的測試里,如果在測試運行之前不需要做一些自定義的事情,而且這些事情無法在測試夾具和測試套件的框架中表達時,main 函數這部分其實都一樣,那麼 googletest 就在庫 gtest_main 里提供了一個很方便的入口點,也就是幫你提前寫好了 main 函數,你可以省去這部分,編譯的時候記得鏈接庫 gtest_main 即可。


好了,這個系列的文章就寫到這裡啦。

學習可以等,時間不等人!

關註我,帶你學習編程領域更多核心技能!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • “我苦心鍛煉了三年,我變禿了,也變強了。” —— 琦玉老師 0x00 大綱 0x01 前言 四個月前,我在《你是來找茬的吧?對自己的博客進行調優》一文中探討了以博客的使用者而不是開發者身份去進行優化,究竟能做到何種程度的問題。當時以 Edge 瀏覽器的開發者工具里的 lighthouse 評分和載入 ...
  • #例子 星巴茲是以擴張速度最快而聞名的咖啡連鎖店。因為擴張速度實在太快,他們著急更新訂單系統,來匹配他們的飲料供應要求。 ##實現1 繼承 購買咖啡時,也可以要求其中加入各種調料,例如:蒸奶,豆漿 很明顯,星巴茲為自己製造了一個維護噩夢,如果牛奶的價錢上揚,怎麼辦?新增一種焦糖調料風味時,怎麼辦 調 ...
  • 說明 使用 VLD 記憶體泄漏檢測工具輔助開發時整理的學習筆記。同系列文章目錄可見 《記憶體泄漏檢測工具》目錄 1. 使用方式 在 VS 中使用 VLD 的方法可以查看另外一篇博客:在 VS 2015 中使用 VLD。 2. 輸出報告 在 VS 中使用 VLD 時的輸出報告,與在 QT 中使用時是一致的 ...
  • 說明 使用 VLD 記憶體泄漏檢測工具輔助開發時整理的學習筆記。本篇介紹在 VS 2015 中使用 VLD。同系列文章目錄可見 《記憶體泄漏檢測工具》目錄 1. 使用前的準備 參考本人另一篇博客 安裝 Visual Leak Detector 下載 vld-2.5.1-setup.exe 並按步驟安裝 ...
  • 標記介面 標記介面(Marker Interface),又稱標簽介面(Tag Interface) 僅代表一個標記 不包含任何方法 標記介面是用來判斷某個類是否具有某種能力 Cloneable標記介面 此類實現了 Cloneable 介面,以指示 Object.clone 方法可以合法地對該類實例進 ...
  • 1. Set介面基本介紹 Set是無序集合(添加和取出的順序不一致,但取出的順序是固定的),沒有索引 不允許重覆元素,所以最多包含一個null JDK API中Set介面的實現類有: Abstract, ConcurrentHashMap.KeySetView, ConcurrentSkipList ...
  • 在PyQt開發中,時常需要對控制項的值進行校驗,如需要校驗QCheckBox是否被選中,QLabel是否校驗值是否為空等等。在複雜的業務場景下,這類控制項如果數量很多,逐個校驗就顯得麻煩,需要一一獲得控制項名稱,再調用對應的方法來判斷是否被選中、是否為空等。而且開發過程中如果多控制項做了增減,還需要增減校驗 ...
  • 操作系統的四個特性? 併發:同一段時間內多個程式執行(與並行區分,並行指的是同一時刻有多個事件,多處理器系統可以使程式並行執行) 共用:系統中的資源可以被記憶體中多個併發執行的進線程共同使用 虛擬:通過分時復用(如分時系統)以及空分復用(如虛擬記憶體)技術把一個物理實體虛擬為多個 非同步:系統進程用一種走 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...