吃透单元测试:Spock单元测试框架的应用与实践
一,单元测试
单元测试是对软件基本组成单元进行的测试,如函数或一个类的方法。程序是由函数组成的,每个函数都要健壮,这样才能保证程序的整体质量。单元测试是对软件未来的一项必不可少的投资。”具体来说,单元测试有哪些收益呢?
- 它是最容易保证代码覆盖率达到100%的测试。
- 可以⼤幅降低上线时的紧张指数。
- 单元测试能更快地发现问题。
- 单元测试的性价比最高,因为错误发现的越晚,修复它的成本就越高,而且难度呈指数式增长,所以我们要尽早地进行测试
- 编码人员,一般也是单元测试的主要执行者,是唯一能够做到生产出无缺陷程序的人,其他任何人都无法做到这一点。
- 有助于源码的优化,使之更加规范,快速反馈,可以放心进行重构。
尽管单元测试有如此的收益,但在我们日常的工作中,仍然存在不少项目它们的单元测试要么是不完整要么是缺失的。
1,为什么人人都讨厌写单测
要额外写很多很多的代码
一个高覆盖率的单测代码,往往比你要测试的,真正开发的业务代码要多,甚至是业务代码的好几倍。这让人觉得难以接受,你想想开发 5 分钟,单测 2 小时是什么样的心情。而且并不是单测写完就没事了,后面业务要是变更了,你所写的单测代码也要同步维护。
时间成本
代码逻辑过于复杂,写单元测试时耗费的时间较长,任务重、工期紧,写一个单测的时间可以实现一个需求,那么你如何去选?显而易见。
写单测是一件很无趣的事情
因为他比较死,主要目的就是为了验证,相比之下他更像是个体力活,没有真正写业务代码那种创造的成就感。写出来,验证不出bug很失落,白写了,验证出bug又感到自己是在打自己脸。
2,为什么又必须写单测
所以得到的结论就是不写单测?那么问题又来了,出来混迟早是要还的,上线出了问题,最终责任人是谁?不是提需求的产品、不是没发现问题的测试同学,他们顶多就是连带责任。最该负责的肯定是写这段代码的你。
所以单元测试保护的不仅仅是程序,更保护的是写程序的你。 最后得出了一个无可奈何的结论,单测是个让人又爱又恨的东西,是不想做但又不得不做的事情。
虽然我们没办法改变要写单测这件事,但是我们可以改变怎么去写单元测试这件事。
既然我们不得不去写单元测试,那么今天就为大家推荐一款比较神奇的单元测试框架,Spock去提高你编写单测的效率。
二,Spock是什么
spock官网:https://spockframework.org/spock/docs/2.0/index.html
Spock是国外一款优秀的测试框架,基于BDD(行为驱动开发)思想实现,功能非常强大。Spock结合Groovy动态语言的特点,提供了各种标签,并采用简单、通用、结构化的描述语言,让编写测试代码更加简洁、高效。
那么spock 是如何提高编写单测的效率呢?
- 它可以用更少的代码去实现单元测试,让你可以更加专注于去验证结果而不是写单测代码的过程。(Spock使用groovy动态语言的特点)
- **他有更好的语义化,让你的单测代码可读性更高。**Spock提供多种语义标签,如: given、when、then、expect、where、with、and 等,从行为上规范单测代码,每一种标签对应一种语义,让我们的单测代码结构具有层次感,功能模块划分清晰,便于后期维护。
三,Spock使用
Spock引入
<!--引入 groovy 依赖--><dependency><groupId>org.codehaus.groovy</groupId><artifactId>groovy-all</artifactId><version>2.4.15</version><scope>test</scope></dependency><!--引入spock 与 spring 集成包--><dependency><groupId>org.spockframework</groupId><artifactId>spock-spring</artifactId><version>1.2-groovy-2.4</version><scope>test</scope></dependency>
Spock自带Mock功能,所以我们可以来Mock非静态方法。但是遇到静态方法时,我们需要导入powermock
<!--powermock --><dependency><groupId>org.powermock</groupId><artifactId>powermock-api-mockito2</artifactId><version>2.0.0</version><scope>test</scope></dependency><dependency><groupId>org.powermock</groupId><artifactId>powermock-module-junit4</artifactId><version>2.0.0</version><scope>test</scope></dependency>
但是当我们需要测试dao层的sql语句时,我们可以结合H2内存数据库使用,此时需要引入:
<!--db unit--><dependency><groupId>com.github.janbols</groupId><artifactId>spock-dbunit</artifactId><version>0.4</version><scope>test</scope></dependency><dependency><groupId>org.dbunit</groupId><artifactId>dbunit</artifactId><version>2.5.1</version><scope>test</scope></dependency><dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId><version>1.4.200</version><scope>test</scope></dependency>
以上就能满足我们平时用Spock来写单测的日常功能。
下面通过一个简单的例子,来体验一下Spock和junit的不同:
public class IDNumberUtils {/*** 根据身份证号码获取出生日期和年龄** @param idNo 18位身份证号码* @return 返回格式: birth: 1992-01-01 age: 29*/public static Map<String, String> getBirthAge(String idNo) {String birthday = "";String age = "";int year = Calendar.getInstance().get(Calendar.YEAR);birthday = idNo.substring(6, 10) + "-" + idNo.substring(10, 12) + "-" + idNo.substring(12, 14);age = String.valueOf(year - Integer.valueOf(idNo.substring(6, 10)));Map<String, String> result = new HashMap<>();result.put("birthday", birthday);result.put("age",age);return result;}
}
Junit单测:
public class IDNumberUtilsJunitTest {@ParameterizedTest@MethodSource("getBirthAgeParams")public void testGetBirthAge(String idNo,Predicate<Map<String,String>> predicate){Map<String, String> birthAgeMap = IDNumberUtils.getBirthAge(idNo);Assertions.assertTrue(predicate.test(birthAgeMap));}public static Object[] getBirthAgeParams(){return new Object[]{new Object[]{"410225199208091234",(Predicate<Map<String,String>>) map -> "{birthday=1992-08-09, age=29}".equals(map.toString())},new Object[]{"410225199308091234",(Predicate<Map<String,String>>) map -> "{birthday=1993-08-09, age=28}".equals(map.toString())},new Object[]{"410225199408091234",(Predicate<Map<String,String>>) map -> "{birthday=1994-08-09, age=27}".equals(map.toString())},new Object[]{"410225199508091234",(Predicate<Map<String,String>>) map -> "{birthday=1995-08-09, age=26}".equals(map.toString())},new Object[]{"410225199608091234",(Predicate<Map<String,String>>) map -> "{birthday=1996-08-09, age=25}".equals(map.toString())},};}
}
Spock单测:
class IDNumberUtilsGroovyTest extends Specification {@Unrolldef "身份证号:#idNO 的生日,年龄是:#result"() {expect: "执行以及结果验证"IDNumberUtils.getBirthAge(idNO) == resultwhere: "测试用例覆盖"idNO || result"410225199208091234" || ["birthday": "1992-08-09", "age": "29"]"410225199308091234" || ["birthday": "1993-08-09", "age": "28"]"410225199408091234" || ["birthday": "1994-08-09", "age": "27"]"410225199508091234" || ["birthday": "1995-08-09", "age": "26"]"410225199608091234" || ["birthday": "1996-08-09", "age": "25"]}
}
- def 是 groovy 的关键字,可以用来定义变量跟方法名。
- 后面的是你的单元测试名称,可以用中文也可以用英文。
- expect … where …语句块,expect 为核心的测试校验语句块。where 为多个测试用例的列举。
以上测试方法的语义为:列举多个where下面的多个测试用例,以idNo传参,result为结果,调用getBirthAge方法,来验证每条测试用例是否符合我们的预期,添加@Unroll注解,主要是让每一条测试用例都返回测试结果。
如果不加@Unroll注解,那么会把所有的测试用例返回一个结果。
四,Mock
我们的服务大部分是分布式微服务架构。服务与服务之间通常都是通过接口的方式进行交互。即使在同一个服务内也会分为多个模块,业务功能需要依赖下游接口的返回数据,才能继续后面的处理流程。这里的下游不限于接口,还包括中间件数据存储等等,所以如果想要测试自己的代码逻辑,就必须把这些依赖项Mock掉。因为如果下游接口不稳定可能会影响我们代码的测试结果,让下游接口返回指定的结果集(事先准备好的数据),这样才能验证我们的代码是否正确,是否符合逻辑结果的预期。
1,非静态方法Mock
public class UserService {@Autowiredprivate UserDao userDao;public UserInfo getUserInfoById(long id) {List<UserInfo> userInfoList = userDao.getAllUserInfo();for (UserInfo userInfo : userInfoList) {if (userInfo.getId()==id){return userInfo;}}return null;}
}public class UserDao {public List<UserInfo> getAllUserInfo(){return null;}
}
Junit单测:
@ExtendWith({MockitoExtension.class})
public class UserInfoJunitTest {@Mockprivate UserDao userDao;@InjectMocksprivate UserService userService;@Beforepublic void before(){MockitoAnnotations.openMocks(this);}@Testpublic void userinfoTest(){// 准备参数List<UserInfo> userInfoList = new ArrayList<>();UserInfo userInfo1 = new UserInfo();userInfo1.setId(0);userInfo1.setName("小明");UserInfo userInfo2 = new UserInfo();userInfo1.setId(1);userInfo1.setName("小强");userInfoList.add(userInfo1);userInfoList.add(userInfo2);// mock数据Mockito.when(userDao.getAllUserInfo()).thenReturn(userInfoList);UserInfo userInfo = userService.getUserInfoById(1);// 验证结果Assertions.assertEquals(userInfoList.get(1),userInfo);}
}
Spock单测
class UserInfoSpec extends Specification {def userService = new UserService();def "getUserInfoById"() {given: "准备参数"def user1 = new UserInfo(id: 0, name: "小明")def user2 = new UserInfo(id: 1, name: "小强")and: "mock数据"def userDao = Mock(UserDao)Whitebox.setInternalState(userService, "userDao", userDao)userDao.getAllUserInfo() >> [user1, user2]when: "方法调用"def response = userService.getUserInfoById(1)then: "结果验证"with(response) {id == 1name == "小强"}}
}
**given … when … then …**语句块:given为条件,when为执行方法,then为结果验证。
when … then 通常是成对出现的,;‘它代表着当执行了 when 块中的操作,会出现 then 块中的期望。
比较明显,上边的JUnit单元测试代码冗余,缺少结构层次,可读性差,随着后续的迭代,势必会导致代码的堆积,维护成本会变得越来越高。下面的单元测试代码Spock会强制要求使用given
、when
、then
这样的语义标签(至少一个),否则编译不通过,这样就能保证代码更加规范,结构模块化,边界范围清晰,可读性强,便于扩展和维护。而且使用了自然语言描述测试步骤,让非技术人员也能看懂测试代码(given
表示输入条件,when
触发动作,then
验证输出结果)。
Spock自带的Mock语法也非常简单:
def userDao = Mock(UserDao)
使用Spock自导的Mock方法构造一个UserDao的Mock对象。
Whitebox.setInternalState(userService, "userDao", userDao)
给userService对象中的userDao属性赋值刚才mock出来的userDao。
userDao.getAllUserInfo() >> [user1, user2]
。
两个右箭头>>
表示模拟userDao.getAllUserInfo()
接口的返回结果,再加上使用的Groovy语言,可以直接使用[]
中括号表示返回的是List
类型。
如果要指定返回多个值的话,可以使用3个右箭头>>>
,比如:studentDao.getAllUserInfo() >>> [[user1,user2],[user3,user4],[user5,user6]]
。
每次调用userDao.getAllUserInfo()
方法返回不同的值。
2,静态方法Mock
在Spock中可以通过powermock去模拟静态方法、final方法、私有方法等。
但是junit5不支持powermock,只能使用junit4来写
Junit单测
@RunWith(PowerMockRunner.class)
@PrepareForTest(IDNumberUtils.class)
public class IDNumberUtilsStaticTest {@InjectMocksprivate UserService userService;@Beforepublic void setup(){PowerMockito.mockStatic(IDNumberUtils.class);}@Testpublic void testStatic(){// 准备参数Map<String,String> map = new HashMap<>();map.put("birthday","1992-08-09");map.put("age","29");// mockPowerMockito.when(IDNumberUtils.getBirthAge(Mockito.anyString())).thenReturn(map);int age = userService.getUserAgeByCardId("123");// 验证结果Assertions.assertEquals(29,age);}
}
Spock单测:
powermock的PowerMockRunner继承自Junit,所以使用powermock的@PowerMockRunnerDelegate()注解可以指定Spock的父类Sputnik去代理运行power mock,这样就可以在Spock里使用powermock去模拟静态方法、final方法、私有方法等
@PrepareForTest(IDNumberUtils.class)
@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
class IDNumberUtilsStaticSpec extends Specification {def userService = new UserService();void setup() {PowerMockito.mockStatic(IDNumberUtils.class)}def "getAge"() {given: "准备参数"def map = [birthday: "1992-08-09", age: "29"]and: "mock"PowerMockito.when(IDNumberUtils.getBirthAge(Mockito.anyString())).thenReturn(map)when: "方法调用"def respsnse = userService.getUserAgeByCardId("aa")then: "结果验证"with(respsnse){respsnse == 29}}
}
当使用powermock来mock静态方法的时候,必须加注解@PrepareForTest
和@RunWith
。注解@PrepareForTest
里写的类是静态方法所在的类。
when
模块里是真正调用要测试方法的入口userService.getUserAgeByCardId()
。
then
模块作用是验证被测方法的结果是否正确,符合预期值,所以这个模块里的语句必须是boolean
表达式,类似于JUnit的assert
断言机制,但不必显示地写assert
,这也是一种约定优于配置的思想。
then
块中使用了Spock的with
功能,可以验证返回结果response
对象内部的多个属性是否符合预期值,这个相对于JUnit的assertNotNull
或assertEquals
的方式更简单一些。
五,if…else多分支场景
在我们写业务代码时,经常会写到比较复杂的业务,对应的代码实现会走不通的逻辑,这样的 if else 嵌套代码因为业务的原因很难避免,如果要测试这样的代码,保证覆盖到每一个分支逻辑的话,使用传统的Junit单元测试代码写起来会很痛苦和繁琐。
业务代码:
public class TypeCodeUtil {public static String getTypeNameByCode(int code){String result = "";switch (code){case 1:result = "name1";break;case 2:result = "name2";break;case 3:result = "name3";break;case 4:result = "name4";break;case 5:result = "name5";break;default:break;}return result;}
}
junit单测:
public class TypeCodeUtilTest {@ParameterizedTest@MethodSource("getTypeCodeParams")public void testTypeCode(int code, Predicate<String> predicate){String typeNameByCode = TypeCodeUtil.getTypeNameByCode(code);Assertions.assertTrue(predicate.test(typeNameByCode));}public static Object[] getTypeCodeParams(){return new Object[]{new Object[]{1,(Predicate<String>) value -> "name1".equals(value)},new Object[]{2,(Predicate<String>) value -> "name2".equals(value)},new Object[]{3,(Predicate<String>) value -> "name3".equals(value)},new Object[]{4,(Predicate<String>) value -> "name4".equals(value)},new Object[]{5,(Predicate<String>) value -> "name5".equals(value)},new Object[]{6,(Predicate<String>) value -> "".equals(value)},};}
}
spock单测:
class TypeCodeUtilSpec extends Specification {@Unrolldef "code:#code,name:#name"() {expect:TypeCodeUtil.getTypeNameByCode(code) == namewhere:code || name1 || "name1"2 || "name2"3 || "name3"4 || "name4"5 || "name5"6 || ""}
}
where
模块第一行代码是表格的列名,多个列使用|
单竖线隔开,||
双竖线区分输入和输出变量,即左边是输入值,右边是输出值。格式如下:
输入参数1 | 输入参数2 || 输出结果1 | 输出结果2
表格的每一行代表一个测试用例,即被测方法执行了6次,每次的输入和输出都不一样,刚好可以覆盖全部分支情况。
解析点:
- @Unroll注解:表示展开where标签下面的每一行测试用例,都作为一个单独的测试用例来运行。
- 方法名:#code和#name,把请求参数值和返回结果值的字符串动态替换掉,#号后面的变量是在方法内部定义的,实现占位符的功能。
- expect标签:when + then标签的组合,即 “什么时候做什么 + 验证什么结果” 组合起来。
六,异常测试
业务代码,这个大家应该都很熟悉,针对这种抛出多个不同错误码和错误信息的异常。如果使用JUnit的方式测试,会比较麻烦。如果是单个异常还好,如果是多个的话,测试代码就不太好写。
@Data
@AllArgsConstructor
public class APIException extends RuntimeException{private String errorCode;private String errorMessage;
}@Service
public class ValidateService {public void validateUser(UserInfo userInfo){if (userInfo == null){throw new APIException("1001","userInfo is null");}else if (userInfo.getId()==0){throw new APIException("1002","id is not legal");}else if (userInfo.getName() == null || "".equals(userInfo.getName())){throw new APIException("1003","name is not legal");}}
}
Junit单测:在junit3中可以通过一次次的方法调用并捕获异常来进行测试。这种方式的缺陷是显而易见的。如果有100个异常测试,那么这些调用要写100遍简直要疯掉。
而在junit4中,可以使用ExpectedException
和@Test
这两种方式,但是都有缺陷,@Test
方式不能指定断言的异常属性,比如code
、message
。ExpectedException
的方式也只提供了expectMessage
的API,对自定义的code
不支持,尤其像上面的有很多分支抛出多种不同异常码的情况。
在junit4中,可以使用Assertions.assertThrows
方法来验证异常,但是依然有上面方式的缺陷。
public class ExceptionTest {private ValidateService validateService = new ValidateService();/*** junit3*/@Testpublic void testException1(){try {validateService.validateUser(null);}catch (APIException e){Assert.assertEquals(e.getErrorCode(),"1001");Assert.assertEquals(e.getErrorMessage(),"userInfo is null");}try {validateService.validateUser(new UserInfo(0,""));}catch (APIException e){Assert.assertEquals(e.getErrorCode(),"1002");Assert.assertEquals(e.getErrorMessage(),"id is not legal");}try {validateService.validateUser(new UserInfo(1,""));}catch (APIException e){Assert.assertEquals(e.getErrorCode(),"1003");Assert.assertEquals(e.getErrorMessage(),"name is not legal");}}/*** junit 4*/@Rulepublic ExpectedException expectedException = ExpectedException.none();@Testpublic void testException2(){expectedException.expect(APIException.class);expectedException.expectMessage("userInfo is null");validateService.validateUser(null);}@Test(expected = APIException.class)public void testException3(){validateService.validateUser(null);}/*** junit5*/@Testpublic void testException4(){Throwable throwable = Assertions.assertThrows(APIException.class,()->{validateService.validateUser(null);});Assertions.assertEquals("userInfo is null",throwable.getMessage());}
}
Spock单测:
class ExceptionSpec extends Specification {def validateService = new ValidateService()@Unrolldef "验证UserInfo"() {when: "调用校验方法"validateService.validateUser(user)then: "捕获异常并设置需要验证的异常值"def exception = thrown(expectedException)exception.errorCode == expectedErrCodeexception.errorMessage == expectedMessagewhere: "验证用户的合法性"user || expectedException | expectedErrCode | expectedMessagenull || APIException | "1001" | "userInfo is null"new UserInfo(0, "") || APIException | "1002" | "id is not legal"new UserInfo(1, "") || APIException | "1003" | "name is not legal"}
}
Spock内置thrown()方法,可以捕获调用业务代码抛出的预期异常并验证,再结合where表格的功能,可以很方便的覆盖多种自定义业务异常。
在then
标签里用到了Spock的thrown()
方法,这个方法可以捕获我们要测试的业务代码里抛出的异常。thrown()
方法的入参expectedException
,是我们自己定义的异常变量,这个变量放在where
标签里就可以实现验证多种异常情况的功能。expectedException
类型调用validateUser
方法里定义的APIException
异常,可以验证它所有的属性,errorCode
、errorMessage
是否符合预期值。
七,void方法测试
根据UserInfo中的IdNo属性,调用年龄计算工具,计算出年龄后赋值给UserInfo的age属性。
public class UserService {@Autowiredprivate IDNumberUtil idNumberUtil;/*** 根据idNo计算年龄然后赋值给age属性* @param list*/public void setAgeTest(List<UserInfo> list) {if (CollectionUtils.isEmpty(list)) {return;}for (UserInfo userInfo : list) {int age = idNumberUtil.getAge(userInfo.getCardId());userInfo.setAge(age);}}
}
Spock单测:
class VoidMethodSpec extends Specification {def userService = new UserService()def idNumberUtil = Mock(IDNumberUtil)def "测试void方法"() {given: "准备参数"def user1 = new UserInfo(id: 1, cardId: "410225199208091234")def user2 = new UserInfo(id: 2, cardId: "410225199208091234")def list = [user1, user2]and: "属性设置"Whitebox.setInternalState(userService, "idNumberUtil", idNumberUtil)when: "调用方法"userService.setAgeTest(list)then: "验证调用获取年龄是否符合预期: 一共调用2次, 第一次输出29, 第二次是28"2 * idNumberUtil.getAge(_) >> 29 >> 28and: "验证年龄计算后的结果是否符合预期"with(list) {list[0].age == 29list[1].age == 28}}
}
八,dao层测试
DAO层的测试有些不太一样,不能再使用Mock,否则无法验证SQL是否正确。对于DAO测试有一般最简的方式是直接使用@SpringBootTest
注解启动测试环境,通过Spring创建Mybatis、Mapper实例,但这种方式并不属于单元测试,而是集成测试范畴了,因为当启用@SpringBootTest
时,会把整个应用的上下文加载进来。不仅耗时时间长,而且一旦依赖环境上有任何问题,可能会影响启动,进而影响DAO层的测试。最后,需要到数据库尽可能隔离,因为如果大家都使用同一个Test环境的数据的话,一旦测试用例编写有问题,就可能会污染Test环境的数据。
针对以上场景,可采用以下方案:
- 通过MyBatis的SqlSession启动mapper实例(避免通过Spring启动加载上下文信息)。
- 通过内存数据库(如H2)隔离大家的数据库连接(完全隔离不会存在互相干扰的现象)。
- 通过DBUnit工具,用作对于数据库层的操作访问工具。
- 通过扩展Spock的注解,提供对于数据库创建和数据Data加载的方式。
1,通过MapperUtil工具类启动MyBatis的SqlSession获取Mapper实例
public class MapperUtil {private static final String MAPPER_CONFIG_LOCATIONS = "cn/huolala/link/svc/support/mapper-config.xml";/*** 通过MyBatis的SqlSession启动mapper实例* @param type* @param dataSource* @param <T>* @return* @throws IOException*/public static <T> T getMapper(Class<T> type, DataSource dataSource) throws IOException {// 获取资源InputStream inputStream = Resources.getResourceAsStream(MAPPER_CONFIG_LOCATIONS);JdbcTransactionFactory jdbcTransactionFactory = new JdbcTransactionFactory();Environment environment = new Environment("development", jdbcTransactionFactory, dataSource);// 启动SqlSessionFactory获取mapperSqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);sqlSessionFactory.getConfiguration().setEnvironment(environment);SqlSession sqlSession = sqlSessionFactory.openSession();return sqlSession.getMapper(type);}
}
2,通过内存数据库(H2)隔离大家的数据库连接
class SpecUtils {/*** 初始化H2数据库* @return*/static DataSource inMemoryDataSource() {return new HikariDataSource().with { dataSource ->dataSource.driverClassName = 'org.h2.Driver'dataSource.jdbcUrl = 'jdbc:h2:file:/Users/zhaosongbo/spock'dataSource.username = 'root'dataSource.password = 'test'}}/*** 创建相应的表结构* @param dataSource*/static void createAppMainTable(javax.sql.DataSource dataSource){assert dataSourcenew Sql(dataSource).execute("DROP TABLE IF EXISTS app_main; CREATE TABLE `app_main` (\n" +" `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自动增长主键',\n" +" `app_id` varchar(32) NOT NULL COMMENT '分配给接入方的唯一标识',\n" +" `app_key` varchar(32) DEFAULT NULL COMMENT 'appkey',\n" +" `app_secret` varchar(64) NOT NULL COMMENT '分配给接入方的密钥'\n" +") ;")}}
3,Spock来编写相应的测试代码
class AppMainDB2Spec extends Specification {def dataSource/*** 相当于junit的@efore* @return*/def setup() {dataSource = inMemoryDataSource3()createAppMainTable(dataSource)}/*** 构造数据* @return*/@DbUnit(content = {app_main(id: 1001,app_id: "appId1",app_key: "appKey1",app_secret: "appSecret1")app_main(id: 1002,app_id: "appId2",app_key: "appKey2",app_secret: "appSecret2")app_main(id: 1003,app_id: "appId3",app_key: "appKey3",app_secret: "appSecret3")}, configure = { IDatabaseTester it ->it.setUpOperation = DatabaseOperation.CLEAN_INSERTit.tearDownOperation = DatabaseOperation.TRUNCATE_TABLE}, datasourceProvider = { dataSource })def "dao层测试"() {given: "获取mapper"def appMainMapper = MapperUtil.getMapper(AppMainMapper.class,dataSource)when: "执行"def result = appMainMapper.appSecretByAppId("appId1")then: "验证数据"result == 'appSecret1'}
}
吃透单元测试:Spock单元测试框架的应用与实践相关推荐
- 高质量的单元测试 Spock单元测试框架详讲
文章目录 方法篇 为什么需要单元测试 单元测试的定义 单元测试与其他测试的区别 单元测试的作用 关于单元测试的成本 如何写好单元测试 什么场景适合单元测试 单元测试的粒度 关于TDD TDD的三定律 ...
- 系统稳定型建设之单元测试Spock落地
文章目录 背景 项目紧,没时间就不用写单测了吗 单元测试与集成测试的区别 单元测试可以带来的好处 目前主流的mock(单元测试)框架 实际调研过程 为什么不用最流行的mockito Groovy简单培 ...
- Spock测试框架浅尝
文章目录 0. 写在前面 1. 为什么要进行单元测试? 1.1 什么是单元测试? 1.2 单元测试的几个好处 2. Spock框架介绍 2.1 规范 Specification 夹具方法 Fixtur ...
- Spock测试框架如何Mock静态方法
1. 问题场景 在写单元测试时,难免会遇到需要Mock的静态方法.当使用Spock测试框架时,Spock提供Moc静态方法Mock只支持Groovy语言,无法支持Java语言实现的静态方法Mockin ...
- @sql 单元测试_SQL单元测试:使用异常
@sql 单元测试 With this article, we will complete our journey with SQL Unit Testing. But first, let's re ...
- @sql 单元测试_SQL单元测试最佳实践
@sql 单元测试 SQL unit testing is a testing method which allows us to test the smallest, atomic programm ...
- AlexeyAB DarkNet YOLOv3框架解析与应用实践(六)
AlexeyAB DarkNet YOLOv3框架解析与应用实践(六) Tiny Darknet 听过很多人谈论SqueezeNet. SqueezeNet很酷,但它只是优化参数计数.当大多数高质量的 ...
- AlexeyAB DarkNet YOLOv3框架解析与应用实践(五)
AlexeyAB DarkNet YOLOv3框架解析与应用实践(五) RNNs in Darknet 递归神经网络是表示随时间变化的数据的强大模型.为了更好地介绍RNNs,我强烈推荐Andrej K ...
- AlexeyAB DarkNet YOLOv3框架解析与应用实践(四)
AlexeyAB DarkNet YOLOv3框架解析与应用实践(四) Nightmare 从前,在一所大学的大楼里,西蒙尼亚.维达第和齐瑟曼有一个很好的主意,几乎和你现在坐的大楼完全不同.他们想,嘿 ...
- AlexeyAB DarkNet YOLOv3框架解析与应用实践(三)
AlexeyAB DarkNet YOLOv3框架解析与应用实践(三) ImageNet分类 您可以使用Darknet为1000级ImageNet挑战赛分类图像.如果你还没有安装Darknet,你应该 ...
最新文章
- ADPLUS使用配置文件设置断点时无法创建DUMP文件原因分析
- CentOS 7 网络连接优先由与无线问题解决
- OCP考试052考试,新的考题还有答案整理-23题
- 谈谈2018年区块链大事件
- 判断一段文件是UTF-8编码还是GB2312的编码方式
- java 同步锁_Java多线程:synchronized同步锁的使用和实现原理
- 尝试做一个简单的文件系统
- 【Allwinner】---全志GPIO号 计算 最新最详细
- 最好用电脑录音软件推荐
- Ubuntu 16.04 双网卡 同时上内外网
- html视频全套教程,用Dreamweaver讲解html全套视频教程分享
- bat脚本删除文件夹下的重复文件
- 如何用Java读取单元格的数据_Java读取Excel中的单元格数据
- python安装 Autodesk FBX 包
- 宝贝快出生的这三个表现,孕妈尽快去医院待产
- antd Mobile Tabs 垂直布局 后台传入数据
- 【2020版冲刺年薪30W】超全大数据学习路线+思维导图
- android全景设置高度,Android全景SDK | 百度地图API SDK
- 机器学习----纯手撸线性回归代码
- android实现mysql数据库存储_一个简单的Android端对象代理数据库系统的实现(二、执行+存储)...