清洁代码_清洁单元测试
清潔代碼
編寫使用JUnit和某些模擬庫的“單元測試”測試很容易。 即使測試甚至不是單元測試并提供可疑的價值,它們也可能產生使某些涉眾滿意的代碼覆蓋范圍。 編寫單元測試(在理論上是單元測試,但是比基礎代碼更復雜)因此也很容易編寫,因此只會增加整個軟件的熵。
這種特殊類型的軟件熵具有令人不愉快的特征,這使得底層軟件的重組或滿足新需求變得更加困難。 就像測試具有負值一樣。
正確地進行單元測試比人們想象的要難得多。 在本文中,我概述了一些旨在提高單元測試的可讀性,可維護性和質量的技巧。
注意:對于代碼段,使用Spock。 對于那些不了解Spock的人,可以認為它是圍繞JUnit的非常強大的DSL,它添加了一些不錯的功能并減少了冗長性。
失敗原因
僅當被測代碼有問題時,單元測試才應該失敗。 僅當DBService存在錯誤時,才對DBService類的單元測試失敗,而不是它依賴的其他任何類都存在錯誤時,則該測試將失敗。 因此,在DBService的單元測試中,唯一實例化的對象應該是DBService。 DBService依賴的所有其他對象都應該被存根或模擬。
否則,您將測試DBService以外的代碼。 盡管您可能錯誤地認為這更劃算,但這意味著定位問題的根本原因將需要更長的時間。 如果測試失敗,則可能是因為多個類存在問題,但您不知道哪個類。 而如果僅由于被測代碼錯誤而導致失敗,則您可以確切地知道問題出在哪里。 此外,以這種方式思考將改善代碼的面向對象性質。 測試僅測試班級的職責。 如果職責不明確,或者沒有另一個類就不能做任何事情,或者該類太瑣碎,測試毫無意義,它會提示這樣一個問題:就其職責的一般性而言,該類存在問題。 不模擬或存根依賴類的唯一例外是,如果您正在使用Java庫中的知名類(例如String)。 存根或嘲笑沒有什么意義。 或者,從屬類只是一個簡單的不可變POJO,在其中沒有存根或模擬它的價值。
存根和模擬
術語嘲笑和存根通常可以互換使用,就好像存在同一件事一樣。 它們不是同一件事。 總而言之,如果您的被測試代碼依賴于某個對象,而該對象從未在該對象上調用具有副作用的方法,則應將該對象存根。
而如果它依賴對象,并且確實為其調用了具有副作用的方法,則應該對其進行模擬。 為什么這很重要? 因為您的測試應根據與其依賴關系之間的關系類型來檢查不同的事物。 假設您要測試的對象是BusinessDelegate。 BusinessDelegate接收編輯BusinessEntities的請求。 它執行一些簡單的業務邏輯,然后在DBFacade(數據庫前面的Facade類)上調用方法。 因此,正在測試的代碼如下所示:
關于BusinessDelegate類,我們可以看到兩個關系。 與BusinessEntity的只讀關系。 BusinessDelegate在其上調用一些getters(),并且從不更改其狀態或調用任何具有副作用的方法。 與DBFacade的關系,它要求DBFacade做我們假設的事情會產生副作用。 確保更新發生不是BusinessDelegate的責任,這是DBFacade的工作。 BusinessDelegate的責任是確保僅使用正確的參數來調用更新方法。 很清楚,在對BusinessDelegate進行單元測試時,應將BusinessEntity存根,并應模擬DbFacade。 如果我們使用Spock測試框架,我們可以很清楚地看到這一點
class BusinessDelegateSpec { @Subject BusinessDelegate businessDelegate def dbFacade def setup() { dbFacade = Mock(DbFacade) businessDelegate = new BusinessDelegate(dbFacade); } def "edit(BusinessEntity businessEntity)" () { given: def businessEntity = Stub(BusinessEntity) // ... when: businessDelegate.edit(businessEntity) then : 1 * dbFacade.update(data) } }對存根模擬差異的深入了解可以大大提高OO質量。 與其僅考慮對象的作用,不如考慮它們之間的關系和依賴性。 現在,單元測試可以幫助實施可能會迷失的設計原理。
存根和模擬在正確的位置
你們中的好奇者可能想知道為什么在上面的代碼sampledbFacade中在類級別聲明了而businessEntity在方法級聲明了嗎? 好吧,答案是,單元測試代碼的可讀性越強,它越能反映被測代碼。 在實際的BusinessDelegate類中,對dbFacade的依賴關系在類級別,而對BusinessEntity的依賴關系在方法級別。
在現實世界中,當實例化BusinessDelegate時,將存在DbFacade依賴關系,每當實例化BusinessDelegate進行單元測試時,也可以存在DbFacade依賴關系。 聽起來合理嗎? 希望如此。 這樣做還有兩個優點:
- 減少代碼詳細程度。 即使使用Spock,單元測試也可能變得冗長。 如果將類級別的依賴關系移出了單元測試,則將減少測試代碼的冗長性。 如果您的班級在班級級別上依賴于其他四個班級,則每個測試中至少要包含四行代碼。
- 一致性。 開發人員傾向于以自己的方式編寫單元測試。 如果他們是唯一閱讀其代碼的人,那就很好; 但是這種情況很少。 因此,我們在所有測試中擁有的一致性越強,維護起來就越容易。 因此,如果您讀過從未讀過的測試,并且至少看到由于特定原因而在特定位置對變量進行了打樁和模擬,那么您會發現單元測試代碼更易于閱讀。
可變聲明順序
這是最后一點的后續內容。 在正確的位置聲明變量是一個很好的開始,下一步是按照它們在代碼中出現的順序進行操作。 所以,如果我們有類似下面的內容。
public class BusinessDelegate { private BusinessEntityValidator businessEntityValidator; private DbFacade dbFacade; private ExcepctionHandler exceptionHandler; @Inject BusinessDelegate(BusinessEntityValidator businessEntityValidator, DbFacade dbFacade, ExcepctionHandler exceptionHandler) { // ... // ... } BusinessEntity read(Request request, Key key) { public BusinessEntity read(Request request, Key key) { // ... } ???? }如果測試存根和模擬的定義順序與類聲明它們的順序相同,則讀取測試代碼要容易得多。
class BusinessDelegateSpec { @Subject BusinessDelegate businessDelegate // class level dependencies in the same order def businessEntityValidator def dbFacade def exceptionHandler def setup() { businessEntityValidator = Stub(BusinessEntityValidator) dbFacade = Mock(DbFacade) exceptionHandler = Mock(ExceptionHandler) businessDelegate = new BusinessDelegate(businessEntityValidator, dbFacade, exceptionHandler) } def "read(Request request, Key key)" () { given: def request = Stub(Request) def key = Stub(key) when: businessDelegate. read (request, key) then : // ... } }變量命名
而且,如果您認為最后一點是學究的,那么您會很高興知道這一點也是。 用于表示存根和模擬的變量名稱應與實際代碼中使用的名稱相同。 更好的是,如果您可以在測試代碼中將變量命名為與類型相同的名稱,并且不會失去任何業務意義,則可以這樣做。 在最后一個代碼示例中,參數變量被命名為requestInfo和key,并且它們對應的存根具有相同的名稱。 這比做這樣的事情容易得多:
//.. public void read(Request info, Key someKey) { // ... } // corresponding test code def "read(Request request, Key key)" () { given: def aRequest = Stub(Request) def myKey = Stub(key) // you ill get dizzy soon! // ...避免過度存根
過多的存根(或嘲笑)通常意味著出現了問題。 讓我們考慮一下得墨meter耳定律。 想象一下一些伸縮方法調用…
List queryBusinessEntities(Request request, Params params) { // check params are allowed Params paramsToUpdate = queryService.getParamResolver().getParamMapper().getParamComparator().compareParams(params) // ... // ... }僅僅存根queryService是不夠的。 現在,resolveAllowableParams()返回的所有內容都必須進行存根,并且該存根必須具有mapToBusinessParamsstubbed(),然后必須具有mapToComparableParams()。 即使使用Spock這樣的框架,它可以最大限度地減少冗長,但對于一行Java代碼,您將不得不進行四行存根。
def "queryBusinessEntities()" () { given: def params = Stub(Params) def paramResolver = Stub(ParamResolver) queryService.getParamResolver() = paramResolver def paramMapper = Stub(ParamMapper) paramResolver.getParamMapper() >> paramMapper def paramComparator = Stub (ParamComparator) paramMapper.getParamComparator() >> paramComparator Params paramsToUpdate = Stub(Params) paramComparator.comparaParams(params) >> paramsToUpdate when: // ... then : // ... }! 查看那一行Java對我們的單元測試的效果。 如果您不使用Spock之類的東西,情況將會更加糟糕。 解決方案是避免伸縮方法調用,并嘗試僅使用直接依賴項。 在這種情況下,只需將ParamComparator直接注入到我們的類中即可。 然后代碼變成…
List queryBusinessEntities(Request request, Params params) { // check params are allowed Params paramsToUpdate = paramComparator.compareParams(params) // ... // ... }測試代碼變成
setup() { // ... // ... paramComparator = Stub (ParamComparator) businessEntityDelegate = BusinessEntityDelegate(paramComparator) } def "queryBusinessEntities()" () { given: def params = Stub(Params) Params paramsToUpdate = Stub(Params) paramComparator.comparaParams(params) >> paramsToUpdate when: // .. then : // ... }所有突然的人都應該感謝您減少頭暈。
小Cucumber語法
不良的單元測試具有可怕的內容,例如遍歷頂部,底部和底部的斷言。 它會很快使人惡心,哪些是重要的,哪些是多余的。 哪些需要設置的位等等,等等。原理圖更容易理解。 那是Gherkin語法的真正優勢。 該場景是在給定的條件下設置的:總是,該場景何時出現,然后就是我們所期望的。 更好的用法是,像Spock這樣的東西意味著您擁有一個不錯的,整潔的DSL,以便在給定的時間,然后在一個測試方法中將它們放在一起。
窄時寬然后
如果單元測試正在測試四種方法,那是單元測試嗎? 考慮以下測試:
def "test several methods" { given: // ... when: def name = personService.getname(); def dateOfBirth = personService.getDateOfBirth(); def country = personService.getCountry(); then : name == "tony" dateOfBirth == "1970-04-04" country == "Ireland" } 首先,如果Jenkins告訴您這失敗了,那么您將必須扎根,找出班級的哪一部分是錯誤的。 由于測試不針對特定方法,因此您不會立即知道哪個方法失敗。 其次,假設是getName()失敗了,那么getDateOfBirth()和getCountry()的工作方式如何? 測試在第一次失敗時停止。 因此,當測試失敗時,您甚至都不知道是有一種方法無效還是三種方法無效。 您可以四處告訴所有人您有99%的代碼覆蓋率和一項測試失敗。 但是-一項測試完成了多少?
此外,更容易修復嗎? 小測試還是長測試? 理想情況下,測試應檢查與您正在測試的事物的單個交互。 現在,這并不意味著您只能擁有一項資產,但是您應該在此之后擁有一個狹窄的資產。 因此,讓我們先縮小一下范圍。 理想情況下,僅一行代碼。 一行代碼與您要進行單元測試的方法匹配。
現在,如果getName()失敗,但getCountry()和getDateOfBirth()通過,則我們可以擁有完全相同的代碼覆蓋率,但是getName()而不是getCountry()和getDateOfBirth()出現了問題。 獲得測試的粒度與代碼覆蓋率完全不同。 理想情況下,對于每種非私有方法,它應該至少是一個單元測試。 當您將否定測試等因素考慮在內時,效果會更好。在單元測試中具有多個斷言是完全可以的。 例如,假設我們有一個委托給其他類的方法。
考慮一個resynceCache()方法,該方法在其實現中會在cacheService對象上調用另外兩個方法:clear()和reload()。
在這種情況下,進行兩個單獨的測試是沒有意義的。 “時間”相同,并且如果任何一個失敗,您將立即知道必須查看哪種方法。 進行兩個單獨的測試意味著付出兩倍的努力,卻收效甚微。 要做的一個微妙的事情是確保您的資產順序正確。 它們應與代碼執行的順序相同。 因此,在reload()之前調用clear()。 如果在clear()處測試失敗,則由于方法被破壞,無論如何都沒有必要檢查reload()。 如果您不遵循斷言順序提示,而是先對reload()進行斷言并且被報告為失敗,那么您將不知道應該首先發生的clear()是否已經發生。 以這種方式思考將幫助您成為一名測試忍者!
嘲笑和存根的排序技巧也適用于斷言。 按時間順序斷言。 這很花哨,但是它將使測試代碼更易于維護。
參數化
參數化是一項非常強大的功能,可以大大降低測試代碼的詳細程度,并Swift增加代碼路徑中的分支覆蓋范圍。 單元測試忍者應該總是能夠發現何時使用它!
一個明顯的跡象表明,可以將多個測試分組到一個測試中并進行參數化,這表明它們在when塊中具有相同的參數,只是輸入參數不同。 例如,請考慮以下內容。
正如我們在這里看到的,除了輸入參數外,when相同。 這對于參數化毫無疑問。
@Unroll( "number1=#number1, number2=#number2" ) // unroll will provide the exact values in test report unroll will provide the exact values def "addNumbers()" (int number1, int number2) { given: // ... when: def answer = mathService.addNumbers(number1, number2); then : // ... where: number1 | number2 || answer 4 | 4 || 8 5 | 5 || 10 } 立即我們將代碼減少了50%。 通過將另外一行添加到where表中,我們還使添加其他排列變得更加容易。 因此,盡管看起來這兩個測試應該是一個參數化測試非常明顯,但是只有遵守時機狹窄的準則才是顯而易見的。 狹窄的“何時”編碼風格使要測試的確切場景更容易看到。 如果將廣泛的時間用于很多事情,那么事實并非如此,因此很難發現要參數化的測試。
通常,唯一不對具有相同語法的測試進行參數化的時間是:代碼塊是指期望是完全不同的結構。 期望一個int是相同的結構,在一個場景中期望一個異常而一個int是另一個場景則是兩個不同的結構。 在這種情況下,最好不要參數化。 一個經典的眾所周知的例子是將正面和負面的測試混在一起。 假設我們的addNumbers()方法在接收到浮動后會拋出異常,這是一個否定的測試,應該分開放置。 then:塊絕不能包含if語句。 這是測試變得越來越靈活的標志,而沒有if語句的單獨測試更有意義。
摘要
干凈的單元測試對于擁有可維護的代碼基礎,能夠定期且快速地發布并更享受您的軟件工程至關重要。
翻譯自: https://www.javacodegeeks.com/2020/03/clean-unit-testing.html
清潔代碼
總結
以上是生活随笔為你收集整理的清洁代码_清洁单元测试的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 王者荣耀怎么设置拒绝加好友 王者荣耀设置
- 下一篇: 浑身近义词 浑身近义词有哪些