We all know about the importance of unit and functional testing when it comes to developing new applications. In particular, functional testing helps us to see the gaps in our code compared to the functionality that we believe our application should have. One major drawback of functional testing is that they are often dependent on external services, whether that be databases or APIs or AWS components. These dependencies incur additional overhead and costs that are unnecessary from a code testing point of view.
我们都知道在开发新应用程序时进行单元和功能测试的重要性。 特别是,与我们认为应用程序应具有的功能相比,功能测试可帮助我们了解代码中的空白。 功能测试的一个主要缺点是它们通常依赖于外部服务,无论是数据库,API还是AWS组件。 从代码测试的角度来看,这些依赖性会导致额外的开销和成本。
As a result, I’ve been focused within my development team on making our functional tests more lightweight and flexible. Through the power of Docker containerization and an open-source library called Testcontainers, I’ve been able to make our functional tests more portable and economical without sacrificing testing quality.
因此,我一直专注于开发团队,致力于使我们的功能测试更加轻巧和灵活。 通过Docker容器化的强大功能和一个名为Testcontainers的开源库,我能够在不牺牲测试质量的情况下使我们的功能测试更加便携和经济。
This guide aims to show a use case (with Java Spring Boot and Cucumber) that can be extended to most applications. We will mock a PostgreSQL database and AWS S3 bucket for our tests and we’ll take a look at Testcontainers and another library called LocalStack, and how Docker containerization in general can reduce the time and costs incurred when writing functional tests for Java applications.
本指南旨在展示一个可以扩展到大多数应用程序的用例(带有Java Spring Boot和Cucumber)。 我们将为测试模拟一个PostgreSQL数据库和AWS S3存储桶,并查看Testcontainers和另一个名为LocalStack的库,以及Docker容器化通常如何减少为Java应用程序编写功能测试时所花费的时间和成本。
先决条件 (Prerequisites)
Knowledge of Docker (https://docs.docker.com/get-started/)
Docker知识( https://docs.docker.com/get-started/ )
Knowledge of and access to AWS (https://aws.amazon.com/getting-started/)
了解和访问AWS( https://aws.amazon.com/getting-started/ )
Knowledge of Spring Boot (https://spring.io/guides/gs/spring-boot/)
Spring Boot的知识( https://spring.io/guides/gs/spring-boot/ )
Knowledge of Cucumber testing (https://cucumber.io/docs/guides/10-minute-tutorial/)
Cucumber测试知识( https://cucumber.io/docs/guides/10-minute-tutorial/ )
背景 (Background)
Testcontainers originally started as a way to programmatically create throwaway Docker containers. Because of the flexibility of Docker images, Testcontainers modules can now spin up containers with databases, web browsers, or mock AWS endpoints. This is what makes Testcontainers powerful for functional testing — it provides a flexible way of standing up dependencies before your code starts.
Testcontainer最初是通过编程方式创建一次性Docker容器的一种方式。 由于Docker映像的灵活性,Testcontainers模块现在可以使用数据库,Web浏览器或模拟AWS端点来启动容器。 这就是使Testcontainers强大地进行功能测试的原因- 它提供了一种灵活的方式来在代码开始前站起依赖性。
Using Testcontainers’s Localstack module in particular can allow you to remove AWS dependencies from functional tests. Localstack provides a containerized way of mocking AWS components, eliminating any need to connect to your real AWS infrastructure during testing. As we’ll see in the next section, combining these two technologies allows you to write functional tests without connecting to external dependencies or changing the application code.
特别是使用Testcontainers的Localstack模块可以使您从功能测试中删除AWS依赖项。 Localstack提供了一种模拟AWS组件的容器化方式,从而消除了在测试过程中连接到实际AWS基础架构的任何需求。 正如我们将在下一节中看到的那样,将这两种技术结合起来使您可以编写功能测试,而无需连接到外部依赖项或更改应用程序代码。
So why use testcontainers? Here’s a rundown of how the library has improved my testing suite:
那么为什么要使用测试容器? 以下是该库如何改进我的测试套件的摘要:
- Improved portability — Mocking your dependencies with testcontainers allows you to run your tests anywhere Docker is installed. It removes the need for username/password credentials and open ports to the external network, and the overall overhead associated with these resources. 改进的可移植性-使用testcontainer模拟依赖关系使您可以在安装Docker的任何地方运行测试。 它消除了对用户名/密码凭据和通往外部网络的开放端口的需要,以及与这些资源相关的总体开销。
- Reduce reliance on Dev region — Because of the improved portability of our tests, tests are able to be run locally and in DevOps pipelines without using a development region. This allows for significant cost savings on AWS resources. 减少对开发区域的依赖-由于测试的可移植性提高,因此可以在本地和DevOps管道中运行测试,而无需使用开发区域。 这样可以节省大量AWS资源。
- Greater knowledge of dependencies — While there is higher initial overhead in mocking your external dependencies, you’re able to get a better understanding of the behaviors of your dependencies as you mock them. 对依赖关系的了解更多—虽然模拟外部依赖关系的初始开销较高,但是在模拟依赖关系时,您可以更好地了解它们的行为。
- Known stateful setup — Utilizing testcontainers allows you to have the same consistent state before every run of your functional test. This reduces the reliance on a proper test state in your real dependencies. 已知的状态设置-利用测试容器,您可以在每次运行功能测试之前具有相同的一致状态。 这样可以减少您实际依赖项中对正确测试状态的依赖。
场景 (The Scenario)
Let’s say we have a Java Spring Boot application that works to process information from a file in S3 and places it in a PostgreSQL database. All told, we have two components that we want to mock in functional testing — the S3 bucket and the PostgreSQL database. From here, the idea is the same no matter what testing framework you’re using, but we’re going to use Cucumber in this example:
假设我们有一个Java Spring Boot应用程序,该应用程序用于处理S3中文件中的信息并将其放置在PostgreSQL数据库中。 总而言之,我们要在功能测试中模拟两个组件-S3存储桶和PostgreSQL数据库。 从这里开始,无论您使用哪种测试框架,想法都是相同的,但是在此示例中,我们将使用Cucumber:
- Setup the containers for the test. 设置测试容器。
- Run the application and its associated tests. 运行该应用程序及其关联的测试。
- Shut down the containers. 关闭容器。
数据库容器设置 (Database Container Setup)
First we instantiate the database container and start it:
首先,我们实例化数据库容器并启动它:
PostgreSQLContainer dbContainer = new PostgreSQLContainer();
dbContainer.start(); // starts Docker container
We then connect to the database and run a startup script, using a connection provided by Testcontainers:
然后,我们使用Testcontainers提供的连接连接到数据库并运行启动脚本:
Connection postgreSQLConnection = dbContainer.createConnection("");
ScriptRunner scriptExecutor = new ScriptRunner(postgreSQLConnection);
Resource startupScript = new ClassPathResource("startupScript.sql");
Reader scriptReader = new BufferedReader(new FileReader(startupScript.getFile()));
scriptExecutor.runScript(scriptReader); // runs startup script
Finally, we change our Spring properties to point our JDBC datasource to the Testcontainers database:
最后,我们更改Spring属性,以将JDBC数据源指向Testcontainers数据库:
System.setProperty("jdbc.datasource.url",
postgreSQLConnection.getMetaData().getURL());
And that’s it! We’ve set up a mock database for our code to seamlessly connect to.
就是这样! 我们为代码无缝连接建立了一个模拟数据库。
S3容器设置 (S3 Container Setup)
Setting up the S3 container is as easy as setting up the database. Through the AWS SDK that we use in our application, we can seamlessly set our code to use a mock S3 bucket. Start the LocalStack container while specifying S3 as the service that we’re using:
设置S3容器就像设置数据库一样容易。 通过在应用程序中使用的AWS开发工具包,我们可以无缝地将代码设置为使用模拟S3存储桶。 在将S3指定为我们正在使用的服务的同时启动LocalStack容器:
LocalStackContainer s3Container = new LocalStackContainer()
.withServices(S3);
s3Container.start();
Now we change our Spring properties to use our mock S3 container. Because our application uses the Java AWS SDK, this means changing the AWS access key, secret key, region, and most importantly the endpoint (without changing the endpoint, the S3 client will still try to connect to the real AWS):
现在,我们更改Spring属性以使用模拟S3容器。 因为我们的应用程序使用Java AWS开发工具包,所以这意味着更改了AWS访问密钥,秘密密钥,区域,最重要的是更改了端点(在不更改端点的情况下,S3客户端仍将尝试连接到真实的 AWS):
System.setProperty("aws.accessKeyId", s3Container.getDefaultCredentialsProvider().getCredentials().getAWSAccessKeyId());
System.setProperty("aws.secretKey", s3Container.getDefaultCredentialsProvider().getCredentials().getAWSSecretKey());
System.setProperty("s3.endpoint", s3Container.getEndpointConfiguration(S3).getServiceEndpoint());
System.setProperty("s3.region", s3Container.getEndpointConfiguration(S3).getSigningRegion());
Finally, we can create an S3 client and a bucket with any files we want inside for our test:
最后,我们可以创建一个S3客户端和一个存储桶,其中包含我们想要用于测试的任何文件:
AmazonS3 s3 = AmazonS3ClientBuilder
.standard()
.withEndpointConfiguration(s3Container.getEndpointConfiguration(S3)).withCredentials(s3Container.getDefaultCredentialsProvider())
.build();
s3.createBucket("testbucket");
s3.putObject("test.txt", "testfolder/test.txt",
new File("src/main/resources/test.csv"));
In the application code we can also set test and non-test configurations based on the Spring profile. This way, our code will grab the mock AWS endpoint only when the test profile is on:
在应用程序代码中,我们还可以基于Spring概要文件设置测试和非测试配置。 这样,仅当测试配置文件处于启用状态时,我们的代码才会获取模拟的AWS终端节点:
@Bean
@Profile("!test")
public AmazonS3 amazonS3() {
return AmazonS3ClientBuilder.standard()
.withRegion(Regions.US_EAST_1)
.withCredentials(new DefaultAWSCredentialsProviderChain())
.build();
}@Bean
@Profile("test")
public AmazonS3 amazonTestS3() {
return AmazonS3ClientBuilder.standard()
.withEndpointConfiguration(new EndpointConfiguration(endpoint, region)) // using testcontainers endpoint
.withCredentials(new DefaultAWSCredentialsProviderChain())
.build();
}
With that, we’re all done with the setup before the test.
这样,我们就可以在测试之前完成所有设置。
Cucumber测试设置 (Cucumber Test Setup)
Now we’re all ready to run our Cucumber test. I’m going to assume that you’re already familiar with Cucumber tests (if not take a look here: https://cucumber.io/docs/guides/10-minute-tutorial/) so we can focus on integrating them with Testcontainers. Simply define a first step to setup the containers as above and run your Spring Boot app:
现在我们已经准备好运行我们的Cucumber测试。 我假设您已经熟悉Cucumber测试(如果不看这里: https : //cucumber.io/docs/guides/10-minute-tutorial/ ),因此我们可以专注于将其与测试容器。 只需定义第一步即可如上所述设置容器并运行您的Spring Boot应用程序:
@Given("^that the application is running$")
public static void containerSetup() {
postgreSQLContainerSetup();
S3ContainerSetup();
SpringApplication.run(TestApp.class, new String[0]);
}//To finish testing, wait for the application to do its thing and run your asserts thereafter:
@And("^wait for the spring boot app process to complete$")
public void wait_for_1_minute() throws Throwable {
TimeUnit.MINUTES.sleep(1);
}@Then("^data is available in database$")
public void test_results_available_in_database() {
assertDataIsPresent();
...
}
And voila! Your testing is done.
瞧! 测试完成。
While the Testcontainers reaper container (called Ryuk) should automatically destroy all of your containers when the tests concludes, it’s best practice to shut them down in a final Cucumber step as well:
测试结束时,尽管Testcontainers收割机容器(称为Ryuk)应自动销毁所有容器,但最佳实践还是在最后一个Cucumber步骤中将其关闭:
@When("^the test is complete$")
public void containerTeardown() {
postgreSQLContainer.stop();
s3Container.stop();
}
结论 (Conclusion)
Testcontainers provides a portable way of doing functional testing without connecting to external services or changing application code. For my own team, this has removed our dependency on an AWS development environment while still being able to test our application code from beginning to end. It’s ensured that we have a consistent state in our dependencies before testing even begins. It’s allowed us to gain a greater working knowledge of the dependencies through the mocks that we build. Finally, bugs have become faster to find and easier to fix within the code, as Testcontainers tests can be run from anywhere that Docker is installed.
Testcontainer提供了一种进行功能测试的便携式方法,而无需连接到外部服务或更改应用程序代码。 对于我自己的团队来说,这消除了我们对AWS开发环境的依赖,同时仍然能够从头到尾测试我们的应用程序代码。 确保甚至在测试开始之前,我们的依赖项中的状态就一致。 它使我们能够通过构建的模拟对依赖项有更深入的了解。 最终,由于可以在安装了Docker的任何地方运行Testcontainers测试,因此错误变得更容易发现并且更易于在代码内修复。
This is just a first simple pass at working with Testcontainers and Spring Boot, as there is so much more that you can mock. Lambdas, Web browsers, Elasticsearch, Kafka, can all be created and torn down within your application testing. Hopefully this tutorial can help you get started!
这只是与Testcontainers和Spring Boot一起使用的第一步,因为您可以模拟的更多东西。 Lambda,Web浏览器,Elasticsearch,Kafka都可以在应用程序测试中创建和拆除。 希望本教程可以帮助您入门!
Originally published at https://www.capitalone.com.
最初在 https://www.capitalone.com上 发布 。
DISCLOSURE STATEMENT: © 2020 Capital One. Opinions are those of the individual author. Unless noted otherwise in this post, Capital One is not affiliated with, nor endorsed by, any of the companies mentioned. All trademarks and other intellectual property used or displayed are property of their respective owners.
披露声明:©2020 Capital One。 观点是个别作者的观点。 除非本文中另有说明,否则Capital One不与任何提及的公司有附属关系或认可。 使用或显示的所有商标和其他知识产权均为其各自所有者的财产。