Spring Boot对接鉴权远程系统测试最佳实践:Azure Blob测试优化问询
问题描述
我编写了依托Azure SDK for Blobs实现Blob存储交互的代码,没有启动live应用测试,而是直接编写了Spring Boot JUnit测试用例,不使用任何Mock框架,直接对接真实Blob存储实例验证Java方法逻辑。
现在我需要重构测试结构,解决当前的两个痛点:
- 之前为了赶进度把凭证硬编码在源文件中,即使是私有仓库也不希望提交凭证到代码库
- 需要让这些JUnit测试可以在Azure DevOps流水线中正常运行
现有测试逻辑
测试代码会创建临时容器,执行Blob的存、取、删操作,用GUID生成唯一私有工作区,测试结束后自动清理,现有代码如下:
@SpringBootTest(classes = FileRepositoryServiceAzureBlobImplTest.class) @SpringBootConfiguration @TestConfiguration @TestPropertySource(properties = { "azure-storage-container-name:amlcbackendjunit", "azure-storage-connection-string:[not going to post it on Stackoverflow before rotating it]" }) class FileRepositoryServiceAzureBlobImplTest { private static final Resource LOREM_IPSUM = new ClassPathResource("loremipsum.txt", FileRepositoryServiceAzureBlobImplTest.class); private FileRepositoryServiceAzureBlobImpl uut; private BlobContainerClient blobContainerClient; private String loremChecksum; @Value("${azure-storage-connection-string}") private String azureConnectionString; @Value("${azure-storage-container-name}") private String azureContainerName; @BeforeEach void beforeEach() throws IOException { String containerName = azureContainerName + "-" + UUID.randomUUID(); blobContainerClient = new BlobContainerClientBuilder() .httpLogOptions(new HttpLogOptions().setApplicationId("az-sp-sb-aml")) .clientOptions(new ClientOptions().setApplicationId("az-sp-sb-aml")) .connectionString(azureConnectionString) .containerName(containerName) .buildClient(); blobContainerClient.create(); uut = spy(new FileRepositoryServiceAzureBlobImpl(blobContainerClient)); try (InputStream loremIpsumInputStream = LOREM_IPSUM.getInputStream()) { loremChecksum = DigestUtils.sha256Hex(loremIpsumInputStream); } blobContainerClient .getBlobClient("fox.txt") .upload(BinaryData.fromString("The quick brown fox jumps over the lazy dog")); } @AfterEach void afterEach() throws IOException { blobContainerClient.delete(); } @Test void store_ok() { String desiredFileName = "loremIpsum.txt"; FileItemDescriptor output = assertDoesNotThrow(() -> uut.store(LOREM_IPSUM, desiredFileName)); assertAll( () -> assertThat(output, is(notNullValue())), () -> assertThat(output, hasProperty("uri", hasToString(Matchers.startsWith("azure-blob://")))), () -> assertThat(output, hasProperty("size", equalTo(LOREM_IPSUM.contentLength()))), () -> assertThat(output, hasProperty("checksum", equalTo(loremChecksum))), () -> { String localPart = substringAfter(output.getUri().toString(), "azure-blob://"); assertAll( () -> assertTrue(blobContainerClient.getBlobClient(localPart).exists()) ); } ); } }
生产环境(含SIT/UAT)中Spring Boot应用会从容器环境获取存储连接字符串配置,且当前测试不需要依赖Spring和@TestPropertySource,没有用到上下文Bean。
需求
修改现有测试满足以下要求:
- 连接字符串与代码完全解耦
- 连接字符串不存在时自动忽略该测试(适配开发者首次拉取项目快速启动的场景,正式应用无连接字符串时会自动降级用本地临时目录)
- 配置了对应环境变量的Azure DevOps流水线中可以正常执行测试
现有Azure DevOps构建任务配置如下:
- task: Gradle@2 displayName: Build with Gradle inputs: gradleWrapperFile: gradlew gradleOptions: -Xmx3072m $(gradleJavaProperties) options: -Pci=true -PbuildId=$(Build.BuildId) -PreleaseType=${{parameters.releaseType}} jdkVersionOption: 1.11 jdkArchitectureOption: x64 publishJUnitResults: true sqAnalysisEnabled: true sqGradlePluginVersionChoice: specify sqGradlePluginVersion: 3.2.0 testResultsFiles: '$(System.DefaultWorkingDirectory)/build/test-results/**/TEST-*.xml' tasks: clean build
解决方案
第一步:重构测试代码,移除Spring依赖,新增条件判断
用JUnit 5的@EnabledIfEnvironmentVariable注解配合环境变量读取配置,不需要依赖Spring上下文,更轻量,没有配置对应环境变量时测试会自动跳过:
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; import java.util.Optional; import java.util.UUID; // 其他原有import保持不变 @EnabledIfEnvironmentVariable(named = "AZURE_STORAGE_CONNECTION_STRING", matches = ".+", disabledReason = "未配置Azure存储连接字符串,跳过Blob存储集成测试") class FileRepositoryServiceAzureBlobImplTest { private static final Resource LOREM_IPSUM = new ClassPathResource("loremipsum.txt", FileRepositoryServiceAzureBlobImplTest.class); // 直接从环境变量读取配置,无需@Value注解 private static final String AZURE_CONNECTION_STRING = System.getenv("AZURE_STORAGE_CONNECTION_STRING"); private static final String AZURE_CONTAINER_NAME = Optional.ofNullable(System.getenv("AZURE_STORAGE_CONTAINER_NAME")) .orElse("amlcbackendjunit"); private FileRepositoryServiceAzureBlobImpl uut; private BlobContainerClient blobContainerClient; private String loremChecksum; @BeforeEach void beforeEach() throws IOException { String containerName = AZURE_CONTAINER_NAME + "-" + UUID.randomUUID(); blobContainerClient = new BlobContainerClientBuilder() .httpLogOptions(new HttpLogOptions().setApplicationId("az-sp-sb-aml")) .clientOptions(new ClientOptions().setApplicationId("az-sp-sb-aml")) .connectionString(AZURE_CONNECTION_STRING) .containerName(containerName) .buildClient(); blobContainerClient.create(); uut = spy(new FileRepositoryServiceAzureBlobImpl(blobContainerClient)); try (InputStream loremIpsumInputStream = LOREM_IPSUM.getInputStream()) { loremChecksum = DigestUtils.sha256Hex(loremIpsumInputStream); } blobContainerClient .getBlobClient("fox.txt") .upload(BinaryData.fromString("The quick brown fox jumps over the lazy dog")); } @AfterEach void afterEach() { // 改为deleteIfExists避免容器不存在时报错 blobContainerClient.deleteIfExists(); } // 测试方法保持原有逻辑不变 @Test void store_ok() { String desiredFileName = "loremIpsum.txt"; FileItemDescriptor output = assertDoesNotThrow(() -> uut.store(LOREM_IPSUM, desiredFileName)); assertAll( () -> assertThat(output, is(notNullValue())), () -> assertThat(output, hasProperty("uri", hasToString(Matchers.startsWith("azure-blob://")))), () -> assertThat(output, hasProperty("size", equalTo(LOREM_IPSUM.contentLength()))), () -> assertThat(output, hasProperty("checksum", equalTo(loremChecksum))), () -> { String localPart = substringAfter(output.getUri().toString(), "azure-blob://"); assertTrue(blobContainerClient.getBlobClient(localPart).exists()); } ); } }
第二步:本地开发适配
开发者本地如果需要运行该测试,只需在本机配置AZURE_STORAGE_CONNECTION_STRING环境变量即可,不需要修改代码,也不会把凭证提交到代码仓库。没有配置该环境变量时测试会自动跳过,不影响项目整体构建。
第三步:Azure DevOps流水线配置
首先在流水线的变量库中添加AZURE_STORAGE_CONNECTION_STRING,设置为保密变量避免泄露。然后修改Gradle任务配置,将环境变量传递给测试进程:
- task: Gradle@2 displayName: Build with Gradle inputs: gradleWrapperFile: gradlew env: AZURE_STORAGE_CONNECTION_STRING: $(AZURE_STORAGE_CONNECTION_STRING) gradleOptions: -Xmx3072m $(gradleJavaProperties) options: -Pci=true -PbuildId=$(Build.BuildId) -PreleaseType=${{parameters.releaseType}} jdkVersionOption: 1.11 jdkArchitectureOption: x64 publishJUnitResults: true sqAnalysisEnabled: true sqGradlePluginVersionChoice: specify sqGradlePluginVersion: 3.2.0 testResultsFiles: '$(System.DefaultWorkingDirectory)/build/test-results/**/TEST-*.xml' tasks: clean build
如果Gradle测试配置默认会剥离环境变量,还需要在build.gradle中添加测试进程的环境变量传递配置:
test { environment "AZURE_STORAGE_CONNECTION_STRING", System.getenv("AZURE_STORAGE_CONNECTION_STRING") environment "AZURE_STORAGE_CONTAINER_NAME", System.getenv("AZURE_STORAGE_CONTAINER_NAME") // 其他测试配置保持不变 }
内容的提问来源于stack exchange,提问作者usr-local-ΕΨΗΕΛΩΝ

