第十七章、groovy單元測試

往前的娘娘發表於2020-12-17

  • Groovy作為單元測試優勢
    1. Groovy內嵌Junit,不需要引用Junit依賴
    2. Groovy內嵌 test-case(測試斷言場景),新增一個新的斷言方法
    3. Groovy內嵌mock,stub和其他動態類建立功能
    4. 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
    
    }
    
  • image-20201215000932615

  • 就算程式碼覆蓋率達到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、總結


相關文章