You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用JUnit和Mockito模拟Azure文件存储上传服务避免真实调用

问题分析与解决方案

核心问题

  1. Mock对象未被使用:原服务类中直接通过CloudStorageAccount.parse(CONNECTION_STRING)生成实例,测试里的CloudStorageAccount mock完全没参与实际逻辑,导致验证时提示"无交互"。
  2. 测试顺序错误:原测试先调用uploadFile,后设置Mock Stub,导致Stub无法生效。
  3. 硬编码配置:服务类里的连接字符串、共享名等是静态常量,无法在测试中替换,导致验证参数不匹配。
  4. 覆盖率不足:未覆盖异常分支,导致覆盖率仅87%。

第一步:重构ReportUploadService

将硬编码配置移至AzureFileProperties,抽离存储账户创建逻辑为可替换的方法,方便测试Mock:

@Service
@Log4j2
public class ReportUploadService {

    private final AzureFileProperties azureFileProperties;

    @Autowired
    public ReportUploadService(AzureFileProperties azureFileProperties) {
        this.azureFileProperties = azureFileProperties;
    }

    public void uploadFile(String filePath) throws FileNotFoundException {
        try {
            CloudStorageAccount storageAccount = getCloudStorageAccount();
            CloudFileClient fileClient = storageAccount.createCloudFileClient();
            CloudFileShare share = fileClient.getShareReference(azureFileProperties.getShareName());
            CloudFileDirectory rootDir = share.getRootDirectoryReference();
            CloudFileDirectory sampleDir = rootDir.getDirectoryReference(azureFileProperties.getDirName());
            CloudFile cloudFile = sampleDir.getFileReference("testfile.txt");

            cloudFile.uploadFromFile(filePath);
        } catch (Exception ex) {
            log.error("File could not be uploaded", ex);
            throw new FileNotFoundException(ex.getMessage());
        }
    }

    // 抽为protected方法,测试时可通过Spy替换
    protected CloudStorageAccount getCloudStorageAccount() throws InvalidKeyException, URISyntaxException {
        return CloudStorageAccount.parse(azureFileProperties.getConnectionString());
    }
}

对应的AzureFileProperties配置类:

@ConfigurationProperties(prefix = "azure.file")
public class AzureFileProperties {
    private String connectionString;
    private String shareName;
    private String dirName;

    // 生成getter、setter
}

第二步:修复测试类

使用Spy替换存储账户创建逻辑,调整Stub顺序,添加异常分支测试:

@ExtendWith(MockitoExtension.class)
class ReportUploadServiceTest {

    @InjectMocks
    private ReportUploadService uploadService;

    @Mock
    private AzureFileProperties azureFileProperties;

    @Mock
    private CloudStorageAccount cloudStorageAccount;

    @Mock
    private CloudFileClient cloudFileClient;

    @Mock
    private CloudFileDirectory rootDir;

    @Mock
    private CloudFileDirectory sampleDir;

    @Mock
    private CloudFileShare cloudFileShare;

    @Mock
    private CloudFile cloudFile;

    @BeforeEach
    void setUp() {
        // 配置Mock配置类返回值
        when(azureFileProperties.getConnectionString()).thenReturn("mock-connection");
        when(azureFileProperties.getShareName()).thenReturn("abc");
        when(azureFileProperties.getDirName()).thenReturn("test");

        // 创建Spy对象,替换存储账户创建方法
        uploadService = spy(uploadService);
        doReturn(cloudStorageAccount).when(uploadService).getCloudStorageAccount();
    }

    @Test
    void uploadFile_success() throws Exception {
        // 提前设置所有Mock Stub
        when(cloudStorageAccount.createCloudFileClient()).thenReturn(cloudFileClient);
        when(cloudFileClient.getShareReference("abc")).thenReturn(cloudFileShare);
        when(cloudFileShare.getRootDirectoryReference()).thenReturn(rootDir);
        when(rootDir.getDirectoryReference("test")).thenReturn(sampleDir);
        when(sampleDir.getFileReference("testfile.txt")).thenReturn(cloudFile);
        doNothing().when(cloudFile).uploadFromFile(anyString());

        // 调用测试方法
        File testFile = new File("src/test/resources/testfile.txt");
        uploadService.uploadFile(testFile.getAbsolutePath());

        // 验证交互
        verify(cloudStorageAccount).createCloudFileClient();
        verify(cloudFileClient).getShareReference("abc");
        verify(cloudFileShare).getRootDirectoryReference();
        verify(rootDir).getDirectoryReference("test");
        verify(sampleDir).getFileReference("testfile.txt");
        verify(cloudFile).uploadFromFile(testFile.getAbsolutePath());
    }

    @Test
    void uploadFile_throwsFileNotFoundException() throws Exception {
        // 模拟上传异常
        when(cloudStorageAccount.createCloudFileClient()).thenReturn(cloudFileClient);
        when(cloudFileClient.getShareReference("abc")).thenReturn(cloudFileShare);
        when(cloudFileShare.getRootDirectoryReference()).thenReturn(rootDir);
        when(rootDir.getDirectoryReference("test")).thenReturn(sampleDir);
        when(sampleDir.getFileReference("testfile.txt")).thenReturn(cloudFile);
        doThrow(new StorageException("upload failed", null, null, null, null))
                .when(cloudFile).uploadFromFile(anyString());

        // 验证异常抛出
        FileNotFoundException exception = assertThrows(FileNotFoundException.class,
                () -> uploadService.uploadFile("invalid-file-path"));
        assertTrue(exception.getMessage().contains("upload failed"));
    }
}

关键修改说明

  • Spy替换逻辑:通过spy(uploadService)替换getCloudStorageAccount方法,避免调用真实的Azure存储API。
  • Stub顺序调整:先设置所有Mock的返回值,再调用测试方法,确保Mock交互被捕获。
  • 异常分支覆盖:新增异常测试用例,覆盖catch块逻辑,将测试覆盖率提升至100%。
  • 参数匹配:验证时使用服务类实际使用的参数(如原代码中的SHARE_NAME="abc"),避免参数不匹配导致验证失败。

内容的提问来源于stack exchange,提问作者RuchiG

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 19:17:02