📜 ⬆️ ⬇️

TDD applications on Spring Boot: fine-tuning tests and working with context

The third article in the cycle and a small branch from the main series - this time I will show how the Spring integration test library works and how it works, what happens when you run the test and how you can finely tune the application and its environment for the test.


Write this article pushed me a comment Hixon10 about how to use a real base, for example Postgres, in the integration test. The comment author suggested using the convenient all-included library embedded-database-spring-test . And I have already added a paragraph and an example of use in the code, but then I thought. Of course, it’s right and good to take a ready-made library, but if the goal is to understand how to write tests for Spring applications, it will be more useful to show how to implement the very same functionality yourself. First of all, this is a great excuse to talk about the fact that Spring Test is under the hood. And secondly, I believe that it is impossible to rely on third-party libraries, if you do not understand how they are arranged inside, this only leads to strengthening the myth of the "magic" of technology.


This time there will be no user feature, but there will be a problem that needs to be solved - I want to run the real database on a random port and connect the application to this time base automatically, and after the tests, stop the base and delete it.


At first, as has already happened, a little theory. People who are not too familiar with the concepts of bin, context, configuration, I recommend refreshing knowledge, for example, in my article The Other Side of Spring / Habr .


Spring test


Spring Test is one of the libraries included in the Spring Framework, in fact, everything that is described in the documentation section about integration testing is about it. The four main tasks that the library solves are:



I highly recommend reading the official documentation, there are many useful and interesting things written there. Here I will give more likely a brief squeeze and a few practical tips that are useful to keep in mind.


Test life cycle



The life cycle of the test is as follows:


  1. An extension to the test framework ( SpringRunner for JUnit 4 and SpringExtension for JUnit 5) calls Test Context Bootstrapper
  2. Boostrapper creates TestContext - the main class that stores the current state of the test and the application.
  3. TestContext configures different hooks (such as starting transactions before a test and rollback after), inject dependencies into test classes (all @Autowired fields on test classes), and creates contexts
  4. The context is created using the Context Loader - it takes the basic configuration of the application and merges it with the test configuration (overlapped properties, profiles, bins, initializers, etc.)
  5. The context is cached using a composite key that completely describes the application — a set of beans, properties, and so on.
  6. Test starts

All the dirty work on test management does spring-test , and Spring Boot Test in turn adds several auxiliary classes, like the already familiar @DataJpaTest and @SpringBootTest , useful utilities, like TestPropertyValues to dynamically change the context properties. It also allows you to run the application as a real web-server, or as a mock environment (without access via HTTP), it is convenient to use the system components using @MockBean , etc.

Context caching


Perhaps one of the very incomprehensible topics in integration testing that raises many questions and misconceptions is the caching of the context (see clause 5 above) between tests and its effect on the speed of test execution. A frequent comment that I hear is that the integration tests "slow" and "run the application for each test." So, they really run - but not for every test. Each context (i.e. application instance) will be reused to the maximum, i.e. If 10 tests use the same application configuration, then the application will run once for all 10 tests. What does the "same configuration" application mean? For Spring Test, this means that the set of bins, configuration classes, profiles, properties, etc. has not changed. In practice, this means that, for example, these two tests will use the same context:


 @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { } 

The number of contexts in the cache is limited to 32 - then, following the LRSU principle, one of them will be removed from the cache.

So what can prevent Spring Test from reusing the cached context and creating a new one?


@DirtiesContext
The simplest option is if the test is marked as annotations, the context will not be cached. This can be useful if the test changes the state of the application and you want to "reset" it.


@MockBean
A very unobvious option, I even rendered it separately - @MockBean replaces the real bin in context for the mock, which can be tested through the Mockito (in the next articles I will show how to use it). The key point is that this annotation changes the set of beans in the application and forces Spring Test to create a new context. If you take the previous example, for example, two contexts will be created here:


 @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class FirstTest { } @SpringBootTest @ActiveProfiles("test") @TestPropertySource("foo=bar") class SecondTest { @MockBean CakeFinder cakeFinderMock; } 

@TestPropertySource
Any change in properties automatically changes the cache key and a new context is created.


@ActiveProfiles
Changing active profiles will also affect the cache.


@ContextConfiguration
Well, of course, any configuration change will also create a new context.


Run the database


So now with all this knowledge we will try. take off understand how and where you can run the database. The only correct answer is not here, it depends on the requirements, but you can think of two options:


  1. Run once before all tests in the class.
  2. Run a random instance and a separate database for each cached context (potentially more than one class).

Depending on the requirements, you can choose any opitsyu. If in my case, Postgres starts relatively quickly and the second option looks appropriate, then the first one may be suitable for something heavier.


The first option is not tied to Spring, but rather to the test framework. For example, you can make your Extension for JUnit 5 .

If you put together all the knowledge about the test library, contexts and caching, then the task is as follows: when creating a new application context, you need to run the database on a random port and transfer the connection data to the context .


The execution of actions with the context before launch in Spring is the responsibility of the ApplicationContextInitializer interface


ApplicationContextInitializer


The interface has only one initialize method that runs before the context “starts” (that is, before calling the refresh method) and allows you to make changes to the context — add beans and properties.


In my case, the class looks like this:


 public class EmbeddedPostgresInitializer implements ApplicationContextInitializer<GenericApplicationContext> { @Override public void initialize(GenericApplicationContext applicationContext) { EmbeddedPostgres postgres = new EmbeddedPostgres(); try { String url = postgres.start(); TestPropertyValues values = TestPropertyValues.of( "spring.test.database.replace=none", "spring.datasource.url=" + url, "spring.datasource.driver-class-name=org.postgresql.Driver", "spring.jpa.hibernate.ddl-auto=create"); values.applyTo(applicationContext); applicationContext.registerBean(EmbeddedPostgres.class, () -> postgres, beanDefinition -> beanDefinition.setDestroyMethodName("stop")); } catch (IOException e) { throw new RuntimeException(e); } } } 

The first thing that happens here is run Postgres starts, from the library yandex-qatools / postgresql-embedded . Then, a set of properties is created — the JDBC URL for the newly launched database, the type of driver, and the behavior of Hibernate for the schema (automatically create). One unobvious thing is only spring.test.database.replace=none - this is what we say to DataJpaTest, that you should not try to connect to the embedded database, such as H2, and you shouldn’t replace the DataSource bean (it works).


And another important point is the application.registerBean(…) . In general, this bin can, of course, not be registered - if nobody uses it in the application, it is not really needed. Registration is needed only to specify the destroy method that Spring will call when the context is destroyed, and in my case this method will call postgres.stop() and stop the base.


In general, that's all, the magic is over, if some was. Now I will register this initializer in a test context:


 @DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) ... 

Or even for convenience, you can create your own annotation, because we all love annotations!


 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @DataJpaTest @ContextConfiguration(initializers = EmbeddedPostgresInitializer.class) public @interface EmbeddedPostgresTest { } 

Now, any test annotated with @EmbeddedPostgrestTest will launch a database on a random port and with a random name, configure Spring to connect to this database, and stop it at the end of the test.


 @EmbeddedPostgresTest class JpaCakeFinderTestWithEmbeddedPostgres { ... } 

Conclusion


I wanted to show that there is no mysterious magic in Spring, there are just a lot of "smart" and flexible internal mechanisms, but knowing them you can get complete control on the tests and the application itself. In general, in combat projects, I do not motivate everyone to write my own methods and classes for setting up an integration environment for tests, if there is a ready-made solution, then we can take it. Although if the whole method is 5 lines of code, then probably dragging the dependency into the project, especially not understanding the implementation, is unnecessary.


Links to other articles in the series



')

Source: https://habr.com/ru/post/446184/


All Articles