单元测试说明
spock是基于groovy的测试框架,spock本身集成了Mockito+junit的功能,并且可以springboot-test结合启动容器测试。静态方法和私有方法仍需要使用PowerMock进行功能增强。
第一次使用推荐先看一下demo代码再看官方文档。
demo代码:可参考金融网关 CiticLoanApplyMsgConvertServiceImplTest(基础用法) 和 CiticbankLoanApplyServiceImplTest(Data Table 例子)
官方学习文档:http://spockframework.org/spock/docs/1.3/all_in_one.html
单元测试需要保证以下几点:
- 不依赖外部系统
- 单元测试足够“单元”,避免流程过长的测试逻辑
- 测试方法的数据操作不影响真实数据源的数据现状
- 每个测试方法都要有verify或者Assert的验证或断言的操作,否则都是无效的测试方式
- 一些依赖外部系统的调用,或不需要每次单元测试都执行的测试类或测试方法,及时使用
@Ignore
,避免mvn test时执行单元测试异常
基础语法
段落
spock单测方法是场景化测试,将单元测试分成了 [given->]when->then[->where] 或者 [given->]expect->where 段落,
每一个段落都有固定的作用
given: 准备数据阶段,可以以没有
when: 一般放置各种mock和测试方法调用,所以spock要求这个段落必须有(或者用expect)
then: 用于写断言的区域,spock要求必须有
expect: 可以理解为when + then的集合,需要固定跟 where搭配使用
where: 变量填充区。例如我们需要测试入参不同走不同逻辑的测试,junit需要写多个测试用例和mock。spock允许将这部分以数据表格集成到同一个用例,
每行代表一个测试场景,每一列代表一个变量
//@Subject是定义一个主题,目前没什么用处
@Subject(CiticLoanApplyMsgConvertServiceImpl.class)
class CiticLoanApplyMsgConvertServiceImplSpockTest extends SpockBaseRunner {
def init() {
//等于junit @before
}
//spock默认遇到不通过的测试场景就不跑后续单测,@Unroll代表不中断
@Unroll
def "方法名称,建议直接使用中文"() {
given:
//准备数据阶段,没有可写null,或可以不写
when:
//这里写各种mock方法的返回等和调用需要单测的目标方法,没有可写null
null
then:
//then用于编写断言,判断相等可以直接用 ==
loanApplyRequest.data.applyAmt == 1004.00
and://断言分段编写用and连接
def contract = loanApplyRequest.data.contractInfoList[0]
contract.billCode == '60000144202004020128P'
}
}
启动模式
集成springboot-test 测试类继承 SpockBaseRunner,使用这种模式初始化方法为 init() 类似@Before(SpockBaseRunner封装了一下原生的setup())
这种模式下默认会为单测开启数据库事务,单测执行完后自动回滚事务
Specification 纯粹mock的形式类似纯junit + mockito,使用这种模式初始化方法为 setup() 类似junit @Before
Mock
mock一个对象例子如下
def serviceAObject = new ServiceA(
property1: Mock(ServiceB)
//多重mock
property2: Mock(new ServiceC(
cProperty1: Mock(ServiceD)
))
)
需要注意的是如果使用SpockBaseRunner 启动,默认容器就会注入所有依赖的bean,所以这时如果需要局部Mock某些属性,需要主动
在原来的Service上暴露对应属性set方法,然后再mock,可以参考 CiticbankLoanApplyServiceImplTest 中 Mock sellSendFileService
class CiticbankLoanApplyServiceImplTest extends SpockBaseRunner {
...
def sellSendFileService = Mock(SellSendFileService)
@Override
def init() {
...
citicbankLoanApplyService.sellSendFileService = sellSendFileService
}
}
@Setter //方便单元测试局部mock注入
@Service
public class CiticbankLoanApplyServiceImpl implements SellLoanApplyService
@Autowired
@Qualifier("citicbankSellSendFileServiceImpl")
private SellSendFileService sellSendFileService;
}
mock 方法的返回值
citicbankHttpServerTemplate.packageRequest(*_) >> response
_指代入参,类似Mockit的any(),*_标识任意个参数
赋值
变量 << 值
变量 << [数组]
变量 << new 对象(property1: value1, property2: value2)
断言
断言值相等 loanApplyRequest.data.applyAmt == 1004.00
其他场景可以转化为类似 表达式 == true
代表方法必须执行一次 1 * sellSendFileService.uploadAndSendFiles(*_)
代表方法至少行一次 (1.._) * sellSendFileService.uploadAndSendFiles(*_)
代表方法执行2~5次 (2..5) * sellSendFileService.uploadAndSendFiles(*_)
多行数据比较(CiticLoanApplyMsgConvertServiceImplSpockTest)
假设方法正常运行应该保存了4行数据,需要验证实际入库行数是否4行,各行的列值是否正确
...
when:
citicLoanApplyMsgConvertService.saveFileList(transNo, loanApplyRequest.data)
...
List fileInfoList = sellFileInfoMapper.selectList(queryWrapper)
then:
fileInfoList.businessTransNo == [transNo, transNo, transNo, transNo]
fileInfoList.fileCode == ['QT20200407000002179', 'QT20200402000001406', 'FM20200407112812', 'FM20200402117504']
一个单测多个用例(data table)使用(CiticbankLoanApplyServiceImplTest)
spock 运行 when和then 的表达式带有变量,具体的变量在运行时通过 where段落提供的数据填充,
例如以下代码提供了where 3个测试用例,每个用例对应where 里面的一行数据,复杂的变量(response)可以通过数组的方式单独赋值余列表方式等效:
...
when:
citicbankHttpServerTemplate.packageRequest(*_) >> response
...
then:
uploadFileCount * sellSendFileService.uploadAndSendFiles(*_)
sellApply.getSellState() == sellState
where:
response << [
new Response(//模拟成功
code: CommonConstants.ResponseCode.SUCCESS
),
new Response(//模拟前置接口失败
code: CommonConstants.ResponseCode.FRONT_FAILURE
),
new Response(//模拟中信主动返回失败
code: CommonConstants.ResponseCode.FAILURE,
data: new Response()
)]
sellState | uploadFileCount
Constants.SellState.INIT.getState() | 1
Constants.SellState.INIT.getState() | 1
Constants.SellState.FAIL.getState() | 0
@WebAppConfiguration@SpringBootTestclassSpockBaseRunnerextendsSpecification{@AutowiredDataSourceTransactionManager dataSourceTransactionManager
@AutowiredTransactionDefinition transactionDefinition
TransactionStatus transactionStatus
def setup(){
transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);init()}
def init(){}
def cleanup(){
dataSourceTransactionManager.rollback(transactionStatus)clean()}
def clean(){}}
版权归原作者 MiMing仔 所有, 如有侵权,请联系我们删除。