Unlike many platforms, Java suffers from a lack of connection stub libraries. If you have been in this world for a long time, you probably should be familiar with WireMock, Betamax or even Spock. Many developers in tests use Mockito to describe the behavior of objects, DataJpaTest with a local h2 database, Cucumber tests. Today you will get acquainted with a lightweight alternative that will help to cope with various problems that you may have encountered using these approaches. In particular, anyStub tries to solve the following problems:
AnyStub wraps function calls, tries to find matching calls that have already been recorded. Two things can happen with this:
Out of the box, anyStub provides wrappers for the http client from Apache HttpClient to create http request stubs and several interfaces from javax.sql. * For DB connections. You are also provided with an API for creating stubs of other connections.
AnyStub is a simple class library and does not require special customization of your environment. This library is aimed at working with spring-boot applications and you will get the maximum benefit by following this path. You can use it outside of Spring, in plain Java applications, but definitely you will have to do some extra work. The further description is focused on testing spring-boot applications.
Let's look at integration testing. This is the most exciting and comprehensive way to test your system. In fact, spring-boot and JUnit do almost everything for you when you write magical annotations:
@RunWith(SpringRunner.class) @SpringBootTest
Currently, integration tests are undervalued and are used limitedly, and some developers avoid them. This is mainly due to time-consuming preparation and maintenance of tests or the need for a special configuration of the environment on the build servers.
With anyStub, you don’t need to disable spring-contex. Instead, keeping the context close to the production configuration is simple and straightforward.
In this example, we will look at how to connect anyStub to Consuming a RESTful Web Service from the Pivotal manual.
Library connection via pom.xml
<dependency> <groupId>org.anystub</groupId> <artifactId>anystub</artifactId> <version>0.2.27</version> <scope>test</scope> </dependency>
The next step is to modify the spring context.
package hello; import org.anystub.http.StubHttpClient; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class TestConfiguration { @Bean public RestTemplateBuilder builder() { RestTemplateCustomizer restTemplateCustomizer = new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { HttpClient real = HttpClientBuilder.create().build(); StubHttpClient stubHttpClient = new StubHttpClient(real); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(stubHttpClient); restTemplate.setRequestFactory(requestFactory); } }; return new RestTemplateBuilder(restTemplateCustomizer); } }
This modification does not change the relationship of components in the application, but only replaces the implementation of a single interface. This leads us to the Barbara Liskov Principle of Substitution . If the design of your application does not violate it, then this substitution does not violate the functionality.
All is ready. This project already includes a test.
@RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Autowired private RestTemplate restTemplate; @Test public void contextLoads() { assertThat(restTemplate).isNotNull(); } }
This test is empty, but it already runs the application context. The most interesting begins here . As we said above, the application context in the test coincides with the working context created by the CommandLineRunner, in which the http request to the external system takes place.
@SpringBootApplication public class Application { private static final Logger log = LoggerFactory.getLogger(Application.class); public static void main(String args[]) { SpringApplication.run(Application.class); } @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } @Bean public CommandLineRunner run(RestTemplate restTemplate) throws Exception { return args -> { Quote quote = restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); log.info(quote.toString()); }; } }
This is sufficient to demonstrate the work of the library. After running the tests for the first time, you will find the new complete/src/test/resources/anystub/stub.yml
.
request0: exception: [] keys: [GET, HTTP/1.1, 'https://gturnquist-quoters.cfapps.io/api/random'] values: [HTTP/1.1, '200', OK, 'Content-Type: application/json;charset=UTF-8', 'Date: Thu, 25 Apr 2019 23:04:49 GMT', 'X-Vcap-Request-Id: 5ffce9f3-d972-4e95-6b5c-f88f9b0ae29b', 'Content-Length: 177', 'Connection: keep-alive', '{"type":"success","value":{"id":3,"quote":"Spring has come quite a ways in addressing developer enjoyment and ease of use since the last time I built an application using it."}}']
What happened? spring-boot built RestTemplateBuilder from the test configuration into the application. This led to the operation of the application through the stub-implementation of the http client. StubHttpClient intercepted the request, did not find the stub file, executed the query, saved the result in a file and returned the result restored from the file.
From now on, you can run this test without an Internet connection and this request will be executed successfully. restTemplate.getForObject()
will return the same result. You can rely on this fact in your future tests.
You can find all the described changes on GitHub .
In fact, we still have not created a single test. Before writing tests, let's see how it works with databases.
In this example, we will add an integration test to Accessing Relational Data using JDBC with Spring from the Pivotal manual.
The test configuration for this case looks like this:
package hello; import org.anystub.jdbc.StubDataSource; import org.h2.jdbcx.JdbcDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class TestConfiguration { @Bean public DataSource dataSource() { JdbcDataSource ds = new JdbcDataSource(); ds.setURL("jdbc:h2:./test"); return new StubDataSource(ds); } }
This creates a regular datasource to the external database and wraps it in a stub implementation — the StubDataSource class. Spring-boot embeds it in context. We also need to create at least one test to run the spring context in the test.
package hello; import org.anystub.AnyStubId; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Test @AnyStubId public void test() { } }
This is again an empty test - its only task is to launch the application context. Here we see a very important annotation @AnystubId
, but it will not be involved yet.
After the first launch, you will find a new src/test/resources/anystub/stub.yml
that includes all database accesses. You will be surprised how spring works behind the scenes with databases. Note that new test launches will not lead to real access to the database. If you delete test.mv.db, it will not appear after the test runs again. A complete set of changes can be viewed on GitHub .
Let's sum up. with anyStub:
Probably, you have questions: how it covers cases when the database does not exist yet, what to do with negative testing and exception handling. We will come back to this, but first we will deal with writing simple tests.
Now we are experimenting with Consuming a RESTful Web Service . This project does not contain components that can be tested. Below are two classes that should depict two layers of some kind of architectural design.
package hello; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class DataProvider { private final RestTemplate restTemplate; public DataProvider(RestTemplate restTemplate) { this.restTemplate = restTemplate; } Quote provideData() { return restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); } }
DataProvider provides access to data in changeable external system.
package hello; import org.springframework.stereotype.Component; @Component public class DataProcessor { private final DataProvider dataProvider; public DataProcessor(DataProvider dataProvider) { this.dataProvider = dataProvider; } int processData() { return dataProvider.provideData().getValue().getQuote().length(); } }
DataProcessor will process the data from the external system.
We intend to test DataProcessor
. It is necessary to test the correctness of the processing algorithm and to protect the system from degradation from future changes.
To achieve these goals, you can think about creating a DataProvider mock object with a dataset and passing it to the DataProcessor constructor in tests. Another way may be to decompose the DataProcessor to highlight the Quote class processing algorithm. Then, such a class is easy to test with the help of unit tests (Surely, this is the recommended method in respected books about clean code). Let's try to avoid changes in the code and invention of test data and just write a test.
@RunWith(SpringRunner.class) @SpringBootTest public class DataProcessorTest { @Autowired private DataProcessor dataProcessor; @Test @AnyStubId(filename = "stub") public void processDataTest() { assertEquals(131, dataProcessor.processData()); } }
Time to talk about the annotation @AnystubId. This annotation helps to manage and control stub files in tests. It can be used with a test class or its method. This annotation sets an individual stub file for the corresponding area. If a region is simultaneously covered by class and method level annotations, the method annotation takes precedence. This annotation has a perameter filename that defines the name of a stub file. The extension ".yml" is added automatically if omitted. By running this test you will not find a new file. The src/test/resources/anystub/stub.yml
has already been created and this test src/test/resources/anystub/stub.yml
it. We got the number 131 from this stub by analyzing the result of the query.
@Test @AnyStubId public void processDataTest2() { assertEquals(131, dataProcessor.processData()); Base base = getStub(); assertEquals(1, base.times("GET")); assertTrue(base.history().findFirst().get().matchEx_to(null, null, ".*gturnquist-quoters.cfapps.io.*")); }
In this test, the @AnyStubId annotation appears without the filename parameter. In this case, the src/test/resources/anystubprocessDataTest2.yml
. The file name is based on the name of the function (class) + ".yml". Once anyStub creates a new file for this test, you must make a real system call. And it is our luck that the new quote has the same length. The last two checks show how to check the behavior of the application. Available to you: the choice of requests for parameters or part of the parameters and counting the number of requests. There are several variations of the times and match functions that can be found in the documentation .
@Test @AnyStubId(requestMode = RequestMode.rmTrack) public void processDataTest3() { assertEquals(79, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); assertEquals(168, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); Base base = getStub(); assertEquals(4, base.times("GET")); }
In this test, @AnyStubId appears with the new requestMode parameter. It allows you to manage permissions for stub files. There are two aspects to control: file search and permission to call an external system.
RequestMode.rmTrack
establishes the following rules: if the file is just created, all requests are sent to the external system and written to the file along with the responses, regardless of whether there is an identical request in the file (duplicates in the file are allowed). If before running the tests, the stub file exists requests to the external system are prohibited. Calls are expected in exactly the same sequence. If the next request does not match the request in the file, an exception is generated.
RequestMode.rmNew
this mode is activated by default. Each request is searched in a stub file. If a matching query is found - the corresponding result is restored from the file, the request to the external system is postponed. If the request is not found, the external system is requested, the result is saved in a file. Duplicate requests in the file - do not occur.
RequestMode.rmNone
Each request is searched in a stub file. If a matching query is found, its result is restored from the file. If the test forms a request that is not in the file, then an exception is generated.
RequestMode.rmAll
before the first request, the stub file is cleared. All requests are written to a file (duplicates in the file are allowed). You can use this mode if you want to watch the connection work.
RequestMode.rmPassThrough
all requests are sent directly to the external system bypassing the implementation stub.
These changes are available on github
We saw how anyStub saves answers. If an exception is generated when accessing the external system, anyStub will save it, and at subsequent requests it will reproduce it.
Often, exceptions are generated by top-level classes, with connection classes getting a valid response (Probably with an error code). In this case, anyStub is responsible for reproducing that answer with the error code, and the classes at the top levels will also generate exceptions for your tests.
Add stub files to the repository.
Do not be afraid to delete and overwrite the stub files.
Manage stub files intelligently. You can reuse one file in several tests or provide an individual file for each test. Take advantage of this opportunity for your needs. But usually using the same file with different access modes is a bad idea.
These are all the main features of anyStub.
Source: https://habr.com/ru/post/450958/
All Articles