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 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.
The life cycle of the test is as follows:
SpringRunner
for JUnit 4 and SpringExtension
for JUnit 5) calls Test Context BootstrapperTestContext
- the main class that stores the current state of the test and the application.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 contextsAll the dirty work on test management doesspring-test
, andSpring Boot Test
in turn adds several auxiliary classes, like the already familiar@DataJpaTest
and@SpringBootTest
, useful utilities, likeTestPropertyValues
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.
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.
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:
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
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 { ... }
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.
Source: https://habr.com/ru/post/446184/
All Articles