日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
談一談單元測(cè)試

寫(xiě)在前面

為了寫(xiě)而寫(xiě)的單元測(cè)試沒(méi)什么價(jià)值,但一個(gè)好的單元測(cè)試帶來(lái)的收益是非常客觀的。問(wèn)題是怎么去寫(xiě)好單元測(cè)試?怎么去驅(qū)動(dòng)寫(xiě)好單元測(cè)試?

一 、我們的現(xiàn)狀

現(xiàn)狀一:多個(gè)項(xiàng)目完全沒(méi)有單元測(cè)試。

現(xiàn)狀二:開(kāi)發(fā)人員沒(méi)有寫(xiě)單元測(cè)試的習(xí)慣,或者由于趕業(yè)務(wù)記錄而沒(méi)有時(shí)間去寫(xiě)。

現(xiàn)狀三:?jiǎn)卧獪y(cè)試寫(xiě)成了集成測(cè)試,比如容器、數(shù)據(jù)庫(kù),導(dǎo)致單元測(cè)試運(yùn)行時(shí)間長(zhǎng),失去了意義。

現(xiàn)狀四:太依賴集成測(cè)試。

以上是我在aone找的兩個(gè)項(xiàng)目的測(cè)試情況,基本不考慮單元測(cè)試就合并發(fā)布,形同虛設(shè)。

站在開(kāi)發(fā)的角度講,導(dǎo)致以上問(wèn)題的原因大概有以下幾點(diǎn):

開(kāi)發(fā)成本

對(duì)于系統(tǒng)初期,可能要花很多時(shí)間去寫(xiě)新業(yè)務(wù),對(duì)于老系統(tǒng)又太過(guò)龐大,無(wú)法下手。

維護(hù)成本

每修改相關(guān)的類,或者重構(gòu)一次代碼,我們就要去修改相應(yīng)的單元測(cè)試。

ROI

投入產(chǎn)出是不是正收益?可能無(wú)論是管理者還是我們開(kāi)發(fā)自己都回質(zhì)疑這個(gè)問(wèn)題,所以有時(shí)候沒(méi)有強(qiáng)有力的動(dòng)力。

二、 怎么解決

說(shuō)來(lái)說(shuō)去都是成本的問(wèn)題,所以我們?cè)趺慈ソ鉀Q成本呢?

那么,我們一切從最開(kāi)始說(shuō)起:開(kāi)發(fā)的成本

一個(gè)單元測(cè)試的傳統(tǒng)寫(xiě)法,包含以下幾個(gè)方面:

  • 測(cè)試數(shù)據(jù) (被測(cè)數(shù)據(jù),和依賴對(duì)象)
  • 測(cè)試方法
  • 返回值斷言
@Test
public void testAddGroup() {
// 數(shù)據(jù)
BuyerGroupDTO groupDTO = new BuyerGroupDTO();
groupDTO.setGmtCreate(new Date());
groupDTO.setGmtModified(new Date());
groupDTO.setName("中國(guó)");
groupDTO.setCustomerId(customerId);
// 方法
Result result = customerBuyerDomainService.addBuyerGroup(groupDTO);
// 返回值斷言
Assert.assertTrue(result.isSuccess());
Assert.assertNotNull(result.getData());
}

一個(gè)簡(jiǎn)單的測(cè)試還好,但如果是一邏輯復(fù)雜,且入?yún)?shù)據(jù)復(fù)雜的時(shí)候,那寫(xiě)起來(lái)其實(shí)挺頭痛的。怎么解放我們程序員的雙手?

“工欲善其事必先利其器”

我們以最大的努力降低我們的開(kāi)發(fā)成本,這就涉及到我們測(cè)試框架和工具的選擇問(wèn)題

1. 測(cè)試框架選擇

首先第一個(gè)問(wèn)題就是junit4和junit5的選擇,【從junit4到j(luò)unit5】 我覺(jué)得最便利的一個(gè)好處就是可以參數(shù)化測(cè)試,并且基于參數(shù)化測(cè)試我們可以更加靈活的配置我們的參數(shù)。

效果如下:

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}

更好的是,junit5提供了擴(kuò)展,比如我們常用的json格式。這里我們使用json文件作為輸入:

@ParameterizedTest
@JsonFileSource(resources = {"/com/cq/common/KMPAlgorithm/test.json"})
public void test2Test(JSONObject arg) {
Animal animal = JSONObject.parseObject(arg.getString("Animal"),Animal.class);
List stringList = JSONObject.parseArray(arg.getString("List"),String.class);
when(testService.testOther(any(Student.class))).thenReturn(stringArg);
when(testService.testMuti(any(List.class),any(Integer.class))).thenReturn(stringList);
when(testService.getAnimal(any(Integer.class))).thenReturn(animal);
String result = kMPAlgorithm.test2();
//todo verify the result
}

2. mock框架

然后就是其他mock類的框架了

Mockito: 語(yǔ)法特別優(yōu)雅,對(duì)于容器類的模擬比較合適,且對(duì)于返回值為空的函數(shù)調(diào)用也提供比較好的斷言。缺點(diǎn)是不能模擬靜態(tài)方法(3.4.x以上版本已支持)

EasyMock: 使用方法類似,但是更嚴(yán)格

PowerMock: 可以作為Mockito的一個(gè)補(bǔ)充,比如要測(cè)試靜態(tài)方法,不過(guò)不支持junit5

Spock: 基于Groovy語(yǔ)言的單元測(cè)試框架

3. 數(shù)據(jù)庫(kù)層

這里主要介紹一下H2數(shù)據(jù)庫(kù),其基于內(nèi)存來(lái)作為對(duì)于關(guān)系型數(shù)據(jù)庫(kù)的模擬,運(yùn)行完成自動(dòng)釋放,達(dá)到隔離的目的。

主要配置:ddl文件路徑、dml文件路徑。這里不作詳述。

但對(duì)于要不要集成數(shù)據(jù)庫(kù),很難去定義,它的作用主要是用來(lái)驗(yàn)證sql語(yǔ)法的問(wèn)題,但是相對(duì)來(lái)說(shuō)較重,建議可以用于輕量級(jí)的集成測(cè)試。

三、 Junit5和Mockito

后面講到的自動(dòng)生成使用的框架和業(yè)界使用最多的都是MocKito,所以這里重點(diǎn)介紹一下,包括使用時(shí)遇到的問(wèn)題。

1. 使用方法

分別單獨(dú)引入依賴,推薦引入最新版



org.junit.jupiter
junit-jupiter
5.7.2
test




org.mockito
mockito-core
3.9.0
test



org.mockito
mockito-junit-jupiter
3.9.0
test

使用spring-test全家桶


org.springframework.boot
spring-boot-starter-test
test
2.5.0

junit5的使用方法這里就不多做介紹,主要說(shuō)一下這個(gè)ArgumentsProvider接口,實(shí)現(xiàn)它就可以自定義參數(shù)化類,類似于自帶的ValueSource、EnumSource等。

2. Mockito 主要注解介紹

先問(wèn)為什么,為什么需要Mockito

因?yàn)椋含F(xiàn)在的java項(xiàng)目幾乎離不開(kāi)spring框架,而其最為著名的就是IOC,所有的bean用容器來(lái)管理,所以這給我們單元測(cè)試帶來(lái)一個(gè)問(wèn)題,如果要對(duì)bean做單元測(cè)試,就需要啟動(dòng)容器,那么帶來(lái)的時(shí)間的開(kāi)銷將會(huì)很大。所以Mockito給我門(mén)帶來(lái)了一系列的解決方法,讓我們可以輕松的對(duì)bean 進(jìn)行測(cè)試。

@Component
public class A {
@Autowired
private B b; // 完全mock
@Autowired
private C c; // 需要執(zhí)行方法
@Autowired D d; // 需要執(zhí)行真實(shí)方法
public void func(){
}
}
@Component
class C {
@Autowired
private B b;
public void needExec(){
}
}
@Component
public class B {
}

假設(shè)我們要對(duì)上面的A.func()進(jìn)行單元測(cè)試。

@InjectMocks注解

表示需要注入bean的類,有兩種

  • 被測(cè)試類,這種很容易理解,我們測(cè)試這個(gè)類,當(dāng)然也需要向其注入bean。比如上面的A
  • 被測(cè)試類中的,需要執(zhí)行其真實(shí)的方法,但其里面也要主要bean,也就是上面的C,我們需要測(cè)試neeExec方法,但我們不關(guān)系B的具體細(xì)節(jié)?,F(xiàn)實(shí)中比如事物,并發(fā)鎖等。這一類需要Mockito.spy(new C())的形式,不然會(huì)報(bào)錯(cuò)

@Mock

表示要mock的數(shù)據(jù),也就是不真實(shí)執(zhí)行其方法內(nèi)容,只按照我們的規(guī)則執(zhí)行,或者返回,比如使用when().thenReturn()語(yǔ)法。

當(dāng)然也可以,執(zhí)行真實(shí)方法,則需要when().thenCallRealMethod()方式。

@Spy

表示所有方法都走真實(shí)方式,比如有些工具類,轉(zhuǎn)換類,我們也寫(xiě)成了bean的形式(嚴(yán)格來(lái)說(shuō)這種需要寫(xiě)成靜態(tài)工具類)。

@ExtendWith(MockitoExtension.class)
public class ATest {
@InjectMocks
private A a=new A();
@Mock
private B b;
@Spy
private D d;
@InjectMocks
private C c= Mockito.spy(new C());;

@BeforeEach
public void setUp() throws Exception {
MockitoAnnotations.openMocks(this);
}
@ParameterizedTest
@ValueSource(strings = {"/com/alibaba/cq/springtest/jcode5test/needMockService/A/func.json"})
public void funcTest(String str) {
JSONObject arg= TestUtils.getTestArg(str);
a.func();
//todo verify the result
}

}

3. Mockito和junit5常見(jiàn)問(wèn)題

mock靜態(tài)方法

mockito3.4以后開(kāi)始支持,之前的版本可以使用PowerMock輔助使用

Mockito版本和java版本兼容問(wèn)題

報(bào)錯(cuò)如下

Mockito cannot mock this class: xxx
Mockito can only mock non-private & non-final classes.

原因是2.17.0及之前的版本與java8是兼容的

但2.18之后需要使用java11,為了在java8中使用Mockito,則需要引入另一個(gè)包


net.bytebuddy
byte-buddy
1.12.6

Jupiter-api版本兼容問(wèn)題

Process finished with exit code 255
java.lang.NoSuchMethodError: org.junit.jupiter.api.extension.ExtensionContext.getRequiredTestInstances()Lorg/junit/jupiter/api/extension/TestInstance

第一個(gè)問(wèn)題是因?yàn)閖unit5中api、engine、params版本不一致導(dǎo)致的。

第二個(gè)問(wèn)題是因?yàn)閖upiter-api版本太低的問(wèn)題,5.7.0以后的版本才支持。

四 、測(cè)試代碼自動(dòng)生成

選好了框架,我們還是沒(méi)有解決我們的問(wèn)題,“怎么節(jié)約開(kāi)發(fā)成本?” ,這一節(jié)我們來(lái)談這個(gè)問(wèn)題,這也是我主要想表達(dá)的。

對(duì)于寫(xiě)單元測(cè)試,一直以來(lái)是比較頭痛的事情,要組裝各種各樣的數(shù)據(jù),可能還沒(méi)跑成功,就被一堆“xxxx不能為null”的報(bào)錯(cuò)搞煩了。因此我們有理由去設(shè)想,有沒(méi)有辦法去解決這件事情。

1. 業(yè)界和集團(tuán)方案調(diào)研

在做這個(gè)事情之前,肯定是要調(diào)研有沒(méi)有現(xiàn)成的框架。答案是有,但很遺憾,沒(méi)有找到完全契合我想要的效果,我們來(lái)看一下這些插件:

public class BaseTest {
protected TestService testService;
public String baseTest() {
return testService.testBase(1); // 4
}
}
public class JCode5 extends BaseTest {
public void testExtend(){
String s = testService.testOther(new Student()); //1
// 調(diào)用 另一個(gè)方法
System.out.println(testBean());
// 調(diào)用基類方法
baseTest();
}
// 使用testService
public String testBean() {
testService.testMuti(new ArrayList() {{add(1);}}, 2); //2
return testService.getStr(12); //3
}
/**
* 測(cè)試范型類
*/
public void testGeneric(Person person) {
//test
list.stream().forEach(a -> {
System.out.println(a);
});
for (int i = 0; i < 2; i++) {
Long aLong = testService.getLong("1213"
, "12323");
System.out.println(aLong);
}
System.out.println(testBean());
}
}
public class TestService {
public String testBase(Integer integer) {
return "TestBase";
}
public List testMuti(List a, Integer c) {
List res = new ArrayList<>();
res.add(a.toString() + c + "test muti");
return res;
}
public String getStr(Integer integer) {
return "TestService" + getInt();
}
public String testOther(Student student) {
return student.getAge() + "age";
}

}

如上,testExtend一共調(diào)用了testService的4個(gè)方法,我們對(duì)比下各個(gè)插件生成的代碼。

TestMe

@Test
void testTestExtend() {
when(testService.getStr(anyInt())).thenReturn("getStrResponse");
when(testService.testMuti(any(), anyInt())).thenReturn(Arrays.asList("String"));
when(testService.testOther(any())).thenReturn("testOtherResponse");
jCode5.testExtend(Integer.valueOf(0));
}
@Test
void testTestGeneric() {
when(testService.getStr(anyInt())).thenReturn("getStrResponse");
when(testService.getLong(anyString(), anyString())).thenReturn(Long.valueOf(1));
when(testService.testMuti(any(), anyInt())).thenReturn(Arrays.asList("String"));

jCode5.testGeneric(new Person());
}

生成的代碼基本符合邏輯,包括需要mock的bean的邏輯都生成了。

  • 但它把最重要的一環(huán),也就是數(shù)據(jù)省略了,只是單純的用了構(gòu)造函數(shù)的形式。這顯然對(duì)于我們DDD模型不適應(yīng)。
  • 另外他沒(méi)用用到j(luò)unit5的一些特性,比如參數(shù)化測(cè)試。
  • 對(duì)于testExtend的方法,它只識(shí)別了3個(gè)方法。沒(méi)有識(shí)別父類的調(diào)用。

JunitGenerate

只能生成基礎(chǔ)的框架代碼,對(duì)于我想mock的邏輯、以及測(cè)試方法都沒(méi)有生成,用處不大。

@Test
public void testTestExtend() throws Exception {
//TODO: Test goes here...
}

Squaretest

生成的方法非常豐富,且一個(gè)非常厲害的一點(diǎn),它能生成多個(gè)分支,比如代碼邏輯中有if條件,它能生成兩個(gè)測(cè)試,從而走不通的分支。

但是,最大的缺點(diǎn)是“收費(fèi)軟件,不開(kāi)源”,這就決定了我們沒(méi)法用它,除非是特別需要。另外測(cè)試用過(guò)程中還發(fā)現(xiàn)了一些其他問(wèn)題,比如對(duì)于繼承,重載之類的問(wèn)題,它解決的也不是很好,往往識(shí)別不了需要調(diào)用的方法。

雖然無(wú)法使用,但還是可以借鑒。

五 、打造代碼自動(dòng)生成最佳方案

既然市場(chǎng)上的插件都不是特別合適,就決定寫(xiě)一個(gè)適合自己項(xiàng)目的插件(暫時(shí)命名JCode5)。有興趣的也可以自己試試。

1. 插件安裝

idea插件市場(chǎng)下載,搜索JCode5

2. 插件使用

插件有三個(gè)功能

  • 生成測(cè)試代碼,也就是生成單元測(cè)試。
  • 生成json數(shù)據(jù),通常用來(lái)生成測(cè)試數(shù)據(jù),比如model。用來(lái)參數(shù)化測(cè)試。
  • 增加測(cè)試方法,隨著業(yè)務(wù)開(kāi)發(fā),類可能增加一下功能方法,這個(gè)時(shí)候相應(yīng)的可以增加測(cè)試方法

定位到需要測(cè)試的類,快捷鍵或菜單定位到generater,如下,選擇JCode5.

生成測(cè)試類

目前支持三個(gè)選項(xiàng),后續(xù)會(huì)逐漸完善

另外兩個(gè)功能類似,直接嘗試使用一下就行。

生成的結(jié)果---類+json數(shù)據(jù)

@ParameterizedTest
@ValueSource(strings = {"/com/cq/common/JCode5/testExtend.json"})
public void testExtendTest(String str) {
JSONObject arg= TestUtils.getTestArg(str);
Integer i = arg.getInteger("Integer");
// 識(shí)別泛型活著集合類
List stringList = JSONObject.parseArray(arg.getString("List"),String.class);
String stringArg = arg.getString("String");
String stringArg1 = arg.getString("String");
String stringArg0 = arg.getString("String");
// 識(shí)別四個(gè)方法,包括父類調(diào)用、其他方法調(diào)用
when(testService.testBase(any(Integer.class))).thenReturn(stringArg);
when(testService.testMuti(any(List.class),any(Integer.class))).thenReturn(stringList);
when(testService.getStr(any(Integer.class))).thenReturn(stringArg0);
when(testService.testOther(any(Student.class))).thenReturn(stringArg1);
jCode5.testExtend(i);
//todo verify the result
}

如上除了生成基本的代碼,另外會(huì)生成測(cè)試數(shù)據(jù),它會(huì)將該方法所需要的測(cè)試數(shù)據(jù)全都生成在一個(gè)json文件當(dāng)中,完全實(shí)現(xiàn)

“數(shù)據(jù)和代碼的分離”

如testExtend.json:

{
"Integer":1,
"String":"test",
"List":[
"test"
]
}

補(bǔ)充判定語(yǔ)句

這一塊前期考慮對(duì)于不同的方法有不同的校驗(yàn),所以目前想的還是開(kāi)發(fā)者自己去寫(xiě)驗(yàn)證代碼。

注意事項(xiàng)

在自動(dòng)生成完代碼之后,雖然可以運(yùn)行,但如我們前面提到的,為了寫(xiě)單元測(cè)試而寫(xiě)的單元測(cè)試是沒(méi)什么價(jià)值的,我們的最終目的是為了寫(xiě)一個(gè)好的測(cè)試。代碼自動(dòng)生成,但它終究能力有限,所以還是需要我們自己再去驗(yàn)證,比如

  • 該插件生成的代碼需要junit5和mockito的支持,使用時(shí)需要引入相關(guān)的依賴
  • 增加assert校驗(yàn)邏輯,看是不是想要的結(jié)果,目前插件不會(huì)自動(dòng)生成assertEquals等斷言代碼。
  • 運(yùn)用參數(shù)化測(cè)試能力,復(fù)制一份生成的json文件并修改輸入數(shù)據(jù),多組測(cè)試

3. 插件實(shí)現(xiàn)介紹

主要的實(shí)現(xiàn)思路,參考了dubbo的SPI的源碼,也就是自動(dòng)實(shí)現(xiàn)自適應(yīng)SPI那部分,簡(jiǎn)單點(diǎn)說(shuō)就是反射獲取代碼邏輯,然后生成測(cè)試代碼。

4. 后期規(guī)劃

mock數(shù)據(jù)可定制,目前的想法是

固定值比如目前的String: test、Integer和boolean: 0、1

測(cè)試者使用配置模版,比如txt文件包含keyValue對(duì)

使用Faker,對(duì)于name、email、phone這種特定傾向的數(shù)據(jù)進(jìn)行特色自動(dòng)生成

自動(dòng)分支測(cè)試,這一塊的想法目前主要針對(duì)if來(lái)做,需要一定的時(shí)間。

其他

六、 寫(xiě)在最后

對(duì)于代碼自動(dòng)生成,還是有很多東西可以做的,但有些問(wèn)題還尚待解決,希望能盡最大努力解放我們的雙手,也能提高我們單元測(cè)試的質(zhì)量。

已在我們項(xiàng)目中使用此模式增加135個(gè)測(cè)試用例(除去mock的單模塊達(dá)到70%):速度比集成測(cè)試(pandora、spring等)提升一個(gè)等級(jí)。代碼的覆蓋率相對(duì)可觀。


當(dāng)前名稱:談一談單元測(cè)試
文章位置:http://m.5511xx.com/article/cccisee.html