1. Overview
1.概述
In this tutorial, we’ll understand the popular software-testing model called the test pyramid.
在本教程中,我们将了解被称为测试金字塔的流行软件测试模型。
We’ll see how it’s relevant in the world of microservices. In the process, we’ll develop a sample application and relevant tests to conform to this model. In addition, we’ll try to understand the benefits and boundaries of using a model.
我们将看到它在微服务的世界中是如何发挥作用的。在这个过程中,我们将开发一个样本应用程序和相关的测试来符合这个模型。此外,我们将尝试了解使用一个模型的好处和界限。
2. Let’s Take a Step Back
2.让我们退一步
Before we start to understand any particular model like the test pyramid, it’s imperative to understand why we even need one.
在我们开始理解任何特定的模型如测试金字塔之前,必须理解为什么我们甚至需要一个模型。
The need to test software is inherent and perhaps as old as the history of software development itself. Software testing has come a long way from manual to automation and further. The objective, however, remains the same — to deliver software conforming to specifications.
测试软件的需求是与生俱来的,也许和软件开发本身的历史一样悠久。软件测试已经走过了漫长的道路,从手动到自动化,甚至更远。然而,目标仍然是相同的 – 交付符合规范的软件。
2.1. Types of Tests
2.1.测试的类型
There are several different types of tests in practice, which focus on specific objectives. Sadly, there is quite a variation in vocabulary and even understanding of these tests.
在实践中,有几种不同类型的测试,它们侧重于特定的目标。可悲的是,这些测试的词汇甚至理解都存在相当大的差异。
Let’s review some of the popular and possibly unambiguous ones:
让我们回顾一下一些流行的、可能是不含糊的。
- Unit Tests: Unit tests are the tests that target small units of code, preferably in isolation. The objective here is to validate the behavior of the smallest testable piece of code without worrying about the rest of the codebase. This automatically implies that any dependency needs to be replaced with either a mock or a stub or such similar construct.
- Integration Tests: While unit tests focus on the internals of a piece of code, the fact remains that a lot of complexity lies outside of it. Units of code need to work together and often with external services like databases, message brokers, or web services. Integration tests are the tests that target the behavior of an application while integrating with external dependencies.
- UI Tests: A software we develop is often consumed through an interface, which consumers can interact with. Quite often, an application has a web interface. However, API interfaces are becoming increasingly popular. UI tests target the behavior of these interfaces, which often are highly interactive in nature. Now, these tests can be conducted in an end-to-end manner, or user interfaces can also be tested in isolation.
2.2. Manual vs. Automated Tests
2.2.手动测试与自动测试
Software testing has been done manually since the beginning of testing, and it’s widely in practice even today. However, it’s not difficult to understand that manual testing has restrictions. For the tests to be useful, they have to be comprehensive and run often.
自测试开始以来,软件测试一直是由人工完成的,即使在今天,它也被广泛用于实践。然而,我们不难理解,手工测试是有限制的。为了使测试有用,它们必须是全面的并且经常运行。
This is even more important in agile development methodologies and cloud-native microservice architecture. However, the need for test automation was realized much earlier.
这在敏捷开发方法和云原生微服务架构中甚至更为重要。然而,对测试自动化的需求在更早的时候就已经意识到了。
If we recall the different types of tests we discussed earlier, their complexity and scope increase as we move from unit tests to integration and UI tests. For the same reason, automation of unit tests is easier and bears most of the benefits as well. As we go further, it becomes increasingly difficult to automate the tests with arguably lesser benefits.
如果我们回顾一下前面讨论的不同类型的测试,当我们从单元测试转向集成和UI测试时,它们的复杂性和范围就会增加。出于同样的原因,单元测试的自动化是比较容易的,而且也承担了大部分的好处。随着我们的进一步发展,自动化测试变得越来越困难,可以说是好处越来越少。
Barring certain aspects, it’s possible to automate testing of most software behavior as of today. However, this must be weighed rationally with the benefits compared to the effort needed to automate.
除去某些方面,到今天为止,大多数软件行为的自动化测试都是可能的。然而,这必须合理地权衡与自动化所需努力相比的好处。
3. What Is a Test Pyramid?
3.什么是测试金字塔?
Now that we’ve gathered enough context around test types and tools, it’s time to understand what exactly a test pyramid is. We’ve seen that there are different types of tests that we should write.
现在我们已经围绕测试类型和工具收集了足够的背景资料,现在是时候了解测试金字塔到底是什么。我们已经看到,有不同类型的测试,我们应该写。
However, how should we decide how many tests should we write for each type? What are the benefits or pitfalls to look out for? These are some of the problems addressed by a test automation model like the test pyramid.
然而,我们应该如何决定为每种类型写多少个测试?需要注意的好处或陷阱是什么?这些都是像测试金字塔这样的测试自动化模型所解决的一些问题。
Mike Cohn came up with a construct called Test Pyramid in his book “Succeeding with Agile”. This presents a visual representation of the number of tests that we should write at different levels of granularity.
Mike Cohn在他的书”Succeeding with Agile“中提出了一个称为测试金字塔的结构。这代表了我们应该在不同层次上编写的测试数量的可视化表示颗粒度。
The idea is that it should be highest at the most granular level and should start decreasing as we broaden our scope of the test. This gives the typical shape of a pyramid, hence the name:
我们的想法是,在最细微的层面上,它应该是最高的,随着我们测试范围的扩大,它应该开始下降。这就形成了典型的金字塔形状,因此得名。
While the concept is pretty simple and elegant, it’s often a challenge to adopt this effectively. It’s important to understand that we must not get fixated with the shape of the model and types of tests it mentions. The key takeaway should be that:
虽然这个概念相当简单和优雅,但要有效地采用这个概念往往是一个挑战。重要的是要明白,我们不能固执于模型的形状和它所提到的测试类型。关键的收获应该是:。
- We must write tests with different levels of granularity
- We must write fewer tests as we get coarser with their scope
4. Test Automation Tools
4.测试自动化工具
There are several tools available in all mainstream programming languages for writing different types of tests. We’ll cover some of the popular choices in the Java world.
在所有的主流编程语言中都有一些工具可以用来编写不同类型的测试。我们将介绍Java世界中的一些流行选择。
4.1. Unit Tests
4.1.单元测试
- Test Framework: The most popular choice here in Java is JUnit, which has a next-generation release known as JUnit5. Other popular choices in this area include TestNG, which offers some differentiated features compared to JUnit5. However, for most applications, both of these are suitable choices.
- Mocking: As we saw earlier, we definitely want to deduct most of the dependencies, if not all, while executing a unit test. For this, we need a mechanism to replace dependencies with a test double like a mock or stub. Mockito is an excellent framework to provision mocks for real objects in Java.
4.2. Integration Tests
4.2.集成测试
- Test Framework: The scope of an integration test is wider than a unit test, but the entry point is often the same code at a higher abstraction. For this reason, the same test frameworks that work for unit testing are suitable for integration testing as well.
- Mocking: The objective of an integration test is to test an application behavior with real integrations. However, we may not want to hit an actual database or message broker for tests. Many databases and similar services offer an embeddable version to write integration tests with.
4.3. UI Tests
4.3.UI测试
- Test Framework: The complexity of UI tests varies depending on the client handling the UI elements of the software. For instance, the behavior of a web page may differ depending upon device, browser, and even operating system. Selenium is a popular choice to emulate browser behavior with a web application. For REST APIs, however, frameworks like REST-assured are the better choices.
- Mocking: User interfaces are becoming more interactive and client-side rendered with JavaScript frameworks like Angular and React. It’s more reasonable to test such UI elements in isolation using a test framework like Jasmine and Mocha. Obviously, we should do this in combination with end-to-end tests.
5. Adopting Principles in Practice
5.在实践中采用原则
Let’s develop a small application to demonstrate the principles we’ve discussed so far. We’ll develop a small microservice and understand how to write tests conforming to a test pyramid.
让我们开发一个小的应用程序来展示我们到目前为止所讨论的原则。我们将开发一个小型的微服务,并了解如何编写符合测试金字塔的测试。
Microservice architecture helps structure an application as a collection of loosely coupled services drawn around domain boundaries. Spring Boot offers an excellent platform to bootstrap a microservice with a user interface and dependencies like databases in almost no time.
微服务架构有助于将应用程序构建为围绕领域边界绘制的松散耦合的服务集合。Spring Boot提供了一个优秀的平台,可以在几乎没有时间的情况下启动一个带有用户界面和数据库等依赖关系的微服务。
We’ll leverage these to demonstrate the practical application of the test pyramid.
我们将利用这些来展示测试金字塔的实际应用。
5.1. Application Architecture
5.1.应用架构
We’ll develop an elementary application that allows us to store and query movies that we’ve watched:
我们将开发一个初级应用程序,使我们能够存储和查询我们所看的电影。
As we can see, it has a simple REST Controller exposing three endpoints:
我们可以看到,它有一个简单的REST控制器,暴露了三个端点。
@RestController
public class MovieController {
@Autowired
private MovieService movieService;
@GetMapping("/movies")
public List<Movie> retrieveAllMovies() {
return movieService.retrieveAllMovies();
}
@GetMapping("/movies/{id}")
public Movie retrieveMovies(@PathVariable Long id) {
return movieService.retrieveMovies(id);
}
@PostMapping("/movies")
public Long createMovie(@RequestBody Movie movie) {
return movieService.createMovie(movie);
}
}
The controller merely routes to appropriate services, apart from handling data marshaling and unmarshaling:
除了处理数据编组和解除编组之外,控制器只是将其路由到适当的服务。
@Service
public class MovieService {
@Autowired
private MovieRepository movieRepository;
public List<Movie> retrieveAllMovies() {
return movieRepository.findAll();
}
public Movie retrieveMovies(@PathVariable Long id) {
Movie movie = movieRepository.findById(id)
.get();
Movie response = new Movie();
response.setTitle(movie.getTitle()
.toLowerCase());
return response;
}
public Long createMovie(@RequestBody Movie movie) {
return movieRepository.save(movie)
.getId();
}
}
Furthermore, we have a JPA Repository that maps to our persistence layer:
此外,我们有一个JPA存储库,映射到我们的持久化层。
@Repository
public interface MovieRepository extends JpaRepository<Movie, Long> {
}
Finally, our simple domain entity to hold and pass movie data:
最后,我们的简单领域实体来持有和传递电影数据。
@Entity
public class Movie {
@Id
private Long id;
private String title;
private String year;
private String rating;
// Standard setters and getters
}
With this simple application, we’re now ready to explore tests with different granularity and quantity.
有了这个简单的应用,我们现在准备探索不同颗粒度和数量的测试。
5.2. Unit Testing
5.2.单元测试
First, we’ll understand how to write a simple unit test for our application. As evident from this application, most of the logic tends to accumulate in the service layer. This mandates that we test this extensively and more often — quite a good fit for unit tests:
首先,我们将了解如何为我们的应用程序编写一个简单的单元测试。从这个应用中可以看出,大部分的逻辑都倾向于积累在服务层。这就要求我们对其进行广泛而频繁的测试–相当适合于单元测试。
public class MovieServiceUnitTests {
@InjectMocks
private MovieService movieService;
@Mock
private MovieRepository movieRepository;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
}
@Test
public void givenMovieServiceWhenQueriedWithAnIdThenGetExpectedMovie() {
Movie movie = new Movie(100L, "Hello World!");
Mockito.when(movieRepository.findById(100L))
.thenReturn(Optional.ofNullable(movie));
Movie result = movieService.retrieveMovies(100L);
Assert.assertEquals(movie.getTitle().toLowerCase(), result.getTitle());
}
}
Here, we’re using JUnit as our test framework and Mockito to mock dependencies. Our service, for some weird requirement, was expected to return movie titles in lower case, and that is what we intend to test here. There can be several such behaviors that we should cover extensively with such unit tests.
在这里,我们用JUnit作为我们的测试框架,用Mockito来模拟依赖关系。我们的服务,由于一些奇怪的要求,被期望返回小写的电影标题,这就是我们打算在这里测试的内容。可能有几个这样的行为,我们应该用这样的单元测试来广泛地覆盖。
5.3. Integration Testing
5.3.集成测试
In our unit tests, we mocked the repository, which was our dependency on the persistence layer. While we’ve thoroughly tested the behavior of the service layer, we still may have issues when it connects to the database. This is where integration tests come into the picture:
在我们的单元测试中,我们模拟了存储库,这是我们对持久化层的依赖。虽然我们已经彻底测试了服务层的行为,但当它连接到数据库时,我们仍然可能有问题。这就是集成测试出现的地方。
@RunWith(SpringRunner.class)
@SpringBootTest
public class MovieControllerIntegrationTests {
@Autowired
private MovieController movieController;
@Test
public void givenMovieControllerWhenQueriedWithAnIdThenGetExpectedMovie() {
Movie movie = new Movie(100L, "Hello World!");
movieController.createMovie(movie);
Movie result = movieController.retrieveMovies(100L);
Assert.assertEquals(movie.getTitle().toLowerCase(), result.getTitle());
}
}
Note a few interesting differences here. Now, we’re not mocking any dependencies. However, we may still need to mock a few dependencies depending upon the situation. Moreover, we’re running these tests with SpringRunner.
注意这里有几个有趣的区别。现在,我们没有模拟任何依赖关系。然而,我们可能仍然需要模拟一些依赖关系,这取决于情况。此外,我们正在用SpringRunner运行这些测试。
That essentially means that we’ll have a Spring application context and live database to run this test with. No wonder, this will run slower! Hence, we much choose fewer scenarios to tests here.
这基本上意味着,我们将有一个Spring应用上下文和实时数据库来运行这个测试。难怪,这将会运行得更慢!因此,我们在这里选择更少的场景进行测试。
5.4. UI Testing
5.4.UI测试
Finally, our application has REST endpoints to consume, which may have their own nuances to test. Since this is the user interface for our application, we’ll focus to cover it in our UI testing. Let’s now use REST-assured to test the application:
最后,我们的应用程序有REST端点需要消费,这可能有他们自己的细微差别需要测试。由于这是我们应用程序的用户界面,我们将在我们的用户界面测试中重点覆盖它。现在让我们使用REST-assured来测试该应用程序。
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MovieApplicationE2eTests {
@Autowired
private MovieController movieController;
@LocalServerPort
private int port;
@Test
public void givenMovieApplicationWhenQueriedWithAnIdThenGetExpectedMovie() {
Movie movie = new Movie(100L, "Hello World!");
movieController.createMovie(movie);
when().get(String.format("http://localhost:%s/movies/100", port))
.then()
.statusCode(is(200))
.body(containsString("Hello World!".toLowerCase()));
}
}
As we can see, these tests are run with a running application and access it through the available endpoints. We focus on testing typical scenarios associated with HTTP, like the response code. These will be the slowest tests to run for obvious reasons.
正如我们所看到的,这些测试是与正在运行的应用程序一起运行的,并通过可用的端点访问它。我们专注于测试与HTTP相关的典型场景,如响应代码。由于明显的原因,这些将是运行速度最慢的测试。
Hence, we must be very particular to choose scenarios to test here. We should only focus on complexities that we’ve not been able to cover in previous, more granular tests.
因此,我们必须非常谨慎地选择这里的测试场景。我们应该只关注我们在以前更细化的测试中无法覆盖的复杂情况。
6. Test Pyramid for Microservices
6.微服务的测试金字塔
Now we’ve seen how to write tests with different granularity and structure them appropriately. However, the key objective is to capture most of the application complexity with more granular and faster tests.
现在我们已经看到了如何编写不同颗粒度的测试,并适当地构造它们。然而,关键的目标是用更细化和更快速的测试来捕捉大部分的应用程序的复杂性。
While addressing this in a monolithic application gives us the desired pyramid structure, this may not be necessary for other architectures.
虽然在单片机应用中解决这个问题给我们带来了理想的金字塔结构,但对于其他架构来说,这可能不是必要的。
As we know, microservice architecture takes an application and gives us a set of loosely coupled applications. In doing so, it externalizes some of the complexities that were inherent to the application.
正如我们所知,微服务架构将一个应用程序,给我们一组松散耦合的应用程序。在这样做的过程中,它将应用中固有的一些复杂性外部化。
Now, these complexities manifest in the communication between services. It’s not always possible to capture them through unit tests, and we have to write more integration tests.
现在,这些复杂性表现在服务之间的通信上。通过单元测试来捕捉它们并不总是可能的,我们必须要写更多的集成测试。
While this may mean that we deviate from the classical pyramid model, it does not mean we deviate from principle as well. Remember, we’re still capturing most of the complexities with as granular tests as possible. As long as we’re clear on that, a model that may not match a perfect pyramid will still be valuable.
虽然这可能意味着我们偏离了经典的金字塔模型,但这并不意味着我们也偏离了原则。记住,我们仍然在用尽可能细化的测试来捕捉大部分的复杂性。只要我们清楚这一点,一个可能不符合完美金字塔的模型仍将是有价值的。
The important thing to understand here is that a model is only useful if it delivers value. Often, the value is subject to context, which in this case is the architecture we choose for our application. Therefore, while it’s helpful to use a model as a guideline, we should focus on the underlying principles and finally choose what makes sense in our architecture context.
这里需要理解的重要一点是,一个模型只有在提供价值时才是有用的。通常情况下,价值受制于背景,在这种情况下,就是我们为我们的应用选择的架构。因此,虽然使用模型作为指导原则很有帮助,但我们应该关注基本原则,并最终选择在我们的架构背景下有意义的东西。
7. Integration with CI
7.与CI的整合
The power and benefit of automated tests are largely realized when we integrate them into the continuous integration pipeline. Jenkins is a popular choice to define build and deployment pipelines declaratively.
当我们将自动化测试集成到持续集成管道中时,自动化测试的威力和好处就基本实现了。Jenkins是以声明方式定义构建和部署管道的流行选择。
We can integrate any tests which we’ve automated in the Jenkins pipeline. However, we must understand that this increases the time for the pipeline to execute. One of the primary objectives of continuous integration is fast feedback. This may conflict if we start adding tests that make it slower.
我们可以在Jenkins管道中集成任何我们已经自动化的测试。然而,我们必须明白,这增加了流水线的执行时间。持续集成的主要目标之一是快速反馈。如果我们开始添加测试,使其变得更慢,这可能会发生冲突。
The key takeaway should be to add tests that are fast, like unit tests, to the pipeline that is expected to run more frequently. For instance, we may not benefit from adding UI tests into the pipeline that triggers on every commit. But, this is just a guideline and, finally, it depends on the type and complexity of the application we’re dealing with.
关键的启示应该是:将快速的测试,如单元测试,添加到预计更频繁运行的管道中。例如,我们可能不会从在每次提交时触发的管道中添加UI测试中受益。但是,这只是一个指导原则,最后,这取决于我们所处理的应用程序的类型和复杂性。
8. Conclusion
8.结语
In this article, we went through the basics of software testing. We understood different test types and the importance of automating them using one of the available tools.
在这篇文章中,我们经历了软件测试的基础知识。我们了解了不同的测试类型,以及使用现有工具之一将其自动化的重要性。
Furthermore, we understood what a test pyramid means. We implemented this using a microservice built using Spring Boot.
此外,我们了解了测试金字塔的含义。我们使用Spring Boot构建的微服务来实现这一点。
Finally, we went through the relevance of the test pyramid, especially in the context of architecture like microservices.
最后,我们讨论了测试金字塔的相关性,特别是在微服务这样的架构背景下。