第十七章、groovy單元測試
- Groovy作為單元測試優勢
- Groovy內嵌Junit,不需要引用Junit依賴
- Groovy內嵌 test-case(測試斷言場景),新增一個新的斷言方法
- Groovy內嵌mock,stub和其他動態類建立功能
- Groovy很容易整合Gradle、Maven、IDE工具
1、開始入門
1.1、寫測試很簡單
-
/** * @author liangchen* @date 2020/12/13 */ // 攝氏度轉 華氏溫度 class Converter{ static celsius(fahrenheit) { (fahrenheit - 32) * 5 / 9 } } // 驗證一個方法是否正確 assert 20 == Converter.celsius(68) assert 35 == Converter.celsius(95) assert -17 == Converter.celsius(0).toInteger() assert 0 == Converter.celsius(32)
1.2、GroovyTestCase類介紹
- 本質上是繼承了Junit的類,使得測試更加容易使用
1.3、GroovyTestCase使用
-
直接繼承 GroovyTestCase
-
使用@Test註解
-
繼承 TestCase
-
import groovy.test.GroovyTestCase import junit.framework.TestCase import org.junit.Test /** * @author liangchen* @date 2020/12/13 */ class SimpleUnitTest extends GroovyTestCase { //直接繼承GroovyTestCase ,且方法名需要以test開頭 void testSimple(){ assertEquals("Groovy should add correctly", 2, 1+1) } } import static org.junit.Assert.assertEquals // 或者不繼承GroovyTestCase ,直接使用註解 class SimpleUnitAnnotationTest{ @Test void shouldAdd() { assertEquals("Groovy should add correctly", 2, 1 + 1) } } //或者直接繼承TestCase class AnotherSimpleUnitTest extends TestCase{ void testSimpleAgain(){ assertEquals("should substract correctly too", 2, 3-1) } }
2、Groovy單元測試程式碼
-
單元測試程式碼
- assertLength
- assertArrayEquals
- assertTrue
- assertEquals
-
package com.jack.groovy.ch17 import groovy.test.GroovyTestCase /** * @author liangchen* @date 2020/12/13 */ class _17_2CounterTest extends GroovyTestCase { static final Integer[] NEG_NUMBERS = [-2, -3, -4] static final Integer[] POS_NUMBERS = [4, 5, 6] static final Integer[] MIXED_NUMBERS = [4, -6, 0] private Counter counter // 初始化 void setUp(){ counter = new Counter() } void testCounterWorks() { // 大於7 的數字數量 assertEquals(2, counter.biggerThan([5, 10, 15], 7)) } void testCountHowManyFromSampleNumbers(){ check(0, NEG_NUMBERS, -1) check(0, NEG_NUMBERS, -2) check(2, NEG_NUMBERS, -4) check(3, NEG_NUMBERS, -5) // check(0, POS_NUMBERS, 7) check(0, POS_NUMBERS, 6) check(2, POS_NUMBERS, 4) check(3, MIXED_NUMBERS, -7) check(2, MIXED_NUMBERS, -1) } //測試輸入日期是否改變 void testInputDataUnchanged(){ def numbers = NEG_NUMBERS.clone() def origLength = numbers.size() counter.biggerThan(numbers, 0) assertLength(origLength, numbers) assertArrayEquals( NEG_NUMBERS, numbers) } void testCountHowManyFromSapleStrings() { check(2, ['Dog', 'Cat', 'Antelope'], 'Bird') } /** * 使用了 assertTrue 和 assertContains */ void testInputDataAssumptions(){ assertTrue( NEG_NUMBERS.every { it < 0 }) assertTrue(POS_NUMBERS.every { it > 0 }) assertContains 0, MIXED_NUMBERS int negCount =0 int posCount =0 MIXED_NUMBERS.each { if(it <0) negCount ++ else if (it> 0) posCount ++ } assert negCount && posCount } // 自定義檢查方法 private check(expectedCount, items, threshold) { assertEquals(expectedCount, counter.biggerThan(items, threshold)) } } class Counter{ int biggerThan(items, threshold) { items.grep { it > threshold }.size() } }
3、java單元測試程式碼
-
shouldFail 異常優雅判斷
-
package com.jack.groovy.ch17 import groovy.test.GroovyTestCase /** * @author liangchen* @date 2020/12/13 */ //測試Hashmap class HashMapTest extends GroovyTestCase { static final KEY = new Object() static final MAP = [key1: new Object(), key2: new Object()] // 丟擲空指標異常 void testHashtableRejectsNull(){ shouldFail (NullPointerException){ new Hashtable()[KEY] = null } } // 建立map傳入的長度不對 void testBadInitialSize(){ def msg = shouldFail(IllegalArgumentException) { new HashMap(-1) } assertEquals "Illegal initial capacity: -1", msg } // 判斷 void testHashMapAcceptsNull(){ def myMap = new HashMap() myMap.entrySet().each { myMap[it] = MAP[it] assertSame Map[it], myMap[it] } assert MAP.dump().contains("java.lang.Object") assert myMap.size() == MAP.size() } }
4、組織你單元測試
4.1、測試套件
-
GroovyTestSuite
-
import groovy.test.AllTestSuite import groovy.test.GroovyTestSuite /** * @author liangchen* @date 2020/12/13 */ /** * 將多個指令碼集合在一起跑 */ import junit.framework.* import junit.runner.TestRunListener import junit.textui.TestRunner static Test suite(){ def suite = new TestSuite() def gts = new GroovyTestSuite() suite.addTestSuite(gts.compile("src/com/jack/groovy/ch17/_17_2Counter.groovy")) suite.addTestSuite(gts.compile("src/com/jack/groovy/ch17/_17_3TestingHashMap.groovy")) return suite } // 跑多個指令碼 TestRunner.run(suite()) // 利用匹配表示式 def suiteAll = AllTestSuite.suite(".", "_17_2Counter*.groovy") TestRunner.run(suiteAll)
4.2、引數化,或資料驅動測試
-
引數化測試,不同輸入值,同一個過程
-
package com.jack.groovy.ch17 import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.junit.Test /** * @author liangchen* @date 2020/12/13 */ // 引數化驅動測試,使用JUnit 4 , 這個是一個寶藏, @RunWith(Parameterized) class DataDrivenJUnitTest{ private c, f, scenario // 列出一些引數入參建立物件,然後測試不同物件情況,可以不用寫死這些情況 @Parameterized.Parameters static scenarios(){ [ [0,32,'Freezing'], [20,68,'Garden party condition'], [35,95,'Beach conditions'], [100,212,'Boiling'] ]*.toArray() } DataDrivenJUnitTest(c, f, scenario) { this.c = c this.f = f this.scenario = scenario } @Test void convert(){ def actual = Converter.celsius(f) def msg = "$scenario:${f}F should convert into ${c}C" assert c == actual, msg } }
-
使用quick-check jar, 進行隨機測試
-
import net.java.quickcheck.generator.PrimitiveGenerators /** * @author liangchen* @date 2020/12/13 */ @Grab('net.java.quickcheck:quickcheck:0.6') // 利用隨機測試,確定某一個範圍是正確的 def gen = PrimitiveGenerators.integers(-40, 240) def liquidC = 0..100 def liquidF = 32..212 100.times { int f = gen.next() int c = Math.round(Converter.celsius(f)) assert c <= f assert c in liquidC == f in liquidF }
5、單元測試進階
5.1、使用模擬class
-
/** * @author liangchen* @date 2020/12/13 */ def relay(request, farm) { //排序取第一條 farm.machines.sort{ it.load }[0].send(request) } // 模擬虛擬機器器 class FakeMachine{ def load def send(request){return this} } final LOW_LOAD = 5, HIGH_LOAD=10 def farm = [machines:[ new FakeMachine(load: HIGH_LOAD), new FakeMachine(load: LOW_LOAD) ]] assertSame(LOW_LOAD, relay(null,farm).load)
5.2、Stubbing和mocking
-
mport groovy.mock.interceptor.MockFor import groovy.mock.interceptor.StubFor import groovy.transform.Sortable /** * @author liangchen* @date 2020/12/13 */ //利用stubing, 其實就一個空殼方法 class Farm{ def getMachines(){ // 這裡程式碼非常複雜 } } def relay(request) { //這裡呼叫getMachines方法時候自動會呼叫到,返回fakeOne, fakeTwo new Farm().getMachines().sort{it.load}[0].send(request) } def fakeOne = new Expando(load:10, send: { false }) def fakeTwo = new Expando(load:5, send: { true }) // 確定存根物件 def farmStub = new StubFor(Farm) // 需要實現存根物件方法,返回兩個fake物件 farmStub.demand.getMachines{[fakeOne, fakeTwo]} farmStub.use { assert relay(null) } // 使用mocking class SortableFarm extends Farm{ void sort(){ //排序machine } } def relayMocking(request) { def farm = new SortableFarm() farm.sort() farm.getMachines()[0].send(request) } def farmMock = new MockFor(SortableFarm) farmMock.demand.sort(){} farmMock.demand.getMachines { [new Expando(send: {})] } farmMock.use{ relayMocking(null) }
5.3、使用GroovyLogTestCase
-
記錄日誌,將日誌進行分析
-
package com.jack.groovy.ch17 import groovy.test.GroovyLogTestCase import java.util.logging.Level import java.util.logging.Logger /** * @author liangchen* @date 2020/12/13 */ //主要記錄測試的日誌 class LoggingCounter{ static final LOG = Logger.getLogger('LoggingCounter') def biggerThan(items, target) { def count = 0 items.each { if (it > target) { count++ LOG.finer("item was bigger - count this one") }else if (it == target) { LOG.finer "item was equal - don't count this one" } else { LOG.finer "item was smaller - don't count this one" } } return count } } class LoggingCounterTest extends GroovyLogTestCase { static final MIXED_NUMBERS = [99, 2, 1, 0, -1, -2, -99] private count void setUp(){ //初始化建立物件 count = new LoggingCounter() } void testCounterAndLog(){ // 將日誌轉換成字串,確定收集日誌的級別和類,我們是不是利用這個類收集一下mybatis列印的sql語句日誌?可以試一下 // 非侵入式測試 def log = stringLog(Level.FINER, 'LoggingCounter'){ def bigger = count.biggerThan(MIXED_NUMBERS, -1) assertEquals( 4, bigger) } checkLogCount(1, "was equal", log) checkLogCount(4, "was bigger", log) checkLogCount(2, "was smaller", log) checkLogCount(4,/[^d][^o][^n][^'][^t] count this one/, log) checkLogCount(3, "don't count this one", log) } private checkLogCount(expectedCount, regex, log) { def matcher = (log =~ regex) assertTrue log, expectedCount == matcher.count } }
5.4、單元測試效能
-
引用junitperf 的依賴
-
import com.clarkware.junitperf.ConstantTimer import com.clarkware.junitperf.LoadTest import com.clarkware.junitperf.TimedTest import junit.framework.TestCase import junit.framework.Test import junit.textui.TestRunner; /** * @author liangchen* @date 2020/12/13 */ // 測試效能 @Grab('junitperf:junitperf:1.9.1') @GrabResolver('https://repository.jboss.org/') class JUnitPerf extends TestCase{ JUnitPerf(String testName) { super(testName) } void testConverter() { assert 0 == Converter.celsius(32) assert 100 == Converter.celsius(212) } static main(args) { TestRunner.run(suite()) } static Test suite() { def testCase = new JUnitPerf('testConverter') def numUsers =20 def stagger = new ConstantTimer(100) def loadTest = new LoadTest(testCase, numUsers, stagger) def timeLimit = 2100 return new TimedTest(loadTest, timeLimit) } }
5.5、groovy 程式碼覆蓋
-
用的gradle, 沒有用過cobertura測試jar
-
// 程式碼覆蓋測試 class BiggestPairCalc{ int sumBiggestPair(a, b, c) { def op1 = a def op2 = b if (c > a) { op1 = c }else if (c > b) { op2 = c } return op1 + op2 } } class BiggestPairCalcTest extends GroovyTestCase{ void testSumBiggestPair(){ def calc = new BiggestPairCalc() assertEquals(9, calc.sumBiggestPair(5, 4,1)) assertEquals(15, calc.sumBiggestPair( 5, 9, 6)) assertEquals(16, calc.sumBiggestPair(10, 2, 6)) // assertEquals(11, calc.sumBiggestPair(5, 2, 6)) } } // 修復bug int sumBiggestPair(int a, int b, int c) { int op1 = a int op2 = b if (c > [a,b].min()) { op1 = c op2 = [a,b].max() } return op1 + op2 }
-
就算程式碼覆蓋率達到100% ,依然會有bug,本身邏輯,通過隨機測試,或者
6、IDE 整合
6.1、使用 GroovyTestSuite
6.2、使用AllTestSuite
7、使用Spock框架做單元測試
- Behavior-Driven Development (BDD) 行為驅動開發
- Given-When-Then 格式
7.1、使用mocks
-
mock方法
-
package com.jack.groovy.ch17 import groovy.transform.TupleConstructor import spock.lang.Specification /** * @author liangchen* @date 2020/12/15 */ // 使用spock框架 // 只能手動下載下來放到grape中了,哎,還要注意spock-core的版本, 原書的版本太老的了 @Grab('org.spockframework:spock-core:2.0-M4-groovy-3.0') class GivenWhenThenSpec extends Specification { def "test adding a new item to a set"(){ given: def items = [4, 6, 3, 2] as Set when: items << 1 then: items.size() == 5 } } interface MovieTheater{ void purchaseTicket(name, number) boolean hasSeatsAvailable(name, number) } @TupleConstructor class Purchase{ def name, number, completed = false def fill(theater) { if (theater.hasSeatsAvailable(name, number)) { theater.purchaseTicket(name, number) completed = true } } } // 需要引入spock框架 //想要測這段邏輯但是沒有具體實現這個時候可以使用mock class MovieSpec extends Specification { def "buy ticket for a movie theater"() { given: def purchase = new Purchase("Lord of the Rings", 2) // mock物件,mock方法 MovieTheater theater = Mock() theater.hasSeatsAvailable("Lord of the Rings", 2) >> true when : // 執行測試方法 purchase.fill(theater) then : // 判斷結果 purchase.completed // 斷言方法接下來方法入參(1表示呼叫次數為1次) 1 * theater.purchaseTicket("Lord of the Rings", 2) } // 用萬用字元 def "cannot buy a ticket when the movie is sold out"(){ given: def purchase = new Purchase("Lord of the rings", 2) MovieTheater theater = Mock() when: theater.hasSeatsAvailable(_,_) >> false purchase.fill(theater) then: !purchase.completed //表示方法被呼叫0次 _ 表示萬用字元(不管引數) 0 * theater.purchaseTicket(_, _) } // 可以進行入參的校驗動作,如下 偶數票數被銷售掉了 def "on couples night tickets are sold in pairs"(){ given: def purchase = new Purchase("Lord of the Rings", 2) MovieTheater theater = Mock() //呼叫這個方法返回true theater.hasSeatsAvailable("Lord of the Rings", 2) >> true when: purchase.fill(theater) then: 1*theater.purchaseTicket(_, { it % 2 == 0 }) } }
7.2 spock的資料驅動測試
-
集中測試場景
-
package com.jack.groovy.ch17 import groovy.transform.TupleConstructor import spock.lang.Specification import spock.lang.Unroll /** * @author liangchen* @date 2020/12/15 */ // 使用spock框架 // 只能手動下載下來放到grape中了,哎,還要注意spock-core的版本, 原書的版本太老的了 @Grab('org.spockframework:spock-core:2.0-M4-groovy-3.0') class SpockDataDriven extends Specification { def "test temperature scenario"() { //期望值 expect: Converter.celsius(tempF) == tempC // 列出一些場景, || 分割輸入引數和輸出引數 where: scenario | tempF || tempC 'Freezing' | 32 || 0 'Garden party condition' | 68 || 20 'Beach condition' | 95 || 315 'Boiling' | 212 || 100 } @Unroll def "test unroll temperature scenario"() { //期望值 expect: Converter.celsius(tempF) == tempC // 列出一些場景, || 分割輸入引數和輸出引數 where: scenario | tempF || tempC 'Freezing' | 32 || 0 'Garden party condition' | 68 || 20 'Beach condition' | 95 || 35 'Boiling' | 212 || 100 } /** * 進一步優雅 * @return */ @Unroll def "Scenario #scenario: #tempFºF should convert to #tempCºC"() { //期望值 expect: Converter.celsius(tempF) == tempC // 列出一些場景, || 分割輸入引數和輸出引數 where: scenario | tempF || tempC 'Freezing' | 32 || 0 'Garden party condition' | 68 || 20 'Beach condition' | 95 || 315 'Boiling' | 212 || 100 } }
8、建立自動測試
9、總結
相關文章
- 第 15 篇:介面的單元測試
- 測試 之Java單元測試、Android單元測試JavaAndroid
- 單元測試:單元測試中的mockMock
- 單元測試,只是測試嗎?
- 單元測試-【轉】論單元測試的重要性
- SpringBoot單元測試Spring Boot
- python 單元測試Python
- iOS 單元測試iOS
- Flutter 單元測試Flutter
- 單元測試 Convey
- 單元測試真
- golang單元測試Golang
- 單元測試工具
- 前端單元測試前端
- 十五、單元測試
- Go單元測試Go
- 聊聊單元測試
- 前端測試:Part II (單元測試)前端
- JavaScript單元測試框架JavaScript框架
- 單元測試 -- mocha + chaiAI
- React元件單元測試React元件
- Spring Boot 單元測試Spring Boot
- Vue單元測試探索Vue
- Google 單元測試框架Go框架
- 單元測試與MockitoMockito
- Junit單元測試—MavenMaven
- 單元測試框架 mockito框架Mockito
- 單元測試?即刻搞定!
- Source Generator 單元測試
- 聊聊前端單元測試前端
- 單元測試基礎
- Go 單元測試之mock介面測試GoMock
- 測試氣味-整潔單元測試
- 單元測試 - 測試場景記錄
- 單元測試-一份如何寫好單元測試的參考
- 測試開發之單元測試-禪道結合ZTF驅動單元測試執行
- 測試夜點心:單元測試測什麼
- Vue 應用單元測試的策略與實踐 04 - Vuex 單元測試Vue