简体   繁体   English

针对经过身份验证的远程系统进行 Spring 启动测试的最佳实践

[英]Best practices for Spring boot testing against authenticated remote system

I have written code that leverages Azure SDK for Blobs in order to interact with the blob storage.我编写了利用Azure SDK 的代码,以便与 Blob 存储交互。

As a clever and dutiful developer, I have not tested my code by navigating the live application, but rather created a Spring Boot JUnit test and spent a few hours fixing all my mistakes.作为一名聪明而尽职的开发人员,我没有通过导航实时应用程序来测试我的代码,而是创建了一个 Spring Boot JUnit 测试并花了几个小时来修复我所有的错误。 I didn't use anyh kind of mocking, in fact, as my problem was using the library the correct way.事实上,我没有使用任何类型的 mocking,因为我的问题是以正确的方式使用库。 I ran the code against a live instance of a blob storage and checked that all my Java methods worked as expected.我针对 blob 存储的实时实例运行代码,并检查我的所有 Java 方法是否按预期工作。

I am writing here because我写在这里是因为

  • To call it a day, I hardcoded the credentials in my source files.最后,我在我的源文件中硬编码了凭据。 The repository is a company-private repository, not that harm.仓库是公司私有的仓库,没有那个害处。 Credentials can be rotated, developers can all access from Azure portal and get the credentials.凭证可以轮换,开发者都可以从Azure门户访问并获取凭证。 But still I don't like the idea of pushing credentials into code但我仍然不喜欢将凭据推送到代码中的想法
  • Having these junit tests work on Azure DevOps pipelines could be some of a good idea让这些 junit 测试在 Azure DevOps 管道上运行可能是个好主意

I know from the very beginning that hardcoding credentials into code is a worst practice, but since this morning I wanted to focus on my task.我从一开始就知道将凭据硬编码到代码中是最糟糕的做法,但从今天早上开始我想专注于我的任务。 Now I want to adopt the best practices.现在我想采用最佳实践。 I am asking about redesigning the test structure我问的是重新设计测试结构

Testing code is this.测试代码是这样的。

The code creates an ephemeral container and tries to store/retrieve/delete blobs.该代码创建一个临时容器并尝试存储/检索/删除 blob。 It uses a GUID to create a unique private workspace, to clear after test is finished.它使用 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())
                    );
                }
        );
    }

}

In production (but also in SIT/UAT), the real Spring Boot application will get the configuration from the Container environment, including the storage connection string.在生产中(也包括在 SIT/UAT 中),真正的 Spring Boot 应用程序将从 Container 环境中获取配置,包括存储连接字符串。 Yes, for this kind of test I could also avoid using Spring and @TestPropertySource , because I'm not leveraging any bean from the context.是的,对于这种测试,我也可以避免使用 Spring 和@TestPropertySource ,因为我没有利用上下文中的任何 bean。

Question问题

I want to ask how can I amend this test in order to我想问一下如何修改这个测试以便

  1. Decouple the connection string from code将连接字符串与代码分离
  2. Softly-ignore the test if for some reason the connection string is not present (eg developer downloaded the project the first time and wants to kick-start) (note 1)如果由于某种原因连接字符串不存在(例如,开发人员第一次下载项目并想要启动),请轻柔地忽略测试(注 1)
  3. Integrate this test (with a working connection string) from Azure DevOps pipelines, where I can configure virtually any environment variable and such集成来自 Azure DevOps 管道的此测试(使用工作连接字符串),我可以在其中配置几乎任何环境变量等

Here is the build job comprised of tests这是由测试组成的构建作业

          - 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

Note 1: the live application can be kick-started without the storage connection string.注 1:实时应用程序可以在没有存储连接字符串的情况下启动。 It falls back to a local temporary directory.它回退到本地临时目录。

The answer is a bit complex to explain, so I did my best答案解释起来有点复杂,所以我尽力了

TL;DR长话短说

Note that the original variable names are redacted and YMMV if you try to recreate the example with the exact keys I used请注意,如果您尝试使用我使用的确切键重新创建示例,则原始变量名称和 YMMV 已被编辑

  • Create a secret pipeline variable containing the connection string, and bury* it into the pipeline创建一个包含连接字符串的秘密管道变量,并将其埋入*到管道中

Example name testStorageAccountConnectionString示例名称testStorageAccountConnectionString

管道变量

  • Change the Gradle task更改Gradle任务
              - task: Gradle@3
                displayName: Build with Gradle
                inputs:
                  gradleWrapperFile: gradlew
                  gradleOptions: -Xmx10240m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -DAZURE_STORAGE_CONNECTION_STRING=$(AZURE_STORAGE_CONNECTION_STRING)
                  options: --build-cache -Pci=true -PgitCommitId=$(Build.SourceVersion) -PbuildId=$(Build.BuildId) -Preckon.stage=${{parameters.versionStage}} -Preckon.scope=${{parameters.versionScope}}
                  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
                env:
                 AZURE_STORAGE_CONNECTION_STRING: $(testStorageAccountConnectionString)

Explanation解释

  • Spring Boot accepts placeholder ${azure.storageConnectionString} from an environment variable AZURE_STORAGE_CONNECTION_STRING . Spring Boot 从环境变量AZURE_STORAGE_CONNECTION_STRING接受占位符${azure.storageConnectionString} Please read the docs and try it locally first.请阅读文档并先在本地尝试。 This means we need to run the test with an environment variable propely set in order to resolve the placeholder这意味着我们需要使用适当设置的环境变量来运行测试,以解析占位符
  • Gradle can run with -D to add an environment variable. Gradle可以用-D运行加个环境变量。 -DAZURE_STORAGE_CONNECTION_STRING=$(AZURE_STORAGE_CONNECTION_STRING) adds an environment variable AZURE_STORAGE_CONNECTION_STRING to the test run equal to the pipeline environment variable AZURE_STORAGE_CONNECTION_STRING (not that fantasy) -DAZURE_STORAGE_CONNECTION_STRING=$(AZURE_STORAGE_CONNECTION_STRING)将环境变量AZURE_STORAGE_CONNECTION_STRING添加到测试运行中,等于管道环境变量AZURE_STORAGE_CONNECTION_STRING (不是那个幻想)
  • Azure DevOps pipelines protect secret variables from unwanted access. Azure DevOps 管道保护秘密变量免遭不必要的访问。 We created the pipeline variable as secret, so there is another trick to do first我们将管道变量创建为秘密变量,因此还有一个技巧需要先做

Gradle's env attributes set environment variable for the pipeline container. Gradle 的env属性为管道容器设置环境变量。 In this case, we make sure that Gradle runs with AZURE_STORAGE_CONNECTION_STRING set to testStorageAccountConnectionString .在这种情况下,我们确保 Gradle 在AZURE_STORAGE_CONNECTION_STRING设置为testStorageAccountConnectionString的情况下运行。 Env is the only place where Azure pipelines agent will resolve and set free the content of the secret variable Env 是 Azure 管道代理解析和释放秘密变量内容的唯一地方

  • Secrets cannot be retrieved any more from web interface.无法再从 web 接口检索机密。 Azure Pipelines are designed for this Azure 管道就是为此而设计的

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM