第十七章、groovy单元测试

  • 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、总结


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值