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