优秀的编程知识分享平台

网站首页 > 技术文章 正文

使用 Spock 编写高效简洁的单元测试

nanyue 2024-10-17 11:16:14 技术文章 10 ℃

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 单元测试的速度和简洁性:

  1. 利用 Spock 的 Mocking 功能:快速模拟依赖,避免加载整个应用上下文。
  2. 使用测试切片:仅加载需要测试的部分,减少不必要的 Bean 加载。
  3. 数据驱动测试:使用 where 块编写参数化测试,减少重复代码。
  4. 并行执行测试:配置测试框架以并行执行,提高测试总体速度。
  5. 优化测试配置:使用内存数据库、调整日志级别等,提升测试性能。
  6. 保持测试类简洁:聚焦于单一功能,利用 Spock 的语法糖编写清晰的测试用例。

通过以上策略,您可以编写高效、简洁且易于维护的 Spock 单元测试,进一步提升开发和测试的整体效率。

如果您有具体的测试案例或需要进一步的指导,欢迎继续交流!

Tags:

最近发表
标签列表