949 lines
35 KiB
Plaintext
949 lines
35 KiB
Plaintext
image:https://api.travis-ci.org/spring-projects/spring-test-data-geode.svg?branch=master["Build Status", link="https://travis-ci.org/spring-projects/spring-test-data-geode"]
|
|
|
|
[[about]]
|
|
== Spring Test Framework for Apache Geode & VMware Tanzu GemFire
|
|
|
|
The STDG project is a _Spring Data_ module, building on the core _Spring Framework's_ `TestContext`, used to write
|
|
_Unit_ and _Integration Tests_ when building _Spring Data_ for https://geode.apache.org/[Apache Geode]
|
|
& https://pivotal.io/pivotal-gemfire[VMware GemFire] (SDG) applications.
|
|
|
|
This project was born from https://spring.io/projects/spring-data-gemfire[_Spring Data for VMware GemFire's_]
|
|
(https://github.com/spring-projects/spring-data-gemfire[@GitHub])
|
|
https://github.com/spring-projects/spring-data-gemfire/tree/2.1.19.RELEASE/src/test/java/org/springframework/data/gemfire/test[test framework].
|
|
This _test framework_ is used in SDG's test suite to test the proper function and behavior of Apache Geode
|
|
& VMware GemFire in a _Spring_ context.
|
|
|
|
For several years now, users have asked for a better way to test their Apache Geode & VMware GemFire based,
|
|
_Spring_ applications reliably and easily, particularly when writing _Unit_ and _Integrations_ tests.
|
|
|
|
Additionally, STDG was created to consolidate the testing efforts, lessons learned, and knowledge and experience of
|
|
effectively testing all Spring for Apache Geode/VMware GemFire projects: _Spring Boot for Apache Geode & VMware GemFire_
|
|
(SBDG) and _Spring Session for Apache Geode & VMware GemFire_ (SSDG) in addition to
|
|
_Spring Data for Apache Geode & VMware GemFire_ (SDG).
|
|
|
|
Eventually, STDG will replace the SDG test classes so that tests and testing efforts are consistent across all Spring
|
|
projects for Apache Geode/VMware GemFire: SDG, SSDG and SBDG.
|
|
|
|
This (relatively) **new** project is still under development and will have documentation, examples and an extensive test
|
|
suite once completed.
|
|
|
|
In the meantime, you can review the
|
|
https://github.com/spring-projects/spring-boot-data-geode/tree/master/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure[test suite for SBDG]
|
|
and the https://github.com/spring-projects/spring-session-data-geode/tree/master/spring-session-data-geode/src/test/java/org/springframework/session/data/gemfire[test suite for SSDG]
|
|
to get a sense of how this project is used and works.
|
|
|
|
[[code-of-conduct]]
|
|
== Code of Conduct
|
|
|
|
Please see our https://github.com/spring-projects/.github/blob/master/CODE_OF_CONDUCT.md[code of conduct]
|
|
|
|
[[report-security-vulnerability]]
|
|
== Reporting Security Vulnerabilities
|
|
|
|
Please see our https://github.com/spring-projects/spring-test-data-geode/security/policy[Security policy].
|
|
|
|
[[license]]
|
|
== License
|
|
|
|
_Spring Test for Apache Geode_ and _Spring Test for VMware GemFire_ is Open Source Software
|
|
released under the https://www.apache.org/licenses/LICENSE-2.0.html[Apache 2.0 license].
|
|
|
|
|
|
[[nutshell]]
|
|
== STDG in a Nutshell
|
|
|
|
Until proper documentation has been provided, this very short and simple tutorial will hopefully give you a better idea
|
|
of how this project is used.
|
|
|
|
|
|
[[unit-tests]]
|
|
=== Unit Testing with STDG
|
|
|
|
We all write tests, right? TDD style? ;-)
|
|
|
|
As we begin to write tests, we typically start with _Unit Tests_ since they are designed to test the subject
|
|
in isolation without actual collaborators and dependencies in order to gather feedback quickly,
|
|
i.e. "_Is my logic correct?_"
|
|
|
|
It common when writing _Unit Tests_ to *mock* the collaborators/dependencies since the test should assume that the
|
|
dependencies "_work as designed_". During _Unit Testing_, it does not matter whether or not the dependencies actually
|
|
work as expected (that is the purpose of _Integration Tests_ or the _Unit Tests_ for the dependencies themselves), just
|
|
that they have a contract and our application components, the "_Subject Under Test_" (SUT), honors that contract and
|
|
uses the external dependencies correctly. Essentially we are asserting that the interactions between our application
|
|
components and external collaborators/dependencies are correct and the results lead to the desired outcome.
|
|
|
|
Well, it is, or should be, no different when you are using Apache Geode or VMware GemFire.
|
|
|
|
For instance, you might want to mock that your _Data Access Object_ (DAO) performs the proper interactions on
|
|
a GemFire/Geode Region, performing the right CRUD operations, making sure the right (OQL) Queries are executed
|
|
for the Use Case or business function and workflow being performed by the application.
|
|
|
|
In this case, we don't care whether the Region is real or not, or that an OQL Query is actually well formed and would
|
|
execute properly, performantly, returning the correct results. We would "mock" the Regions' behavior in this case
|
|
to make sure that our DAO interactions with the Region are correct, that it handles the translation of Exceptions
|
|
or other Error conditions, that it transforms values to/from the backend data store (i.e. Region), and so on. That is
|
|
how you properly test the subject.
|
|
|
|
To support _Unit Testing_ with Apache Geode or VMware GemFire in a Spring context, STDG provides the
|
|
`@EnableGemFireMockObjects` annotation. If you want to use GemFire/Geode Mock Objects, e.g. a "mock" Region rather
|
|
than a "live" Region, than you simply only need to annotate your test configuration with `@EnableGemFireMockObjects`.
|
|
|
|
For example:
|
|
|
|
.Unit Test with GemFire/Geode Mock Objects
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class ExampleUnitTestClass {
|
|
|
|
// test case methods here
|
|
|
|
@EnableGemFireMockObjects
|
|
@ClientCacheApplication
|
|
@EnableEntityDefinedRegions(basePackageClasses=ExampleEntity.class,
|
|
clientRegionShortcut = ClientRegionShortcut.LOCAL)
|
|
static class TestConfiguration { }
|
|
|
|
}
|
|
----
|
|
|
|
|
|
In the example above, the `@EnableGemFireMockObjects` annotation creates "mocks" for the `ClientCache`, all the `Regions`
|
|
identified and created by the `@EnableEntityDefinedRegions(..)` annotation, along with all the other GemFire/Geode
|
|
object types. There are no "live" GemFire/Geode objects when "mocking" is enabled.
|
|
|
|
Here is 1
|
|
https://github.com/spring-projects/spring-test-data-geode/blob/master/spring-data-geode-test/src/test/java/org/springframework/data/gemfire/MockClientCacheApplicationIntegrationTests.java[example]
|
|
of a concrete _Unit Test_ in action, using STDG's `@EnableGemFireMockObjects` annotation.
|
|
|
|
It really is that simple!
|
|
|
|
TIP: Mocking GemFire/Geode objects outside a Spring context is possible, but beyond the scope of this simple tutorial
|
|
for the time being.
|
|
|
|
[[unit-tests-mock-object-cleanup]]
|
|
==== Mock Object Scope & Lifecycle Management
|
|
|
|
Currently, GemFire/Geode mock objects are cleaned up after an individual test class runs. Therefore, the mocked
|
|
GemFire/Geode objects persist for the entire lifecycle of a single test class, or test suite, and can be reused
|
|
across all the test cases of the test class.
|
|
|
|
The Spring `TestContext` framework emits certain "test" events during the test lifecycle as documented in
|
|
the `EventPublishingTestExecutionListener` class https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/event/EventPublishingTestExecutionListener.html[_Javadoc_].
|
|
The test events are actually contained in the https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/event/package-summary.html[`org.springframework.test.context.event`] package.
|
|
|
|
Currently, STDG defaults the cleanup of all mocked GemFire/Geode objects to the
|
|
https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/event/AfterTestClassEvent.html[`AfterTestClassEvent`] type.
|
|
|
|
By way of example, this would be equivalent to:
|
|
|
|
.Default STDG GemFire/Geode mock object cleanup
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class ExampleTest {
|
|
|
|
@ClientCacheApplication
|
|
@EnableGemFireMockObject(destroyOnEvents = AfterTestClassEvent.class)
|
|
static class TestConfiguration { }
|
|
|
|
}
|
|
----
|
|
|
|
You might want to cleanup all GemFire/Geode mock objects after each test case method in your test class using
|
|
the https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/event/AfterTestMethodEvent.html[`AfterTestMethodEvent`] class.
|
|
|
|
In this case, you can do:
|
|
|
|
.GemFire/Geode mock object cleanup after each test case
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class ExampleTest {
|
|
|
|
@ClientCacheApplication
|
|
@EnableGemFireMockObject(destroyOnEvents = AfterTestMethodEvent.class)
|
|
static class TestConfiguration { }
|
|
|
|
}
|
|
----
|
|
|
|
The `destroyOnEvents` attribute of the `@EnableGemFireMockObjects` annotation accepts more than one test event type,
|
|
thereby allowing to perform GemFire/Geode mock object cleanup at multiple points in the test lifecycle.
|
|
|
|
For example, maybe you need to cleanup all mocked GemFire/Geode objects before each test case executes and after each
|
|
test class completes:
|
|
|
|
.GemFire/Geode mock object cleanup before each test case executes and after each test class completes
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class ExampleTest {
|
|
|
|
@ClientCacheApplication
|
|
@EnableGemFireMockObject(destroyOnEvents = { BeforeTestExecutionEvent.class, AfterTestClassEvent.class })
|
|
static class TestConfiguration { }
|
|
|
|
}
|
|
----
|
|
|
|
You now have the granularity required to control the scope and lifecycle of the GemFire/Geode mocked objects in STDG.
|
|
|
|
|
|
[[unit-tests-mock-region-data]]
|
|
==== Mock Regions with Data
|
|
|
|
While implementing a fully capable GemFire/Geode Region would defeat the purpose of Mocking and Unit Testing in general,
|
|
it is desirable to sometimes perform basic Region data access operations, such as `get` and `put`, with small quantities
|
|
of data and emulate, or simulate the same effects.
|
|
|
|
As such, with STDG, it is currently possible to perform the following Region data access operations:
|
|
|
|
* `clear()`
|
|
* `containsKey(key)`
|
|
* `containsValue(value)`
|
|
* `containsValueForKey(value)`
|
|
* `forEach(:BiConsumer<K, V>)`
|
|
* `get(key)`
|
|
* `getAll()`
|
|
* `getEntry(key)`
|
|
* `getOrDefault(key, defaultValue)`
|
|
* `invalidate(key)`
|
|
* `isEmpty()`
|
|
* `keySet()`
|
|
* `localClear()`
|
|
* `localValidate()`
|
|
* `put(key, value)`
|
|
* `putAll(:Map<K, V>)`
|
|
* `remove(key)`
|
|
* `removeAll(:Collection<K>)`
|
|
* `size()`
|
|
* `values()`
|
|
|
|
NOTE: Some mock Map/Region data access operations are still being considered, such as: `putIfAbsent(key, value)`,
|
|
`remove(key, value)`, `replace(key, value)`, `replace(key, oldValue, newValue)` and `replaceAll(:BiFunction<K, V>)`.
|
|
Other mock Region data access operations will not be implemented at all (e.g. `keySetOnServer()` or `sizeOnServer()`,
|
|
etc) since they necessarily involve a more complex topology. Regardless, you can still mock any Map/Region operation
|
|
you like by following these <<unit-tests-mock-unsupported-region-ops,instructions>>.
|
|
|
|
WARNING: Some mock Map/Region data access operations are implemented in terms of other Map/Region operations
|
|
(e.g. `putAll(:Map<K, V))` is implemented in terms of `put(key, value)`) and are therefore compound actions
|
|
that are not atomic. In other words, we did not make the atomic.
|
|
|
|
The "mock" Region will behave and function similarly to an actual GemFire/Geode Region involving these
|
|
data access operations.
|
|
|
|
By way of example, this means you can do things like the following in a Unit Test with a "mock" Region:
|
|
|
|
.Basic data access operations on a mocked Region
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class MyGeodeMockRegionUnitTests {
|
|
|
|
@Resource(name = "Example")
|
|
private Region<?, ?> mockRegion;
|
|
|
|
@Test
|
|
public void simpleGetAndPutRegionOpsWork() {
|
|
|
|
mockRegion.put(1, "test");
|
|
|
|
assertThat(mockRegion).containsKey(1);
|
|
assertThat(mockRegion.get(1)).isEqualTo("test");
|
|
}
|
|
|
|
@ClientCacheApplication
|
|
@EnableGemFireMockObjects
|
|
static class TestConfiguration {
|
|
|
|
@Bean("Example")
|
|
ClienRegionFactoryBean mockRegion(GemFireCache gemfireCache) {
|
|
|
|
ClientRegionFactoryBean mockRegion = new ClientRegionFactoryBean();
|
|
|
|
mockRegion.setCache(gemfireCache);
|
|
|
|
return mockRegion;
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
Of course, you can also perform similar Region data access operations using the _Spring Data Repository_ abstraction
|
|
instead. The benefit of _Spring Data's_ _Repository_ abstraction is that it shields your application from Apache Geode
|
|
and hides the fact that you are interfacing with an Region under-the-hood by using the proper _Data Access Object_ (DAO)
|
|
pattern.
|
|
|
|
For example, you can "mock" a Region and `put`/`get` data using a _Spring Data Repository_ for the Region
|
|
as demonstrated in the following code.
|
|
|
|
Given a `Customer` application domain object annotated with the `@Region` mapping annotation:
|
|
|
|
.Customer
|
|
[source,java]
|
|
----
|
|
@Region("Customers")
|
|
class Customer {
|
|
|
|
@Id
|
|
private Long id;
|
|
|
|
// ...
|
|
|
|
}
|
|
----
|
|
|
|
Along with a SD _Repository_ for `Customers`:
|
|
|
|
.CustomerRepository
|
|
[source,java]
|
|
----
|
|
interface CustomerRepository extends CrudRepository<Customer, Long> {
|
|
//...
|
|
}
|
|
----
|
|
|
|
Then you can write a test class like the following, still using a "mock" Region to `put` and `get` actual data:
|
|
|
|
.Spring Data _Repository_ on a mocked Region
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class MySpringDataRepositoryUnitTests {
|
|
|
|
@Autowired
|
|
private CustomerRepository customerRepository;
|
|
|
|
@Test
|
|
public void simpleRepositoryCrudOpsWork() {
|
|
|
|
Customer jonDoe = new Customer(1L, "Jon Doe");
|
|
|
|
customerRepository.save(jonDoe);
|
|
|
|
assertThat(customerRepository.existsById(jonDoe.getId())).isTrue();
|
|
assertThat(customerRepository.findById(jonDoe.getId()).orElse(null)).isEqualTo(jonDoe);
|
|
}
|
|
|
|
@ClientCacheApplication
|
|
@EnableEntityDefinedRegions(basePackageClasses = Customer.class)
|
|
@EnableGemfireRepositories(basePackageClasses = CustomerRepository.class)
|
|
static class TestConfiguration { }
|
|
|
|
}
|
|
----
|
|
|
|
Even though you are using _Spring Data Repositories_ and the `@EnableEntityDefinedRegions` annotation (perhaps;
|
|
yes these components still work with Mocks and mock data), you can still autowire (inject) the Region and access
|
|
it directly in the same test class:
|
|
|
|
.Accessing the mock Region directly in the SD _Repository_ test
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class MySpringDataRepositoryWithMockRegionUnitTests {
|
|
|
|
@Autowired
|
|
private CustomerRepository customerRepository;
|
|
|
|
@Resource(name = "Customers")
|
|
private Region<Long, Customer> customers;
|
|
|
|
@Test
|
|
public void simpleRepositoryCrudOpsWork() {
|
|
//...
|
|
}
|
|
|
|
@Test
|
|
public void customerRegionOpsWorkToo() {
|
|
|
|
Customer janeDoe = new Customer(2L, "Jane Doe");
|
|
|
|
customers.put(janeDoe.getId(), janeDoe);
|
|
|
|
assertThat(customers).containsKey(janeDoe.getId());
|
|
assertThat(customers.get(janeDoe.getId())).isEqualTo(janeDoe);
|
|
assertThat(customerRepository.findById(janeDoe.getId()).orElse(null)).isEqualTo(janeDoe);
|
|
}
|
|
}
|
|
----
|
|
|
|
While you are allowed to inject a Region directly into your test class, it is better to use SDG's `GemfireTemplate`,
|
|
which wraps and decorates a Region's data access operations. `GemfireTemplate` provides a lower-level API, closer
|
|
to the Region API, than _Spring Data Repositories_ allowing you to perform and exercise more control over advanced
|
|
functions, while still shielding you from the Region API.
|
|
|
|
The test class above could be rewritten as:
|
|
|
|
.Accessing the mock Region using the SDG `GemfireTemplate` in the SD _Repository_ test
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class MySpringDataRepositoryWithGemfireTemplateUnitTests {
|
|
|
|
@Autowired
|
|
private CustomerRepository customerRepository;
|
|
|
|
@Autowired
|
|
@Qualifier("customersTemplate")
|
|
private GemfireTemplate customersTemplate;
|
|
|
|
@Test
|
|
public void simpleRepositoryCrudOpsWork() {
|
|
//...
|
|
}
|
|
|
|
@Test
|
|
public void customerTemplateOpsWorkToo() {
|
|
|
|
Customer janeDoe = new Customer(2L, "Jane Doe");
|
|
|
|
customersTemplate.put(janeDoe.getId(), janeDoe);
|
|
|
|
assertThat(customersTemplate).containsKey(janeDoe.getId());
|
|
assertThat(customersTemplate.get(janeDoe.getId())).isEqualTo(janeDoe);
|
|
assertThat(customerRepository.findById(janeDoe.getId()).orElse(null)).isEqualTo(janeDoe);
|
|
}
|
|
}
|
|
----
|
|
|
|
For clarification, obviously many of the Region functions and behaviors are not implemented, like persistence
|
|
and overflow to disk, distribution, replication, eviction, expiration, querying, etc. If you find you need to test
|
|
your application with these behaviors and functions, then your test would clearly be better suited as an actual
|
|
Integration Test.
|
|
|
|
[[unit-tests-mock-region-callbacks]]
|
|
==== Mock Region Callbacks
|
|
|
|
A relatively *new* feature in STDG is the ability to register and invoke cache (Region) callbacks, such as
|
|
`CacheListeners`, or a `CacheLoader` or a `CacheWriter`.
|
|
|
|
Cache callbacks like `CacheListeners` or `CacheLoader/Writers` are user-defined, application objects that can be
|
|
registered with a Region to listen for events, load data on cache misses, or write the Region's data to a backend,
|
|
external data source.
|
|
|
|
It is sometimes useful when testing to partially mock some dependencies (a.k.a. collaborators; e.g. Regions)
|
|
while using live objects for others (e.g. cache callbacks like a `CacheListener`).
|
|
|
|
The reason behind this testing strategy is that some objects are mostly infrastructure related (e.g. a Region),
|
|
and not the primary focus of the test, while other objects are still very much tied to the application's function
|
|
and behavior (e.g. a `CacheListener` or a `CacheLoader`), i.e. they are part of the application's workflow.
|
|
|
|
As such, STDG not only allows you to register `CacheListeners` and `CacheLoaders/Writers` (you could do so before
|
|
as well), but will now additionally invoke the Listeners, Loader and Writer at the appropriate point in the Region
|
|
operation's process flow.
|
|
|
|
For example, a registered `CacheWriter` is invoked before the object (value) is put into the Region using the
|
|
`Region.put(key, value)` operation. This is exactly what GemFire/Geode does in order to ensure consistency with
|
|
the backend, external data source. If the `CacheWriter` throws an exception during 1 of it's event handler callbacks
|
|
(e.g. `beforeCreate(:EntryEvent<K, V>)` then it will prevent the object from being inserted into the Region.
|
|
The same behavior is true for a STDG mock Region.
|
|
|
|
By way of example, let's demonstrate with a `CacheLoader`:
|
|
|
|
.Application `CacheLoader` on mock Region
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class MyMockRegionWithCacheLoaderUnitTests {
|
|
|
|
@Resource(name = "Example")
|
|
private Region example;
|
|
|
|
@Test
|
|
public void cacheLoaderWorks() {
|
|
|
|
assertThat(example.get("one")).isEqualTo(1);
|
|
assertThat(example.get("two")).isEqualTo(2);
|
|
// ...
|
|
|
|
}
|
|
|
|
@ClientCacheApplication
|
|
@EnableGemFireMockObjects
|
|
static class TestConfiguration {
|
|
|
|
@Bean
|
|
ClienRegionFactoryBean exampleRegion(GemFireCache gemfireCache) {
|
|
|
|
ClientRegionFactoryBean exampleRegion = new ClientRegionFactoryBean();
|
|
|
|
exampleRegion.setCache(gemfireCache);
|
|
exampleRegion.setCacheLoader(counterCacheLoader());
|
|
|
|
return exampleRegion;
|
|
}
|
|
}
|
|
|
|
@Bean
|
|
CacheLoader<Object, Object> counterCacheLoader() {
|
|
|
|
AtomicInteger counter = new AtomicInteger(0);
|
|
|
|
return new CacheLoader<>() {
|
|
|
|
@Override
|
|
public Object load(LoaderHelper<Object, Object> helper) {
|
|
return counter.incrementAndGet();
|
|
}
|
|
};
|
|
}
|
|
}
|
|
----
|
|
|
|
As seen in the test above, performing a `Region.get(key)` for keys "one" and "two" on an initially empty Region
|
|
will result in cache misses, which will then invoke the registered, application "counter" `CacheLoader` to supply
|
|
the value for the requested keys.
|
|
|
|
You can register a `CacheWriter` along with 1 or more `CacheListeners` and they will be invoked, too.
|
|
|
|
[[unit-tests-mock-unsupported-region-ops]]
|
|
==== Mocking Unsupported Region Operations
|
|
|
|
As stated in the <<unit-tests-mock-region-data>> section above, only the following Region data access operations are
|
|
supported by STDG out-of-the-box (OOTB):
|
|
|
|
* `clear()`
|
|
* `containsKey(key)`
|
|
* `containsValue(value)`
|
|
* `containsValueForKey(value)`
|
|
* `forEach(:BiConsumer<K, V>)`
|
|
* `get(key)`
|
|
* `getAll()`
|
|
* `getEntry(key)`
|
|
* `getOrDefault(key, defaultValue)`
|
|
* `invalidate(key)`
|
|
* `isEmpty()`
|
|
* `keySet()`
|
|
* `localClear()`
|
|
* `localValidate()`
|
|
* `put(key, value)`
|
|
* `putAll(:Map<K, V>)`
|
|
* `remove(key)`
|
|
* `removeAll(:Collection<K>)`
|
|
* `size()`
|
|
* `values()`
|
|
|
|
How then do you mock other Region operations (e.g. `putIfAbsent(key, value)`) provided by the Region API that is not
|
|
supported by STDG OOTB?
|
|
|
|
Fortunately, you can rely on the fact that the Region object returned when mocking with `@EnableGemFireMockObjects`
|
|
inside your _Unit Tests_ is a "_mock_" object, specifically mocked by _Mockito_. Therefore, you are able to mock
|
|
any other Region data access operations that might be required by your application given a reference to the "mock"
|
|
Region object.
|
|
|
|
For example, suppose you also want to mock the `putIfAbsent(key, value)` _Map_ operation on Region, then you can do:
|
|
|
|
.Mocking Region.putIfAbsent(key, value)
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class ExampleUnitTest {
|
|
|
|
@Autowired
|
|
@Qualifer("exampleTemplate")
|
|
GemfireTemplate exampleTemplate;
|
|
|
|
@Resource(name = "Example")
|
|
Region<?, ?> example;
|
|
|
|
@Before
|
|
public void setup() {
|
|
|
|
doAnswer(invocation -> {
|
|
|
|
Object key = invocation.getArgugment(0);
|
|
Object value = invocation.getArgument(1);
|
|
Object existingValue;
|
|
|
|
synchronized (this.example) {
|
|
|
|
existingValue = this.example.get(key);
|
|
|
|
if (existingValue == null) {
|
|
this.example.put(key, value);
|
|
}
|
|
}
|
|
|
|
return existingValue;
|
|
|
|
}).when(this.example).putIfAbsent(any(), any());
|
|
}
|
|
|
|
@Test
|
|
public void putIfAbsentWorks() {
|
|
|
|
assertThat(this.exampleTemplate.putIfAbsent(1, "test")).isNull();
|
|
assertThat(this.exampleTemplate.putIfAbsent(1, "mock")).isEqualTo("test");
|
|
assertThat(this.exampleTemplate.get(1)).isEqualTo("test");
|
|
}
|
|
|
|
@ClientCacheApplication
|
|
@EnableGemFireMockObjects
|
|
static class TestConfiguration {
|
|
|
|
@Bean("Example")
|
|
ClienRegionFactoryBean mockRegion(GemFireCache gemfireCache) {
|
|
|
|
ClientRegionFactoryBean mockRegion = new ClientRegionFactoryBean();
|
|
|
|
mockRegion.setCache(gemfireCache);
|
|
|
|
return mockRegion;
|
|
}
|
|
|
|
@Bean
|
|
GemfireTemplate exampleTemplate(GemFireCache gemfireCache) {
|
|
return new GemfireTemplate(gemifreCache.getRegion("/Example"));
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
While the `putIfAbsent(key, value)` operation above was mocked (implemented) in terms of the existing, mocked `get(key)`
|
|
and `put(key, value)` Region operations, you could very well have implemented/mocked `putIfAbsent(key, value)` however
|
|
you wanted. The Region object is a "_mock_" object after all.
|
|
|
|
Not only can you mock unsupported Region methods, you can also redefine the mocked behavior of a STDG supported
|
|
and mocked Region method, like `get(key)` or `put(key, value)` as well.
|
|
|
|
This capability applies to any GemFire/Geode mocked object. The choice is up to you what a GemFire/Geode mock object
|
|
does or does not do.
|
|
|
|
[[integration-testing]]
|
|
=== Integration Testing with STDG
|
|
|
|
You should write many more _Unit Tests_ than _Integration Tests_ to get reliable and fast feedback. This is a
|
|
no brainer and software development 101.
|
|
|
|
However, _Unit Tests_ do not completely take the place of _Integration Tests_, either. Both are necessary, as are
|
|
perhaps other forms of testing (e.g. Functional Testing, Acceptance Testing, Smoke Testing, Performance Testing,
|
|
Concurrency Testing, etc).
|
|
|
|
For instance, you should verify that the (OQL) Query you just constructed, maybe even generated, is well-formed
|
|
and yields the desired results, is performant, and all that jazz. You can only reliably do that by executing
|
|
the (OQL) Query against an actual GemFire/Geode Region with a properly constructed and deliberate data set.
|
|
|
|
This sort _Integration Test_ does not have a complex arrangement, and can be performed simply by removing
|
|
or disabling the `@EnableGemFireMockObjects` annotation in our previous example above.
|
|
|
|
However, other forms of _Integration Testing_ might require a more complex arrangement,
|
|
such as client/server integration tests.
|
|
|
|
For instance, you may want to test that a client receives all the events from the server to which it has explicitly
|
|
registered interests. For this type of test, you need to have a (1 or more) GemFire/Geode server(s) running,
|
|
and perhaps even a few clients.
|
|
|
|
Ideally, you want to fork a GemFire/Geode server JVM process in the _Integration Test_ class requiring
|
|
a server instance.
|
|
|
|
Once again, STDG comes to the rescue.
|
|
|
|
For example:
|
|
|
|
.Client/Server Integration Test
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration(classes = GeodeClientTestConfiguration.class)
|
|
class ExampleIntegrationTestClass extends ForkingClientServerIntegrationTestsSupport {
|
|
|
|
@BeforeClass
|
|
public static void startGemFireServer() {
|
|
startGemFireSever(GeodeServerTestConfiguration.class);
|
|
}
|
|
|
|
// test case method here
|
|
|
|
@CacheServerApplication
|
|
@EnableEntityDefinedRegions
|
|
static class GeodeServerTestConfiguration {
|
|
|
|
public static void main(String[] args) {
|
|
|
|
AnnotationConfigApplicationContext applicationContext =
|
|
new AnnotationConfigApplicationContext(GeodeServerTestConfiguration.class);
|
|
|
|
applicationContext.registerShutdownHook();
|
|
}
|
|
}
|
|
|
|
@ClientCacheApplication
|
|
@EnableEntityDefinedRegions
|
|
static class GeodeClientTestConfiguration { }
|
|
|
|
}
|
|
----
|
|
|
|
First we extend the STDG provided `ForkingClientServerIntegrationTestsSupport` class. Then, we define a JUnit
|
|
`@BeforeClass` static setup method to fork our GemFire/Geode JVM process using the `GeodeServerTestConfiguration.class`
|
|
specifying exactly how the server should be configured and finally we create the matching `GeodeClientTestConfiguration`
|
|
class to configure and bootstrap our JUnit, Spring `TestContext` based test, which acts as the client.
|
|
|
|
STDG takes care of coordinating the client & server, using random connection ports, etc. You simply just need to
|
|
provide the configuration of the client and server as required by your application and test case(s).
|
|
|
|
Here is 1
|
|
https://github.com/spring-projects/spring-boot-data-geode/blob/master/spring-geode-autoconfigure/src/test/java/org/springframework/geode/boot/autoconfigure/security/ssl/AutoConfiguredSslIntegrationTests.java[example]
|
|
of a concrete client/server _Integration Test_ extending STDG's `ForkingClientServerIntegrationTestsSupprt` class.
|
|
|
|
Notice, too, that I am using SDG's
|
|
https://docs.spring.io/spring-data/geode/docs/current/reference/html/#bootstrap-annotation-config[Annotation-based configuration model]
|
|
(e.g. `CacheServerApplication`, `@EnableEntityDefinedRegions`) to make the GemFire/Geode configuration even easier.
|
|
|
|
If you are using SBDG with this project, then some of the annotations are not even required (e.g. `ClientCacheApplication`).
|
|
|
|
When SBDG & STDG are combined, the power you have is quite extensive.
|
|
|
|
NOTE: Through the _Integration Test_ support provided by and in STDG is relatively simple, this is also not quite yet
|
|
the ideal way for writing client/sever _Integration Tests_. Eventually, we want to include an annotation, something
|
|
like `@ClientServerIntegrationTest(serverConfigClass = GeodeServerTestConfiguration.class)`, the equivalent to
|
|
`@EnableGemFireMockObjects` for _Unit Testing_, to make configuration and testing of client/server applications
|
|
that much easier. See https://github.com/spring-projects/spring-test-data-geode/issues/9[Issue #9] for more details.
|
|
This feature would be loosely based on, and similar to,
|
|
_Spring Boot_ https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html[Testing]
|
|
with _Test Slices_.
|
|
|
|
[[integration-testing-resource-cleanup]]
|
|
==== Cleaning up after GemFire/Geode during Integration Tests
|
|
|
|
When writing _Integration Tests_ using "live" GemFire/Geode objects (e.g. Regions), those object can leave artifacts
|
|
behind after a test run completes.
|
|
|
|
This can potentially cause conflicts between _Integration Test Cases_ that use features like persistence having
|
|
similarly named Regions particularly if you are not careful to differentiate the working directory between your tests.
|
|
This is also problematic, especially when switching between versions of GemFire/Geode, used by your application, during
|
|
testing. Perhaps you are in the middle of testing a (rolling) upgrade.
|
|
|
|
At any rate, STDG has you covered. If you would like to make sure that artifacts are properly cleaned up after a test
|
|
run, then you can annotate your test class with STDG's `@EnableGemFireResourceCollector` annotation, like so:
|
|
|
|
.Using `@EnableGemFireResourceCollector
|
|
[source,java]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@ContextConfiguration
|
|
class ExampleIntegrationTest {
|
|
|
|
@CacheServerApplication
|
|
@EnableLocator
|
|
@EnableManager
|
|
@EnableGemFireResourceCollector
|
|
static class TestGeodeConfiguration { }
|
|
|
|
}
|
|
----
|
|
|
|
Like the `@EnableGemFireMockObjects` annotation, you can control which Spring `TestContext` test event will trigger
|
|
a GemFire/Geode resource (garbage) collection process using the `collectOnEvents` attribute.
|
|
|
|
Also, you can attempt to clean any GemFire/Geode `DiskStore` files (created by persistence, overflow or PDX) by setting
|
|
the `@EnableGemFireResourceCollector` annotation, `tryCleanDiskStoreFiles` attribute to `true`.
|
|
|
|
The following list of GemFire/Geode files with extensions or names are cleaned up by STDG's
|
|
`@EnableGemFireResourceCollector` functionality:
|
|
|
|
.GemFire/Geode File Extensions
|
|
|===
|
|
| File Extension | Description
|
|
|
|
| `.dat`
|
|
| Locator view file; e.g. `locator10334view.dat`
|
|
|
|
| `.gfs`
|
|
| Statistics archive file
|
|
|
|
| `.crf`
|
|
| Oplog file containing create, update, invalidate operations
|
|
|
|
| `.drf`
|
|
| Oplog file containing delete operations
|
|
|
|
| `.if`
|
|
| DiskStore metadata file
|
|
|
|
| `.krf`
|
|
| Oplog file for key and crf offset information
|
|
|
|
| `.lk`
|
|
| DiskStore access control file
|
|
|
|
| `.log`
|
|
| Log files created by GemFire/Geode process (Locators, Servers, Manager, etc)
|
|
|
|
| `.pid`
|
|
| File containing the OS process ID of the GemFire/Geode process (Locator, Server, etc)
|
|
|
|
| `.properties`
|
|
| GemFire/Geode properties configuration file (e.g. `gemfire.properties`)
|
|
|
|
| `.xml`
|
|
| GemFire/Geode XML configuration file (e.g. `cache.xml`)
|
|
|===
|
|
|
|
.GemFire/Geode Filenames
|
|
|===
|
|
| Filename | Description
|
|
|
|
| `backup` | filename prefix
|
|
| `cache` | filename prefix
|
|
| `configdiskdir` | _Cluster Configuration Service_ directory name
|
|
| `default` | filename prefix
|
|
| `drlk_if` | filename prefix
|
|
| `gfsecurity` | filename prefix
|
|
| `gemfire` | directory/file name
|
|
| `geode` | directory/file name
|
|
| `locator` | directory/file prefix name
|
|
| `overflow` | filename prefix
|
|
|
|
|===
|
|
|
|
The names of file extensions and files/directories are treated by STDG as case insensitive when matching.
|
|
|
|
For a complete https://cwiki.apache.org/confluence/display/GEODE/Geode+Artifacts[list of artifacts] created by
|
|
GemFire/Geode processes, follow the link.
|
|
|
|
|
|
[[testing-logging-behavior]]
|
|
=== Asserting Logging Behavior
|
|
|
|
It is sometimes necessary or useful to write tests to assert an application's logging behavior.
|
|
|
|
For instance, if your application needs to log an event that occurred, output configuration meta-data on startup,
|
|
alert a user to some system event such as low memory, out of disk space, or a temporary network outage, or whatever
|
|
the case might be, it is useful to assert that your application logs an appropriate message.
|
|
|
|
But, how do you assert that certain log events with an appropriate log message has been made by the application
|
|
when the conditions constituting the log event have been arranged?
|
|
|
|
Now, STDG provides the capability to 1) assert that your application, or an application component, made a log event
|
|
at the appropriate moment and 2) that the log message communicates enough contextual-based information to be useful
|
|
to the user of your application.
|
|
|
|
To do this, STDG provides the `org.springframework.data.geode.tests.logging.slf4j.logback.TestAppender` class.
|
|
|
|
This Log Appender can be used when your application logging framework is configured with _Logback_ as the provider.
|
|
|
|
You declare the `TestAppender` in a `logback.xml` configuration file as follows:
|
|
|
|
.logback.xml configuration file
|
|
[source,xml]
|
|
----
|
|
<appender name="testAppender" class="org.springframework.data.gemfire.tests.logging.slf4j.logback.TestAppender">
|
|
<encoder>
|
|
<pattern>TEST - %m%n</pattern>
|
|
</encoder>
|
|
</appender>
|
|
----
|
|
|
|
Then, the `TestAppender` can be used by registering it with a `Logger`:
|
|
|
|
.Logger using the TestAppender
|
|
[source,xml]
|
|
----
|
|
<logger name="example.app.net.service.NetworkService" level="WARN">
|
|
<appender-ref ref="testAppender"/>
|
|
</logger>
|
|
----
|
|
|
|
For example, assume your application's `NetworkService` class uses the named `Logger` to log network events,
|
|
e.g. a DDoS attack:
|
|
|
|
.Application component with logging
|
|
[source,java]
|
|
----
|
|
@Service
|
|
class NetworkService {
|
|
|
|
private final Logger logger = LoggerFactory.getLogger(NetworkService.class);
|
|
|
|
void processDenialOfServiceAttack(NetworkEvent event) {
|
|
|
|
logger.warn("A DDoS attack occured at {} from IP Address {}", event.getTime(), event.getIpAddress());
|
|
|
|
// process the network event
|
|
|
|
logger.warn("Another log message");
|
|
}
|
|
|
|
void processLoginRequest(LoginRequest request) {
|
|
|
|
logger.info("User {} is attepting to login", request.getUser().getName());
|
|
|
|
// process login request
|
|
}
|
|
}
|
|
----
|
|
|
|
Then, it is a simple matter to test the logging behavior of your application by doing:
|
|
|
|
.Test logging behavior of the NetworkService class
|
|
[source,java]
|
|
----
|
|
class NetworkServiceUnitTests {
|
|
|
|
private static TestAppender testAppender = TestAppender.getInstance();
|
|
|
|
private NetworkService service;
|
|
|
|
@Before
|
|
public void setup() {
|
|
this.service = new NetworkService();
|
|
}
|
|
|
|
@Test
|
|
public void processDenialOfServiceAttackLogsNetworkEvent() {
|
|
|
|
NetworkEvent event = new NetworkEvent();
|
|
|
|
this.service.processDenialOfServiceAttack(event);
|
|
|
|
assertThat(testAppender.lastLogMessage())
|
|
.isEqualTo("A DDoS attack occurred at 2019-07-02 19:39:15 from IP Address 10.22.101.16");
|
|
|
|
assertThat(testAppender.lastLogMessage())
|
|
.isEqualTo("Another log message");
|
|
|
|
assertThat(testAppender.lastLogMessage()).isNull();
|
|
}
|
|
|
|
@Test
|
|
public void processLoginRequestDoesNotLogAnyMessageWithLogLevelSetToWarn() {
|
|
|
|
LoginRequest request = new LoginRequest();
|
|
|
|
this.service.processLoginRequest(request);
|
|
|
|
assertThat(testAppender.lastLogMessage()).isNull();
|
|
}
|
|
}
|
|
----
|
|
|
|
You may also clear any remaining, pending log messages from the in-memory queue (`Stack`)
|
|
by calling `TestAppender.clear()`.
|
|
|
|
All log message recorded by the `TestAppender` are stored from the most recent log event to the earliest log event.
|
|
Successively calling `TestAppender.lastLogMessage()` gets the most recent, last log message recorded first, then
|
|
the next log message recorded before the last, most recent log message and so on until no more log messages
|
|
for the operation under test exists, in which case `null` is returned from `lastLogMessage()` thereafter.
|
|
|
|
|
|
[[conclusion]]
|
|
=== Conclusion
|
|
|
|
Anyway, we hope this has intrigued your interests and gets you started for now. Ideas, contributions, or other
|
|
feedback is most welcomed.
|
|
|
|
Thank you!
|