一区二区三区日韩精品-日韩经典一区二区三区-五月激情综合丁香婷婷-欧美精品中文字幕专区

分享

單元測(cè)試成神之路

 刀首木 2023-12-21 發(fā)布于廣東

單元測(cè)試成神之路——C++篇

28 Sep 2020

Reading time ~8 minutes

前言

  在上一篇單元測(cè)試成神之路————GoLang篇 中, 首先介紹了單元測(cè)試的意義和編寫(xiě)單元測(cè)試的一般方法。作為同系列出品,本篇文章則主要介紹如何在C++中寫(xiě)單元測(cè)試。

  本文直接從常用的C++單元測(cè)試框架出發(fā),分別對(duì)幾種框架進(jìn)行了簡(jiǎn)單的介紹和小結(jié),然后介紹了Mock的框架, 并以具體代碼示例進(jìn)行說(shuō)明,最后列舉了一些常見(jiàn)問(wèn)題。

一、常用C++單測(cè)框架

  常用的C++單測(cè)對(duì)比如下:

Google Test Catch 2 CppUTest  
特點(diǎn) <ul><li>成熟、兼容性好</li><li>簡(jiǎn)潔、有效率</li><li>常用、學(xué)習(xí)資源多</li></ul> <ul><li>框架只有一個(gè)catch.hpp、集成輕松</li><li>有Given-When-Then分區(qū),適合BDD行為驅(qū)動(dòng)開(kāi)發(fā)</li><li>無(wú)自帶Mock框架</li></ul> <ul><li>可以檢測(cè)內(nèi)存泄露</li><li>輸出更簡(jiǎn)潔</li><li>適合在嵌入式系統(tǒng)項(xiàng)目中使用</li></ul>
Mock框架 Google Mock 無(wú)自帶Mock框架 CppUMock
推薦指數(shù) ★★★★★ ★★★☆☆ ★★☆☆☆

  一般情況下,我們推薦使用Google Test搭配Google Mock。如果項(xiàng)目有特殊需求或更適合其他框架,也可以考慮。

  根據(jù)實(shí)際使用頻率,在以下部分,Google Test和Google Mock的介紹更為詳細(xì);對(duì)于其他框架,這里介紹它們的主要特點(diǎn), 具體使用方法,可以查閱各自文檔。

二. Google Test

  Google Test是目前比較成熟而且最常用的C++單元測(cè)試框架之一。

1. 基本概念

  斷言(Assertions) 是檢查條件是否為真的語(yǔ)句。斷言的結(jié)果可能是成功或者失敗, 而失敗又分為非致命失敗致命失敗。如果發(fā)生致命失敗,測(cè)試進(jìn)程將中止當(dāng)前運(yùn)行,否則它將繼續(xù)運(yùn)行。

  測(cè)試(Test) 使用斷言來(lái)驗(yàn)證被測(cè)試代碼的行為。如果測(cè)試崩潰或斷言失敗,則測(cè)試失敗;否則測(cè)試成功。

  測(cè)試套件(Test Suite) 包含一個(gè)或多個(gè)測(cè)試(Test)。當(dāng)測(cè)試套件中的多個(gè)測(cè)試需要共享通用對(duì)象和子例程時(shí), 可以將它們放入測(cè)試夾具(Test Fixture)。

  測(cè)試程序(Test Program) 可以包含多個(gè)測(cè)試套件。

2. 斷言

  Google Test中,斷言(Assertions) 是類(lèi)似函數(shù)調(diào)用的。斷言失敗時(shí),googletest會(huì)輸出斷言的源文件和 行號(hào)位置以及失敗消息;我們還可以提供自定義失敗消息,該消息將附加到googletest消息中。

  斷言成對(duì)出現(xiàn)(ASSERT_*EXPECT_*),它們測(cè)試的對(duì)象相同,但對(duì)當(dāng)前運(yùn)行有不同的影響。ASSERT_*版本失敗時(shí) 會(huì)產(chǎn)生致命故障,并中止當(dāng)前函數(shù)(不一定是整個(gè)TEST)運(yùn)行。EXPECT_*版本會(huì)產(chǎn)生非致命故障,不會(huì)停止當(dāng)前函數(shù)運(yùn)行。 通常EXPECT_*是首選,因?yàn)榭梢栽跍y(cè)試中報(bào)告多個(gè)故障。但是如果在斷言失敗時(shí)繼續(xù)執(zhí)行沒(méi)有意義,則應(yīng)使用ASSERT_*

  要提供自定義失敗消息,只需使用<< 運(yùn)算符或此類(lèi)運(yùn)算符的序列將其流式傳輸?shù)胶曛屑纯?。一個(gè)例子:

ASSERT_EQ(x.size(), y.size()) << "x和y長(zhǎng)度不同";

for (int i = 0; i < x.size(); ++i) {
	EXPECT_EQ(x[i], y[i]) << "x和y元素存在不同:" << i;
}

  以下是一些最常用的斷言,如果需要查閱其他斷言,可以前往googletest的官方文檔。

2.1. 基本斷言

  condition 是返回true/false的變量、布爾表達(dá)式、函數(shù)調(diào)用等,以下斷言對(duì)其進(jìn)行驗(yàn)證。

失敗時(shí)中斷執(zhí)行的斷言 失敗時(shí)不中斷執(zhí)行的斷言 斷言成功情況
ASSERT_TRUE(condition); EXPECT_TRUE(condition); condition為真
ASSERT_FALSE(condition); EXPECT_FALSE(condition); condition為假

  例如:在ASSERT_TRUE(condition)中,當(dāng)conditiontrue時(shí),符合斷言,不影響執(zhí)行;當(dāng)conditionfalse時(shí),不符合斷言,且由于是ASSERT,當(dāng)前執(zhí)行中斷。

2.2. 普通比較型斷言

  val1val2是兩個(gè)可用==!=、>、<等運(yùn)算符進(jìn)行比較的值,以下斷言對(duì)其進(jìn)行比較。

失敗時(shí)中斷執(zhí)行的斷言 失敗時(shí)不中斷執(zhí)行的斷言 斷言成功情況
ASSERT_EQ(val1, val2); EXPECT_EQ(val1, val2); val1 == val2
ASSERT_NE(val1, val2); EXPECT_NE(val1, val2); val1 != val2
ASSERT_LT(val1, val2); EXPECT_LT(val1, val2); val1 < val2
ASSERT_LE(val1, val2); EXPECT_LE(val1, val2); val1 <= val2
ASSERT_GT(val1, val2); EXPECT_GT(val1, val2); val1 > val2
ASSERT_GE(val1, val2); EXPECT_GE(val1, val2); val1 >= val2

  例如:在ASSERT_GT(val1, val2)中,只有當(dāng)val1 > val2時(shí),符合斷言,不影響執(zhí)行;當(dāng)val1 <= val2時(shí), 不符合斷言,且由于是ASSERT,當(dāng)前執(zhí)行中斷。

2.3. C字符串比較型斷言

  str1str2是兩個(gè)C字符串,以下斷言對(duì)它們的值進(jìn)行比較;如果要比較兩個(gè)std::string對(duì)象,直接用之前 提到的EXPECT_NE,EXPECT_NE等。

失敗時(shí)中斷執(zhí)行的斷言 失敗時(shí)不中斷執(zhí)行的斷言 斷言成功情況
ASSERT_STREQ(str1,str2); EXPECT_STREQ(str1,str2); 這兩個(gè)C字符串具有相同的內(nèi)容
ASSERT_STRNE(str1,str2); EXPECT_STRNE(str1,str2); 兩個(gè)C字符串的內(nèi)容不同
ASSERT_STRCASEEQ(str1,str2); EXPECT_STRCASEEQ(str1,str2); 忽略大小寫(xiě),兩個(gè)C字符串的內(nèi)容相同
ASSERT_STRCASENE(str1,str2); EXPECT_STRCASENE(str1,str2) 忽略大小寫(xiě),兩個(gè)C字符串的內(nèi)容不同

  例如:char *str1 = "ABC";char *str2 = "ABC";,EXPECT_STREQ(str1, str2);斷言通過(guò), 因?yàn)樗鼈兊膬?nèi)容一樣;而EXPECT_EQ(str1, str2);斷言失敗,因?yàn)樗鼈兊牡刂凡灰粯印?/p>

  注意:一個(gè)NULL指針和一個(gè)空字符串""是不同的。

2.4. 浮點(diǎn)數(shù)比較型斷言

  val1val2是兩個(gè)浮點(diǎn)數(shù),以下斷言對(duì)其進(jìn)行比較。

失敗時(shí)中斷執(zhí)行的斷言 失敗時(shí)不中斷執(zhí)行的斷言 斷言成功情況
ASSERT_FLOAT_EQ(val1, val2); EXPECT_FLOAT_EQ(val1, val2); 這兩個(gè)float值幾乎相等
ASSERT_DOUBLE_EQ(val1, val2); EXPECT_DOUBLE_EQ(val1, val2); 這兩個(gè)double值幾乎相等

  以下斷言可以選擇可接受的誤差范圍:

失敗時(shí)中斷執(zhí)行的斷言 失敗時(shí)不中斷執(zhí)行的斷言 斷言成功情況
ASSERT_NEAR(val1, val2, abs_error); EXPECT_NEAR(val1, val2, abs_error); val1和val2的差的絕對(duì)值不超過(guò)abs_error

2.5. 明確的成功和失敗

  • 明確生成成功:
    • SUCCEED(); 生成一個(gè)成功,但這不代表整個(gè)測(cè)試就成功了。
  • 明確生成失敗:
    • FAIL(); 生成致命錯(cuò)誤
    • ADD_FAILURE(); 生成非致命錯(cuò)誤。
    • ADD_FAILURE_AT("file_path",line_number); 生成非致命錯(cuò)誤,輸出文件名和行號(hào)。

例如:

if(condition) {
	SUCCEED();
} else{
	FAIL();
}

  效果上等同于

ASSERT_TRUE(condition);

  只是ASSERT_TRUE失敗時(shí)可以輸出condition的具體值。當(dāng)?shù)覀冃枰?yàn)證的condition很復(fù)雜時(shí), 或者需要很多個(gè)if..else...分支來(lái)驗(yàn)證彼此互斥的情況以保證覆蓋到每一種可能性時(shí),SUCCEED()、FAIL()等 明確的成功/失敗可能是更好的選擇。

2.6. 異常斷言

  這些斷言驗(yàn)證一段代碼(statement)是否拋出(或不拋出)給定類(lèi)型的異常:

失敗時(shí)中斷執(zhí)行的斷言 失敗時(shí)不中斷執(zhí)行的斷言 斷言成功情況
ASSERT_THROW(statement, exception_type); EXPECT_THROW(statement, exception_type); statement拋出給定類(lèi)型的異常
ASSERT_ANY_THROW(statement); EXPECT_ANY_THROW(statement); statement拋出任何類(lèi)型的異常
ASSERT_NO_THROW(statement); EXPECT_NO_THROW(statement); statement不拋出任何異常

2.7. 使用已有布爾函數(shù)

  當(dāng)predN是一個(gè)有N個(gè)參數(shù),返回布爾值的函數(shù)時(shí),以下斷言可以獲取更好的錯(cuò)誤信息。

失敗時(shí)中斷執(zhí)行的斷言 失敗時(shí)不中斷執(zhí)行的斷言 斷言成功情況
ASSERT_PRED1(pred1, val1); EXPECT_PRED1(pred1, val1); pred1(val1)為真
ASSERT_PRED2(pred2, val1, val2); EXPECT_PRED2(pred2, val1, val2); pred2(val1, val2)為真

  例如:isComparable(Object o1, Object o2)是一個(gè)返回布爾值的函數(shù)。我們可以有以下選擇, 都能達(dá)到驗(yàn)證函數(shù)調(diào)用結(jié)果的目的:

  1. ASSERT_TRUE(isComparable(obj1, obj2));
  2. ASSERT_PRED2(isComparable, obj1, obj2);   區(qū)別在于:當(dāng)斷言失敗時(shí),ASSERT_TRUE只會(huì)告知函數(shù)最后的返回值是false;而ASSERT_TRUE同時(shí) 會(huì)輸出val1、val2的值。

3. 測(cè)試

  創(chuàng)建一個(gè)測(cè)試的步驟:

  1. 使用TEST()宏定義和命名測(cè)試功能。
  2. TEST()宏內(nèi),構(gòu)造出達(dá)到測(cè)試狀態(tài)的函數(shù)、變量
  3. 使用斷言指定函數(shù)、變量期望的返回值、值。
    TEST(MessageTestSuite, BodyLengthNegative) {
     ... 構(gòu)造 ...
     ... 斷言 ...
    }
    

  TEST()第一個(gè)參數(shù)是Test Suite的名稱(chēng),第二個(gè)參數(shù)是Test Suite內(nèi)的Test名稱(chēng)。這兩個(gè)名稱(chēng)都必須是有效的 C++標(biāo)識(shí)符,并且它們不應(yīng)包含任何下劃線(xiàn)(_)。測(cè)試的全名包括Test Suite名和Test名。來(lái)自不同Test Suite的 測(cè)試可以具有相同的Test名。它們都不是變量,也不是字符串。

  在上面的例子中,這個(gè)測(cè)試的名稱(chēng)是BodyLengthNegative,Test Suite的名稱(chēng)是MessageTestSuite

4. 測(cè)試夾具:多個(gè)測(cè)試有共有的數(shù)據(jù)配置

  如果多個(gè)測(cè)試有共有的數(shù)據(jù)配置,可以使用測(cè)試夾具(Test Fixture)將共用部分提取出來(lái)重復(fù)利用。

  要?jiǎng)?chuàng)建一個(gè)測(cè)試夾具:

  1. 創(chuàng)建一個(gè)繼承::testing::Test的類(lèi)。從protected開(kāi)始這個(gè)類(lèi),因?yàn)槲覀円獜淖宇?lèi)訪(fǎng)問(wèn)夾具成員。
  2. 在類(lèi)內(nèi)部,聲明計(jì)劃使用的任何對(duì)象。
  3. 如有必要,編寫(xiě)默認(rèn)的constructorSetUp()函數(shù)為每個(gè)測(cè)試準(zhǔn)備對(duì)象。
  4. 如有必要,編寫(xiě)一個(gè)destructorTearDown()函數(shù)以釋放在SetUp()中分配的任何資源。
  5. 如有必要,定義一些共享的類(lèi)函數(shù)

  當(dāng)使用測(cè)試夾具是,需要使用TEST_F()而不是TEST()

class TestFixtureName : public ::testing::Test {
protected:
	virtual void SetUp() {
		...
	}
	virtual void TearDown() {
		...
	}
	virtual int SomeFunction() {
		...
	}
	SomeObject object;
};

TEST_F(TestFixtureName, TestName1) {
	... 構(gòu)造 ...
	... 斷言 ...
}

TEST_F(TestFixtureName, TestName2) {
	... 構(gòu)造 ...
	... 斷言 ...
}

  那上面這個(gè)例子來(lái)說(shuō),對(duì)于每個(gè)TEST_F()測(cè)試,googletest將在運(yùn)行時(shí)

  1. 創(chuàng)建一個(gè)新的測(cè)試夾具(Test Fixture)對(duì)象
  2. 通過(guò)SetUp()對(duì)其進(jìn)行初始化
  3. 運(yùn)行該TEST_F()測(cè)試
  4. 通過(guò)調(diào)用進(jìn)行清理TearDown()
  5. 然后刪除該測(cè)試夾具(Test Fixture)對(duì)象

  所以,雖然多個(gè)TEST_F共用同一部分代碼,但共同代碼會(huì)每個(gè)TEST_F都獨(dú)立執(zhí)行一次。同一測(cè)試套件中的不同測(cè)試具有不同的測(cè)試夾具對(duì)象。一個(gè)測(cè)試對(duì)測(cè)試夾具所做的任何更改均不會(huì)影響其他測(cè)試。

三、Catch 2

  Catch2 僅有頭部文件(header only),所以它的第一個(gè)優(yōu)點(diǎn)是可以輕易地放入任何項(xiàng)目中進(jìn)行使用。只需要 #include "catch.hpp" 就可以在當(dāng)前文件使用 Catch

1. REQUIRE

  Catch的基礎(chǔ)使用方法也很簡(jiǎn)單。

#define CATCH_CONFIG_MAIN  // This tells Catch to provide a main() - only do this in one cpp file
#include "catch.hpp"
unsigned int Factorial( unsigned int number ) {
	return number <= 1 ? number : Factorial(number-1)*number;
}

TEST_CASE( "Factorials are computed", "[factorial]" ) {   
	REQUIRE( Factorial(0) == 1 );    
	REQUIRE( Factorial(1) == 1 );    
	REQUIRE( Factorial(2) == 2 );    
	REQUIRE( Factorial(3) == 6 );    
	REQUIRE( Factorial(10) == 3628800 );
}

2. SECTIONS

  Catch的SECTION相當(dāng)于GTEST里夾具(fixture)的功能。對(duì)每一個(gè)SECTION,TEST_CASE 都從頭開(kāi)始執(zhí)行。

TEST_CASE( "vectors can be sized and resized", "[vector]" ) {
    std::vector<int> v( 5 );    
    REQUIRE( v.size() == 5 );    
    REQUIRE( v.capacity() >= 5 );    
    SECTION( "resizing bigger changes size and capacity" ) 
        v.resize( 10 );        
        REQUIRE( v.size() == 10 );        
        REQUIRE( v.capacity() >= 10 );    
    }    
    SECTION( "resizing smaller changes size not capacity" ){
        v.resize( 0 );        
        REQUIRE( v.size() == 0 );        
        REQUIRE( v.capacity() >= 5 );    
    }    
    SECTION( "reserving bigger changes capacity not size" ) { 
        v.reserve( 10 );        
        REQUIRE( v.size() == 5 );        
        REQUIRE( v.capacity() >= 10 );    
    }    
    SECTION( "reserving smaller does not change size" ) {
        v.reserve( 0 );        
        REQUIRE( v.size() == 5 );        
        REQUIRE( v.capacity() >= 5 );    
    }
}

3. 標(biāo)簽

  Catch提供標(biāo)簽特性。

TEST_CASE( "A", "[widget]" ) { /* ... */ }
TEST_CASE( "B", "[widget]" ) { /* ... */ }
TEST_CASE( "C", "[gadget]" ) { /* ... */ }
TEST_CASE( "D", "[widget][gadget]" ) { /* ... */ }
  • "[widget]" 選取 A、B、D.
  • "[gadget]" 選取 C、D.
  • "[widget][gadget]" 只選取 D
  • "[widget],[gadget]" 所有A、B、C、D.
  • 還有一些特殊標(biāo)簽指定特殊行為

4. 特點(diǎn)總結(jié)

  • 框架只有一個(gè)catch.hpp、集成輕松
  • 有Given-When-Then分區(qū),適合BDD行為驅(qū)動(dòng)開(kāi)發(fā)
  • 無(wú)自帶Mock框架

四、CppUTest

1. main和test

main.cpp:

#include "CppUTest/CommandLineTestRunner.h"
int main(int ac, char** av){    
	return CommandLineTestRunner::RunAllTests(ac, av);
}

test.cpp:

#include "CppUTest/TestHarness.h"
TEST_GROUP(FirstTestGroup){
    void setup(){      
        // Init stuff   
    } 
    void teardown(){      
        // Uninit stuff   
    }
};

TEST(FirstTestGroup, FirstTest){
    FAIL("Fail me!");
}
TEST(FirstTestGroup, SecondTest){
    STRCMP_EQUAL("hello", "world");
}

2. 斷言

  • CHECK(boolean condition)檢查任何布爾結(jié)果。
  • CHECK_TEXT(boolean condition, text)檢查任何布爾結(jié)果,并在失敗時(shí)輸出文本。
  • CHECK_FALSE(condition)檢查任何布爾結(jié)果
  • CHECK_EQUAL(expected, actual)使用==檢查實(shí)體之間的相等性。因此,如果有一個(gè)支持operator==()的類(lèi),則可以使用此宏比較兩個(gè)實(shí)例。
  • CHECK_COMPARE(first, relop, second)檢查在兩個(gè)實(shí)體之間是否存在關(guān)系運(yùn)算符。失敗時(shí),打印兩個(gè)操作數(shù)求和的結(jié)果。
  • CHECK_THROWS(expected_exception, expression)檢查表達(dá)式是否拋出expected_exception(例如std::exception)。CHECK_THROWS僅在使用標(biāo)準(zhǔn)C ++庫(kù)(默認(rèn))構(gòu)建CppUTest時(shí)可用。
  • STRCMP_EQUAL(expected, actual)使用strcmp()檢查const char *字符串是否相等。
  • STRNCMP_EQUAL(expected, actual, length)使用strncmp()檢查const char *字符串是否相等。
  • STRCMP_NOCASE_EQUAL(expected, actual)不考慮大小寫(xiě),檢查const char *字符串是否相等。

3. 特點(diǎn)

  • 可以檢測(cè)內(nèi)存泄露
  • 輸出更簡(jiǎn)潔
  • 使用在嵌入式系統(tǒng)項(xiàng)目中使用

五、Google Mock

  Google Mock一般來(lái)說(shuō)和Google Test搭配使用,但Google Test也可以和其他Mock框架一起使用。 本部分是Google Mock基礎(chǔ)常用的用法,如需要特殊用法,請(qǐng)查閱Google Mock官方文檔。

1. Fake、Mock、Stub

  • Fake對(duì)象具體的實(shí)現(xiàn),但采取一些捷徑,比如用內(nèi)存替代真實(shí)的數(shù)據(jù)庫(kù)讀取。
  • Stub對(duì)象沒(méi)有具體的實(shí)現(xiàn),只是返回提前準(zhǔn)備好的數(shù)據(jù)。
  • Mock對(duì)象和Stub類(lèi)似,只是在測(cè)試中需要調(diào)用時(shí),針對(duì)某種輸入指定期望的行為。Mock和Stub的區(qū)別是, Mock除了返回?cái)?shù)據(jù)還可以指定期望以驗(yàn)證行為。

2. 簡(jiǎn)單例子:Mock Turtle

Turtle類(lèi):

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;
};

MockTurtle類(lèi):

#include "gmock/gmock.h"

class MockTurtle : public Turtle {
public:
	...
	MOCK_METHOD(void, PenUp, (), (override));
	MOCK_METHOD(void, PenDown, (), (override));
	MOCK_METHOD(void, Forward, (int distance), (override));
	MOCK_METHOD(void, Turn, (int degrees), (override));
	MOCK_METHOD(void, GoTo, (int x, int y), (override));
	MOCK_METHOD(int, GetX, (), (const, override));
	MOCK_METHOD(int, GetY, (), (const, override));
};

  創(chuàng)建Mock類(lèi)的步驟:

  1. MockTurtle繼承Turtle
  2. 找到Turtle的一個(gè)虛函數(shù)
  3. public:的部分中,寫(xiě)一個(gè)MOCK_METHOD();
  4. 將虛函數(shù)函數(shù)簽名復(fù)制進(jìn)MOCK_METHOD();中,加兩個(gè)逗號(hào):一個(gè)在返回類(lèi)型和函數(shù)名之間另一個(gè)在函數(shù)名和參數(shù)列表之間

    例如:void PenDown()有三部分:void、PenDown(),這三部分就是MOCK_METHOD的前三個(gè)參數(shù)

  5. 如果要模擬const方法,添加一個(gè)包含(const) 的第4個(gè)參數(shù)(必須帶括號(hào))。
  6. 建議添加override關(guān)鍵字。所以對(duì)于const方法,第四個(gè)參數(shù)變?yōu)?code>(const, override),對(duì)于非const方法,第四個(gè)參數(shù)變?yōu)?code>(override)。這不是強(qiáng)制性的。
  7. 重復(fù)步驟直至完成要模擬的所有虛擬函數(shù)。

3. 在測(cè)試中使用Mock

  在測(cè)試中使用Mock的步驟:

  1. testing名稱(chēng)空間導(dǎo)入gmock.h的函數(shù)名(每個(gè)文件只需要執(zhí)行一次)。
  2. 創(chuàng)建一些Mock對(duì)象。
  3. 指定對(duì)它們的期望(方法將被調(diào)用多少次?帶有什么參數(shù)?每次應(yīng)該做什么(對(duì)參數(shù)做什么、返回什么值)?等等)。
  4. 使用Mock對(duì)象;可以使用googletest斷言檢查結(jié)果。如果mock函數(shù)的調(diào)用超出預(yù)期或參數(shù)錯(cuò)誤,將會(huì)立即收到錯(cuò)誤消息。
  5. 當(dāng)Mock對(duì)象被銷(xiāo)毀時(shí),gMock自動(dòng)檢查對(duì)模擬的所有期望是否得到滿(mǎn)足。
#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
}

  在這個(gè)例子中,我們期望turtlePenDown()至少被調(diào)用一次。如果在turtle對(duì)象被銷(xiāo)毀時(shí),PenDown()還沒(méi)有被調(diào)用或者調(diào)用兩次或以上,測(cè)試會(huì)失敗。

4. 指定期望

EXPECT_CALL(指定期望)是使用Google Mock的核心。EXPECT_CALL的作用是兩方面的:

  1. 告訴這個(gè)Mock(假)方法如何模仿原始方法:

    我們?cè)?code>EXPECT_CALL中告訴Google Mock,某個(gè)對(duì)象的某個(gè)方法被第一次調(diào)用時(shí),會(huì)修改某個(gè)參數(shù),會(huì)返回某個(gè)值;第二次調(diào)用時(shí),會(huì)修改某個(gè)參數(shù),會(huì)返回某個(gè)值…….

  2. 驗(yàn)證被調(diào)用的情況

    我們?cè)?code>EXPECT_CALL中告訴Google Mock,某個(gè)對(duì)象的某個(gè)方法總共會(huì)被調(diào)用N次(或大于N次、小于N次)。如果最終次數(shù)不符合預(yù)期,會(huì)導(dǎo)致測(cè)試失敗。

4.1. 基本語(yǔ)法

EXPECT_CALL(mock_object, method(matchers))
	.Times(cardinality)
	.WillOnce(action)
	.WillRepeatedly(action);
  • mock_object 是對(duì)象
  • method(matchers) 用于匹配相應(yīng)的函數(shù)調(diào)用
  • cardinality 指定基數(shù)(被調(diào)用次數(shù)情況)
  • action 指定被調(diào)用時(shí)的行為

例子:

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

  這個(gè)EXPECT_CALL()指定的期望是:在turtle這個(gè)Mock對(duì)象銷(xiāo)毀之前,turtlegetX()函數(shù)會(huì)被調(diào)用五次。第一次返回100,第二次返回150,第三次及以后都返回200。指定期望后,5次對(duì)getX的調(diào)用會(huì)有這些行為。但如果最終調(diào)用次數(shù)不為5次,則測(cè)試失敗。

4.2. 參數(shù)匹配:哪次調(diào)用

using ::testing::_;
using ::testing::Ge;
// 只與Forward(100)匹配
EXPECT_CALL(turtle, Forward(100));
// 與GoTo(x,y)匹配, 只要x>=50
EXPECT_CALL(turtle, GoTo(Ge(50), _));
  • _相當(dāng)于“任何”。
  • 100相當(dāng)于Eq(100)。
  • Ge(50)指參數(shù)大于或等于50。
  • 如果不關(guān)心參數(shù),只寫(xiě)函數(shù)名就可以。比如EXPECT_CALL(turtle, GoTo);。

4.3. 基數(shù):被調(diào)用幾次

  用Times(m)Times(AtLeast(n))等來(lái)指定期待的調(diào)用次數(shù)。

  Times可以被省略。比如整個(gè)EXPECT_CALL只有一個(gè)WillOnce(action)相當(dāng)于也說(shuō)明了調(diào)用次數(shù)只能為1。

4.4. 行為:該做什么

  常用模式:如果需要指定前幾次調(diào)用的特殊情況,并且之后的調(diào)用情況相同。使用一系列WillOnce()之后有WillRepeatedly()

  除了用來(lái)指定調(diào)用返回值的Return(),Google Mock中常用行為中還有:SetArgPointee<N>(value), SetArgPointee將第N個(gè)指針參數(shù)(從0開(kāi)始)指向的變量賦值為value。

  比如void getObject(Object* response){...}EXPECT_CALL

Object* a = new Object;
EXPECT_CALL(object, request)
	.WillOnce(SetArgPointee<1>(*a));

  就修改了傳入的指針response,使其指向了一個(gè)我們新創(chuàng)建的對(duì)象。

  如果有多個(gè)行為,應(yīng)該使用DoAll(a1, a2, ..., an)。DoAll執(zhí)行所有n個(gè)action并返回an的結(jié)果。

4.5. 使用多個(gè)預(yù)期

例子:

using ::testing::_;
...
EXPECT_CALL(turtle, Forward(_))		// #1
	.Times(3);  	
EXPECT_CALL(turtle, Forward(10))  	// #2
	.Times(2);
...mock對(duì)象函數(shù)被調(diào)用...
	//Forward(10);						// 與#2匹配
	//Forward(20);						// 與#1匹配

  正常情況下,Google Mock以倒序搜索預(yù)期:如果和多個(gè)EXPECT_CALL都可以匹配,只有之前的, 距離調(diào)用最近的一個(gè)EXPECT_CALL()會(huì)被匹配。例如:

  • 連續(xù)三次調(diào)用Forward(10)會(huì)生錯(cuò)誤因?yàn)樗?2匹配。
  • 連續(xù)三次調(diào)用Forward(20)不會(huì)有錯(cuò)誤因?yàn)樗?1匹配。

  一旦匹配,該預(yù)期會(huì)被一直綁定,即使執(zhí)行次數(shù)達(dá)到上限之后,還是是生效的,這就是為什么三次調(diào)用 Forward(10)超過(guò)了2號(hào)EXPECT_CALL的上限時(shí),不會(huì)去試圖綁定1號(hào)EXPECT_CALL而是報(bào)錯(cuò)的原因。

  為了明確地讓某一個(gè)EXPECT_CALL“退休”,可以加上RetiresOnSaturation(),例子:

using ::testing::Return;

EXPECT_CALL(turtle, GetX())		// #1
	.WillOnce(Return(10))
	.RetiresOnSaturation();
EXPECT_CALL(turtle, GetX())		// #2
	.WillOnce(Return(20))
	.RetiresOnSaturation();

turtle.GetX()					// 與#2匹配,返回20,然后#2“退休”
turtle.GetX()					// 與#1匹配,返回10

  在這個(gè)例子中,第一次GetX()調(diào)用和#2匹配,返回20,然后這個(gè)EXPECT_CALL就“退休”了; 第二次GetX()調(diào)用和#1匹配,返回10

4.6. Sequence

  可以用sequence來(lái)指定期望匹配的順序。

using ::testing::Return;
using ::testing::Sequence;
Sequence s1, s2;
...
EXPECT_CALL(foo, Reset())
    .InSequence(s1, s2)
    .WillOnce(Return(true));
EXPECT_CALL(foo, GetSize())
    .InSequence(s1)
    .WillOnce(Return(1));
EXPECT_CALL(foo, Describe(A<const char*>()))
    .InSequence(s2)
    .WillOnce(Return("dummy"));

  在上面的例子中,創(chuàng)建了兩個(gè)Sequence s1s2,屬于s1的有Reset()GetSize(), 所以Reset()必須在GetSize()之前執(zhí)行。屬于s2的有Reset()Describe(A<const char*>()), 所以Reset()必須在Describe(A<const char*>())之前執(zhí)行。所以,Reset()必須在GetSize()Describe()之前執(zhí)行。而GetSize()Describe()這兩者之間沒(méi)有順序約束。

  如果需要指定很多期望的順序,有另一種用法:

using ::testing::InSequence;
{
  InSequence seq;

  EXPECT_CALL(...)...;
  EXPECT_CALL(...)...;
  ...
  EXPECT_CALL(...)...;
}

  在這種用法中,scope中(大括號(hào)中)的期望必須遵守嚴(yán)格的順序。

5. 更多

六、情景示例

  在這部分,我們用一個(gè)示例項(xiàng)目來(lái)演示,如何在不同情景中使用 Google Test和Google Mock寫(xiě)單元測(cè)試用例。

1. 項(xiàng)目結(jié)構(gòu)

  示例項(xiàng)目是一個(gè)C++命令行聊天室軟件,包含服務(wù)器和客戶(hù)端。

.
├── CMakeLists.txt
├── README.md
├── client_main.cpp
├── server_main.cpp
├── include
│   ├── chat_client.hpp
│   ├── chat_message.hpp
│   ├── chat_participant.hpp
│   ├── chat_room.hpp
│   ├── chat_server.hpp
│   ├── chat_session.hpp
│   ├── http_request.hpp
│   ├── http_request_impl.hpp
│   ├── message_dao.hpp
│   └── message_dao_impl.hpp
├── src
│   ├── chat_client.cpp
│   ├── chat_message.cpp
│   ├── chat_room.cpp
│   ├── chat_server.cpp
│   ├── chat_session.cpp
│   ├── http_request_impl.cpp
│   └── message_dao_impl.cpp
└── tests
    ├── chat_message_unittest.cpp
    └── chat_room_unittest.cpp

2. 普通測(cè)試

  如果被測(cè)試的函數(shù)不包含外部依賴(lài),用Google Test基礎(chǔ)的用法就可以完成用例編寫(xiě)。

原函數(shù):

void chat_message::body_length(std::size_t new_length) {
    body_length_ = new_length;
    if (body_length_ > 512)
        body_length_ = 512;
}

  這個(gè)函數(shù)很簡(jiǎn)單。就是給body_length_賦值但是有最大值限制。測(cè)試用例可以這樣寫(xiě):

TEST(ChatMessageTest, BodyLengthNegative) {
    chat_message c;
    c.body_length(-50);
    EXPECT_EQ(512, c.body_length());
}

TEST(ChatMessageTest, BodyLength0) {
    chat_message c;
    c.body_length(0);
    EXPECT_EQ(0, c.body_length());
}

TEST(ChatMessageTest, BodyLength100) {
    chat_message c;
    c.body_length(100);
    EXPECT_EQ(100, c.body_length());
}

TEST(ChatMessageTest, BodyLength512) {
    chat_message c;
    c.body_length(512);
    EXPECT_EQ(512, c.body_length());
}

TEST(ChatMessageTest, BodyLength513) {
    chat_message c;
    c.body_length(513);
    EXPECT_EQ(512, c.body_length());
}

  我們可以看到,對(duì)于這類(lèi)函數(shù),用例編寫(xiě)很直接簡(jiǎn)單,步驟都是構(gòu)造變量,再用合適的Google Test的 宏來(lái)驗(yàn)證變量值或者函數(shù)調(diào)用返回值。

3. 簡(jiǎn)單 Mock

原函數(shù)

void chat_room::leave(chat_participant_ptr participant) {
    participants_.erase(participant);
}

  participants_ 的類(lèi)型是 std::set<chat_participant_ptr>。這個(gè)函數(shù)的目的很明顯,將一個(gè)participantset中移除。

  真實(shí)地創(chuàng)建一個(gè)聊天參與者participant對(duì)象可以條件比較苛刻或者成本比較高。為了有效率地驗(yàn)證這個(gè)函數(shù),我們可以新建一些Mock的chat_participant_ptr而不用嚴(yán)格地去創(chuàng)建真實(shí)的participant對(duì)象。

chat_participant對(duì)象:

class chat_participant {
public:
    virtual ~chat_participant() {}
    virtual void deliver(const chat_message &msg) = 0;
};

Mock對(duì)象:

class mock_chat_participant : public chat_participant {
public:
    MOCK_METHOD(void, deliver, (const chat_message &msg), (override));
};

測(cè)試用例:

TEST(ChatRoomTest, leave) {
    auto p1 = std::make_shared<mock_chat_participant>();	//新建第一個(gè)Mock指針
    auto p2 = std::make_shared<mock_chat_participant>();	//新建第二個(gè)Mock指針
    auto p3 = std::make_shared<mock_chat_participant>();	//新建第三個(gè)Mock指針
    auto p4 = std::make_shared<mock_chat_participant>();	//新建第四個(gè)Mock指針
    chat_room cr;											//新建待測(cè)試對(duì)象chat_room
    cr.join(p1);
    cr.join(p2);
    cr.join(p3);
    cr.join(p4);
    EXPECT_EQ(cr.participants().size(), 4);
    cr.leave(p4);
    EXPECT_EQ(cr.participants().size(), 3);
    cr.leave(p4);
    EXPECT_EQ(cr.participants().size(), 3);
    cr.leave(p2);
    EXPECT_EQ(cr.participants().size(), 2);
}

4. Web請(qǐng)求

  chat_room中有一個(gè)log(),依賴(lài)網(wǎng)絡(luò)請(qǐng)求。原函數(shù):

std::string chat_room::log() {
    std::string* response;
    this->requester->execute("request",response);		// web訪(fǎng)問(wèn),結(jié)果存在response指針中
    return *response;
}

  在單元測(cè)試中,我們只關(guān)心被測(cè)試部分的邏輯。為了測(cè)試這個(gè)函數(shù),我們不應(yīng)該創(chuàng)建真實(shí)的requester,應(yīng)該使用mock。

http_request對(duì)象:

class http_request {
public:
    virtual ~http_request(){}
    virtual bool execute(std::string request, std::string* response)=0;
};

Mock對(duì)象:

class mock_http_request : public http_request {
public:
    MOCK_METHOD(bool, execute, (std::string request, std::string * response), (override));
};

測(cè)試用例:

TEST(ChatRoomTest, log) {
    testing::NiceMock<mock_message_dao> mock_dao;	//在下一部分會(huì)提到mock_message_dao
    mock_http_request mock_requester;				//Mock對(duì)象
    std::string response = "response";				//期待調(diào)用函數(shù)的第二個(gè)參數(shù)將指向這個(gè)string對(duì)象
    EXPECT_CALL(mock_requester, execute)
    	.WillRepeatedly(							//每次調(diào)用都會(huì)(WillRepeatedly)執(zhí)行
    		testing::DoAll(							//每次執(zhí)行包含多個(gè)行為
    			testing::SetArgPointee<1>(response),//將傳入?yún)?shù)指針變量response指向response
    			testing::Return(true)));			//返回值為true
    chat_room cr 
    	= chat_room(&mock_dao, &mock_requester);	//將mock對(duì)象通過(guò)chat_room的constructor注入
    EXPECT_EQ(cr.log(),"response");					//調(diào)用和Google Test斷言
}

5. 數(shù)據(jù)庫(kù)訪(fǎng)問(wèn)

  chat_room對(duì)象會(huì)將聊天者發(fā)送的消息存儲(chǔ)在redis數(shù)據(jù)庫(kù)中。當(dāng)新用戶(hù)加入時(shí),chat_room對(duì)象從數(shù)據(jù)庫(kù) 獲取所有歷史消息發(fā)送給該新用戶(hù)。

join()函數(shù):

void chat_room::join(chat_participant_ptr participant) {
    participants_.insert(participant);
    std::vector<std::string> recent_msg_strs = 
    	this->dao->get_messages(); 			//從數(shù)據(jù)庫(kù)中獲取歷史消息
    for (std::string recent_msg_str: recent_msg_strs) {
										    //將每一個(gè)消息發(fā)送給該聊天參與者	
        auto msg = chat_message();
        msg.set_body_string(recent_msg_str);
        participant->deliver(msg);
    }
}

message_dao對(duì)象:

class message_dao {
public:
    virtual ~message_dao(){}
    virtual bool add_message(std::string m)=0;
    virtual std::vector<std::string> get_messages()=0;
};

Mock對(duì)象:

class mock_message_dao : public message_dao {
public:
    MOCK_METHOD(bool, add_message, (std::string m), (override));
    MOCK_METHOD(std::vector<std::string>, get_messages, (), (override));
};

測(cè)試用例:

TEST(ChatRoomTest, join) {
    mock_message_dao mock_dao;				//創(chuàng)建mock對(duì)象(需要注入chat_room)
    http_request_impl requester;			//創(chuàng)建web訪(fǎng)問(wèn)對(duì)象(也需要注入chat_room)
    auto mock_p1 = std::make_shared<mock_chat_participant>();
    										//創(chuàng)建participant的mock指針
    EXPECT_CALL(mock_dao, get_messages)
    	.WillOnce(testing::Return(std::vector<std::string>{"test_msg_body_1", "test_msg_body_2", "test_msg_body_3"}));
    										//指定get_messages調(diào)用的返回值
    EXPECT_CALL(*mock_p1, deliver).Times(3);
    										//指定deliver調(diào)用的次數(shù)
    chat_room cr = chat_room(&mock_dao, &requester);
    										//創(chuàng)建chat_room對(duì)象,注入dao和requester
    cr.join(mock_p1);						//調(diào)用
}

  先創(chuàng)建mock對(duì)象,再指定函數(shù)調(diào)用的預(yù)期,最后指向被測(cè)試函數(shù)。我們可以看到,mock_dao指定了get_messages的 返回值時(shí)一個(gè)長(zhǎng)度為3的vector,所以有3條消息會(huì)被deliver。

七、FAQ

1. 單元測(cè)試源文件應(yīng)該放在項(xiàng)目的什么位置?

  一般來(lái)說(shuō),我們會(huì)在根目錄創(chuàng)建一個(gè)tests文件夾,里面放單元測(cè)試部分的源代碼,從而不會(huì)和被測(cè)試代碼混在一起。

  如果需要和其他測(cè)試(如接口測(cè)試、壓力測(cè)試)等區(qū)分開(kāi)來(lái),可以

  1. tests改成unittests、utests等,或者
  2. tests創(chuàng)建不同子文件夾存放不同類(lèi)型的測(cè)試代碼。

2. Google Mock只能Mock虛函數(shù),如果我想Mock非虛函數(shù)怎么辦?

  由于Google Mock(及其他大部分Mock框架)通過(guò)繼承來(lái)動(dòng)態(tài)重載機(jī)制的限制,一般來(lái)說(shuō)Google Mock只能Mock虛函數(shù)。如果要mock非虛函數(shù),官方文檔提供這幾種思路:

  1. Mock類(lèi)和原類(lèi)沒(méi)有繼承關(guān)系,在測(cè)試對(duì)象使用函數(shù)模板。在測(cè)試中,測(cè)試對(duì)象接受Mock類(lèi)。
  2. 創(chuàng)建一個(gè)接口(抽象類(lèi)),原類(lèi)繼承自這個(gè)接口(抽象類(lèi))。在測(cè)試中Mock這個(gè)接口(抽象類(lèi))。

  這兩種方法,都需要對(duì)代碼進(jìn)行一定的修改或重構(gòu)。如果不想修改被測(cè)試代碼??梢钥紤]使用hook技術(shù)替換被mock的部分從而mock一般函數(shù)。

使用TMock對(duì)非虛函數(shù)mock的例子:

mock函數(shù)

# include "tmock.h"

class MockClass
{
public:
    //注冊(cè)mock類(lèi)
    TMOCK_CLASS(MockClass);
    //聲明mock類(lèi)函數(shù),TMOCK_METHOD{n}第一個(gè)參數(shù)與attach_func_lib第一個(gè)參數(shù)相同,其余參考與MOCK_METHOD{n}一致。
    TMOCK_METHOD1("original", original, uint32_t(const char * str_file_md5) )
};

  單測(cè)中應(yīng)用tmock的方法和Google Mock基本一致。但在結(jié)束的時(shí)候需要使用TMOCK_CLEAR清除exception, detach hook的函數(shù),防止干擾其他單元測(cè)試。

3. Google Test官方文檔中說(shuō)測(cè)測(cè)試套件名稱(chēng)、測(cè)試夾具名稱(chēng)、測(cè)試名稱(chēng)中不應(yīng)該出現(xiàn)下劃線(xiàn)_。為什么?

  TEST(TestSuiteName, TestName)生成名為TestSuiteName_TestName_Test的類(lèi)。

  下劃線(xiàn)_是特殊的,因?yàn)镃 ++保留以下內(nèi)容供編譯器和標(biāo)準(zhǔn)庫(kù)使用。所以開(kāi)頭和結(jié)尾有下劃線(xiàn)很容易讓生成的類(lèi)的標(biāo)識(shí)符不合法。

  另一方面,下劃線(xiàn)可能讓不同測(cè)試生成相同的類(lèi)。比如TEST(Time,F(xiàn)lies_Like_An_Arrow){...} TEST(Time_Flies,Like_An_Arrow){...} 都生成名為Time_Flies_Like_An_Arrow_Test的類(lèi)。

4. 測(cè)試輸出里有很多Uninteresting mock function call警告怎么辦?

  創(chuàng)建的Mock的對(duì)象的某些調(diào)用如果沒(méi)有相應(yīng)匹配的EXPECT_CALL,Google Mock會(huì)生成這個(gè)警告。

  為了去除這個(gè)警告,可以使用NiceMock。比如如果原本使用MockFoo nice_foo;新建mock對(duì)象的話(huà),可以改成NiceMock<MockFoo> nice_foo;。NiceMock<MockFoo>MockFoo的子類(lèi)。

八、結(jié)語(yǔ)

1. 實(shí)踐小結(jié)

  和GoLang單元測(cè)試框架有些區(qū)別的是,GoLang自生就提供了自帶的測(cè)試框架,也有第三方框架進(jìn)行選擇。 而C/C++/php等語(yǔ)言的單元測(cè)試框架則需要第三方提供和安裝。

  框架的使用,無(wú)非是一些語(yǔ)法糖的差異和使用的難易程度。不管使用什么語(yǔ)言,什么框架,最關(guān)鍵的是利用單元測(cè)試的思路, 寫(xiě)出解耦的、可測(cè)試的、易于維護(hù)的代碼,保證代碼的質(zhì)量。

  單元測(cè)試是一種手段,能夠一定程度的改善生產(chǎn)力。凡事有度過(guò)猶不及,如果一味的盲目的追求測(cè)試覆蓋率, 忽視了測(cè)試代碼本身的質(zhì)量,那么各種無(wú)效的單元測(cè)試反而帶來(lái)了沉重的維護(hù)負(fù)擔(dān)。因此單測(cè)的代碼,本身也是代碼, 也是和項(xiàng)目本身的代碼一樣,需要重構(gòu)、維護(hù)的(好好寫(xiě)代碼)。

2. 特別鳴謝

  感謝實(shí)習(xí)生鐘梓軒,在暑假實(shí)習(xí)期間,主導(dǎo)整理了C++單測(cè)的代碼示例和部分文章內(nèi)容。

3. 相關(guān)閱讀

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶(hù)發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶(hù) 評(píng)論公約

    類(lèi)似文章 更多

    日本不卡一区视频欧美| 国产三级视频不卡在线观看| 精品女同在线一区二区| 欧美午夜不卡在线观看| 久久机热频这里只精品| 日韩精品视频高清在线观看| 国产在线视频好看不卡| 亚洲婷婷开心色四房播播| 亚洲国产一级片在线观看| 福利在线午夜绝顶三级| 蜜臀人妻一区二区三区| 亚洲一区二区三区av高清| 五月激情婷婷丁香六月网| 国内精品偷拍视频久久| 国产精品超碰在线观看| 视频在线免费观看你懂的| 人妻乱近亲奸中文字幕| 国产成人综合亚洲欧美日韩| 国语对白刺激高潮在线视频| 国产麻豆一区二区三区在| 日韩美成人免费在线视频| 樱井知香黑人一区二区| 国产精品久久精品毛片| 超薄丝袜足一区二区三区| 欧美日韩国产欧美日韩| 午夜福利激情性生活免费视频| 久久福利视频视频一区二区| 日韩高清毛片免费观看| 国产偷拍精品在线视频| 欧美一区二区三区十区| 亚洲国产丝袜一区二区三区四| 国产精品内射婷婷一级二级| 国产成人精品国产亚洲欧洲| av在线免费观看一区二区三区| 日本黄色录像韩国黄色录像| 人妻中文一区二区三区| 精品久久av一二三区| 欧美性欧美一区二区三区| 欧美黑人黄色一区二区| 大伊香蕉一区二区三区| 久久午夜福利精品日韩|