网站首页 > 技术文章 正文
1.依赖注入与 Mocking
Spock 内置了强大的 mocking 功能,允许您轻松地模拟依赖,从而避免加载整个应用上下文。这不仅加快了测试速度,还使测试更加专注于单元功能。
示例:测试 CurrencyConfigServiceImpl
假设我们要为 CurrencyConfigServiceImpl 编写单元测试,该类依赖于 CurrencyConfigRepository 和 BankService。
package com.uaepay.application.remittance.domainservice.base.impl;
import com.uaepay.application.remittance.core.dal.dataobject.CurrencyCountryDO;
import com.uaepay.application.remittance.core.dal.repository.CurrencyConfigRepository;
import com.uaepay.application.remittance.domainservice.base.CurrencyConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CurrencyConfigServiceImpl implements CurrencyConfigService {
public final String CACHE_CURRENCY_CONFIG_ALL = "CURRENCY_CONFIG_ALL";
public final String CACHE_CURRENCY_CONFIG_BY_COUNTRY = "CURRENCY_CONFIG_BY_COUNTRY";
@Autowired
CurrencyConfigRepository currencyConfigRepository;
@Autowired
BankService bankService;
@Override
public String convertData(String channelCode, String rBankCode, String caOrSaLength) {
BankCodeDO bankCodeDO = currencyConfigRepository.selectBankCodeInfo(channelCode, rBankCode, caOrSaLength);
if (bankCodeDO == null) {
return null;
}
return bankCodeDO.getCodeValue();
}
// 其他方法省略
}
Spock 测试示例
package com.uaepay.application.remittance.domainservice.base.impl
import com.uaepay.application.remittance.core.dal.dataobject.BankCodeDO
import com.uaepay.application.remittance.core.dal.repository.CurrencyConfigRepository
import com.uaepay.application.remittance.domain.service.BankService
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
class CurrencyConfigServiceImplSpec extends Specification {
def currencyConfigRepository = Mock(CurrencyConfigRepository)
def bankService = Mock(BankService)
def currencyConfigService = new CurrencyConfigServiceImpl(
currencyConfigRepository: currencyConfigRepository,
bankService: bankService
)
def "convertData should return codeValue when BankCodeDO exists"() {
given:
String channelCode = "CHANNEL123"
String rBankCode = "RBANK456"
String caOrSaLength = "15"
BankCodeDO bankCodeDO = new BankCodeDO(codeValue: "CODE789")
currencyConfigRepository.selectBankCodeInfo(channelCode, rBankCode, caOrSaLength) >> bankCodeDO
when:
def result = currencyConfigService.convertData(channelCode, rBankCode, caOrSaLength)
then:
result == "CODE789"
}
def "convertData should return null when BankCodeDO does not exist"() {
given:
String channelCode = "CHANNEL123"
String rBankCode = "RBANK456"
String caOrSaLength = "15"
currencyConfigRepository.selectBankCodeInfo(channelCode, rBankCode, caOrSaLength) >> null
when:
def result = currencyConfigService.convertData(channelCode, rBankCode, caOrSaLength)
then:
result == null
}
@TestConfiguration
static class Config {
@Bean
CurrencyConfigServiceImpl currencyConfigService() {
return new CurrencyConfigServiceImpl()
}
}
}
2.使用测试切片(@MockBean 和 @SpringBootTest)
尽量避免加载整个应用上下文,使用 Spock 的 @SpringBootTest 结合 @MockBean 仅加载需要测试的部分。
示例:测试 SynCurrencyService
假设我们需要测试 SynCurrencyService 中的方法,该方法依赖于 CurrencyConfigService。
package com.uaepay.application.remittance.domainservice.base.impl;
import com.uaepay.application.remittance.core.dal.dataobject.CurrencyBO;
import com.uaepay.application.remittance.domainservice.common.cache.CacheInvalidator;
import com.uaepay.application.remittance.domainservice.base.CurrencyConfigService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
@Service
public class SynCurrencyService {
@Autowired
CurrencyConfigService currencyConfigService;
@Autowired
CacheInvalidator cacheInvalidator;
public void syncCurrencies(List<CurrencyBO> addList, List<CurrencyBO> removeList) {
// 禁用
removeList.each { currencyBO ->
try {
currencyConfigService.disableCountryCurrency(currencyBO)
} catch (Exception e) {
// 记录警告
}
}
// 初始化国家币种配置
addList.each { currencyBO ->
currencyConfigService.saveCountryCurrency(currencyBO)
}
}
// 其他方法省略
}
Spock 测试示例
package com.uaepay.application.remittance.domainservice.base.impl
import com.uaepay.application.remittance.core.dal.dataobject.CurrencyBO
import com.uaepay.application.remittance.domainservice.common.cache.CacheInvalidator
import com.uaepay.application.remittance.domainservice.base.CurrencyConfigService
import spock.lang.Specification
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.beans.factory.annotation.Autowired
@SpringBootTest
class SynCurrencyServiceSpec extends Specification {
@Autowired
SynCurrencyService synCurrencyService
@MockBean
CurrencyConfigService currencyConfigService
@MockBean
CacheInvalidator cacheInvalidator
def "syncCurrencies should disable and save country currencies"() {
given:
List<CurrencyBO> addList = [new CurrencyBO(currencyCode: "USD"), new CurrencyBO(currencyCode: "EUR")]
List<CurrencyBO> removeList = [new CurrencyBO(currencyCode: "JPY")]
currencyConfigService.disableCountryCurrency(_) >> { args -> /* No-op */ }
currencyConfigService.saveCountryCurrency(_) >> { args -> /* No-op */ }
when:
synCurrencyService.syncCurrencies(addList, removeList)
then:
1 * currencyConfigService.disableCountryCurrency(removeList[0])
1 * currencyConfigService.saveCountryCurrency(addList[0])
1 * currencyConfigService.saveCountryCurrency(addList[1])
}
def "syncCurrencies should handle exceptions during disable"() {
given:
List<CurrencyBO> addList = []
List<CurrencyBO> removeList = [new CurrencyBO(currencyCode: "JPY")]
currencyConfigService.disableCountryCurrency(_) >> { throw new RuntimeException("Disable failed") }
when:
synCurrencyService.syncCurrencies(addList, removeList)
then:
1 * currencyConfigService.disableCountryCurrency(removeList[0])
// Exception is caught, so no exception thrown to the test
}
}
3.优化测试配置
3.1 使用内存数据库
如果测试涉及数据库操作,使用 H2 等内存数据库可以加快测试速度。
// 在 build.gradle 或 pom.xml 中添加 H2 依赖
testImplementation 'com.h2database:h2'
3.2 并行执行测试
配置 JUnit 5 以并行执行 Spock 测试,充分利用多核 CPU。
在 src/test/resources/junit-platform.properties 中添加:
junit.jupiter.execution.parallel.enabled = true
junit.jupiter.execution.parallel.mode.default = concurrent
junit.jupiter.execution.parallel.mode.classes.default = concurrent
3.3 减少不必要的 Bean 加载
仅加载测试所需的 Bean,避免加载整个上下文。
import org.springframework.boot.test.context.TestConfiguration
import org.springframework.context.annotation.Bean
@TestConfiguration
static class TestConfig {
@Bean
SynCurrencyService synCurrencyService() {
return new SynCurrencyService()
}
@Bean
CacheInvalidator cacheInvalidator() {
return Mock(CacheInvalidator)
}
}
4.简化测试编写
利用 Spock 的特性,如数据驱动测试、自动 Mocking 等,编写简洁的测试用例。
示例:数据驱动测试
def "convertData should return #expected when channelCode=#channelCode, rBankCode=#rBankCode, caOrSaLength=#caOrSaLength"() {
given:
currencyConfigRepository.selectBankCodeInfo(channelCode, rBankCode, caOrSaLength) >> bankCodeDO
when:
def result = currencyConfigService.convertData(channelCode, rBankCode, caOrSaLength)
then:
result == expected
where:
channelCode | rBankCode | caOrSaLength | bankCodeDO | expected
"CH1" | "RB1" | "16" | new BankCodeDO(codeValue: "C1")| "C1"
"CH2" | "RB2" | "17" | null | null
}
5.Mock Dubbo 服务
使用 @MockBean 结合 Spock 轻松模拟 Dubbo 服务,避免实际的网络调用。
示例:测试 MockRemChannelFacade
假设 MockRemChannelFacade 实现了 RemChannelFacade 接口。
package com.uaepay.application.remittance.domainservice.channel.mock;
import com.uaepay.application.remittance.channel.template.api.RemChannelFacade;
import com.uaepay.application.remittance.channel.template.reqresp.ChannelResponse;
import org.apache.dubbo.config.annotation.Service;
@Service(group = "localMock")
public class MockRemChannelFacade implements RemChannelFacade {
@Override
public ChannelResponse apply(String request) {
// TODO
return null;
}
}
Spock 测试示例
package com.uaepay.application.remittance.domainservice.channel.mock
import com.uaepay.application.remittance.channel.template.reqresp.ChannelResponse
import com.uaepay.application.remittance.channel.template.api.RemChannelFacade
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.mock.mockito.MockBean
@SpringBootTest
class MockRemChannelFacadeSpec extends Specification {
@Autowired
RemChannelFacade remChannelFacade
def "apply should return ChannelResponse"() {
given:
String request = "TestRequest"
ChannelResponse mockResponse = new ChannelResponse(status: "SUCCESS")
remChannelFacade.apply(request) >> mockResponse
when:
def response = remChannelFacade.apply(request)
then:
response.status == "SUCCESS"
}
}
6.其他优化技巧
- 避免使用 @SpringBootTest:仅在需要集成测试时使用,单元测试尽量使用纯 Spock 规范。
- 使用 setup 和 cleanup:合理利用 setup() 和 cleanup() 方法进行测试前后的初始化和清理。
- 日志级别调整:在测试环境中调整日志级别,避免日志输出影响测试性能。
- @TestConfiguration static class Config { @Bean Environment env() { def env = Mock(Environment) env.getProperty(_, _) >> null return env } }
7.示例:测试带有线程池的服务
假设我们要测试 RemittanceRateRepositoryImpl 中的方法,该方法使用了 CacheInvalidator 和 clearRedisPoolExecutor。
package com.uaepay.application.remittance.domainservice.base.repository.impl;
import com.uaepay.application.remittance.domainservice.common.cache.CacheInvalidator;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.concurrent.Executor;
@Repository
public class RemittanceRateRepositoryImpl {
@Autowired
Executor clearRedisPoolExecutor;
@Autowired
CacheInvalidator cacheInvalidator;
public void clearCaches() {
clearRedisPoolExecutor.execute(() -> {
cacheInvalidator.clearKeyAllByScan("CACHE_QUOTATION_STRATEGY");
cacheInvalidator.clearKeyAllByScan("CACHE_CHARGE_STRATEGY");
});
}
}
Spock 测试示例
package com.uaepay.application.remittance.domainservice.base.repository.impl
import com.uaepay.application.remittance.domainservice.common.cache.CacheInvalidator
import spock.lang.Specification
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.MockBean
import org.springframework.boot.test.context.SpringBootTest
import java.util.concurrent.Executor
@SpringBootTest
class RemittanceRateRepositoryImplSpec extends Specification {
@Autowired
RemittanceRateRepositoryImpl remittanceRateRepository
@MockBean
CacheInvalidator cacheInvalidator
@MockBean
Executor clearRedisPoolExecutor
def "clearCaches should execute cache invalidation"() {
when:
remittanceRateRepository.clearCaches()
then:
1 * clearRedisPoolExecutor.execute(_) >> { Runnable runnable ->
runnable.run()
}
1 * cacheInvalidator.clearKeyAllByScan("CACHE_QUOTATION_STRATEGY")
1 * cacheInvalidator.clearKeyAllByScan("CACHE_CHARGE_STRATEGY")
}
}
8.总结
通过以下方法,您可以显著提升 Spock 单元测试的速度和简洁性:
- 利用 Spock 的 Mocking 功能:快速模拟依赖,避免加载整个应用上下文。
- 使用测试切片:仅加载需要测试的部分,减少不必要的 Bean 加载。
- 数据驱动测试:使用 where 块编写参数化测试,减少重复代码。
- 并行执行测试:配置测试框架以并行执行,提高测试总体速度。
- 优化测试配置:使用内存数据库、调整日志级别等,提升测试性能。
- 保持测试类简洁:聚焦于单一功能,利用 Spock 的语法糖编写清晰的测试用例。
通过以上策略,您可以编写高效、简洁且易于维护的 Spock 单元测试,进一步提升开发和测试的整体效率。
如果您有具体的测试案例或需要进一步的指导,欢迎继续交流!
猜你喜欢
- 2024-10-17 程序员有福了!万字长文带你掌握SpringBoot所提供的测试解决方案
- 2024-10-17 如何使用Spring Boot提供的测试工具和注解。
- 2024-10-17 单元测试实践(Spring-boot+Junbit5+Mockito)
- 2024-10-17 Spring云原生实战指南:8 弹性和可扩展性
- 2024-10-17 SpringBoot 太强了,这些优势你需要了解
- 2024-10-17 如何使用Spring Cloud Contract(如何使用朋友的山姆卡)
- 2024-10-17 Spring Boot 开发离不开这些注解,快来学习啦!
- 2024-10-17 Spring Boot中文参考指南46.3.11、自动配置的Spring WebFlux测试
- 2024-10-17 一台不容错过的Java单元测试代码“永动机”
- 2024-10-17 Spring Boot 中如何完成单元测试?
- 最近发表
- 标签列表
-
- cmd/c (64)
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- sqlset (64)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- chromepost (65)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- linux删除一个文件夹 (65)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)