Gemfire Server for Test Environments
If your application is integrating with a gemfire cache, you may have chosen to stub out this boundary when running behaviour, acceptance or system tests. Intially we were using a fake object (see test doubles) in our test to avoid interaction with a gemfire cache, however we were concerned that the data being returned from the fake object was not visible from our behaviour tests. It went something like this (although I will use a Grocery store as an example instead of our actual domain):
1 public class FakeGroceryDetailsService implements GroceryDetailsService {
2 private Map<String, GroceryDetails> groceryDetails;
3
4 {
5 groceryDetails = new HashMap<String, GroceryDetails>();
6 groceryDetails.put("101", new GroceryDetails("Apple", "0.30"));
7 groceryDetails.put("102", new GroceryDetails("Banana", "0.15"));
8 }
9
10 @Override
11 public GroceryDetails getGroceryDetailsUsing(String groceryId) {
12 return groceryDetails.get(groceryId);
13 }
14 }
There were a few issues with this:
- Anyone writing new tests needed to have knowledge of the fake object
- The fake object had to live in the main source directory, and as such is part of your production code
- A mechanism is required to switch between the fake object and the real implementation
As such, we decided to build our own gemfire cache server.
This can be achieved by creating a cache.xml file and wrapping the regions with a server tag, however I prefer to keep as much as I can in Java code.
Starting Cache Server
Starting up a cache server is simple enough (and the things that are not so obvious are explained below).
1 public class GroceryStoreCache {
2 private static final String DISABLE_MULTICAST = "0";
3
4 private Cache cache;
5
6 public GroceryStoreCache(int serverPort, CacheRegion... regions) {
7 cache = new CacheFactory().set("mcast-port", DISABLE_MULTICAST).create();
8 configureCacheServer(serverPort);
9 configureRegions(regions);
10 }
11
12 private void configureCacheServer(int serverPort) {
13 CacheServer cacheServer = cache.addCacheServer();
14 cacheServer.setPort(serverPort);
15 }
16
17 private void configureRegions(CacheRegion... regions) {
18 for (CacheRegion region : regions) {
19 cache.createRegionFactory(LOCAL).create(region.name());
20 }
21 }
22
23 public void start() throws IOException {
24 cache.getCacheServers().get(0).start();
25 }
26
27 }
Multicast Port
The multicast port is set to zero to prevent the cache server discovering other gemfire cache instances running on a network. With the gemfire default license, there is a restriction in place that prevents more than three instances of a cache being started using the same multicast port. By setting to zero, the cache will not conflict with any other gemfire cache servers you have running on your build server.
Adding Regions
Regions are simply strings so for this example CacheRegion is just an enum.
1 public enum CacheRegion {
2 EMPLOYEES,
3 GROCERIES
4 }
These are added to the cache server using the region factory with LOCAL state. As the cache server will not talk across a network, changes to a region will not need to be reflected in peers.
Populating Regions
With the cache server up and running, the only thing left to do is populate it. There are only two rules that must be followed when populating a region:
- The object that is added to the cache must implement Serializable
- The object must be retrievable by an ID
With that in mind, an interface is used to ensure both of those rules are covered.
1 public interface CacheItem extends Serializable {
2 int getId();
3 }
Then within the cache server code, a populate method can be exposed to allow a class implementing CacheItem to be added to a region.
1 public void populate(CacheRegion region, CacheItem cacheItem) {
2 cache.getRegion(region.name()).put(cacheItem.getId(), cacheItem);
3 }
For this grocery store cache example, it would make sense to populate the cache with a grocery. The override of toString is to allow the contents of a cache region to be asserted in a test.
1 public class Grocery implements CacheItem {
2 private static final long serialVersionUID = -1145880717859373291L;
3
4 private int id;
5 private String name;
6 private String price;
7
8 public Grocery(int id, String name, String price) {
9 this.id = id;
10 this.name = name;
11 this.price = price;
12 }
13
14 @Override
15 public int getId() {
16 return id;
17 }
18
19 @Override
20 public String toString() {
21 return format("id: %d, name: %s, price: %s", id, name, price);
22 }
23 }
Printing Items in a Region
Due to issues with external systems, we ended up using this cache in our smoke environment (our first integrated environment we deploy to). We found it useful to be able to view the contents of a cache region at any given time. Retrieving the contents of a region on the cache is also straight forward.
1 public String print(CacheRegion region) {
2 StringBuilder cacheItems = new StringBuilder();
3 for (Entry<Object, Object> cacheItem : cache.getRegion(region.name()).entrySet()) {
4 cacheItems.append(cacheItem.getValue() + "\n");
5 }
6 return cacheItems.toString();
7 }
Clearing a Region
As the region is essentially a map, clearing the region is performed by retrieving the region and calling clear.
1 public void clear(CacheRegion region) {
2 cache.getRegion(region.name()).clear();
3 }
Connecting to the Cache Server
There is one subtle difference when connecting to a cache server. Instead of using a locator, a server is used instead. As the majority of gemfire users will be using a client-cache.xml file to define pools and regions, I will include the XML changes required for the client.
Previously your client-cache.xml file may have contained a pool element like this:
1 <pool name="client">
2 <locator host="localhost" port="10000" />
3 </pool>
This just needs updating to be:
1 <pool name="client">
2 <server host="localhost" port="10000" />
3 </pool>
Wrapping Up
An example of this cache server can be found at https://github.com/joneland/gemfire-sandbox.
The example includes a runner and exposes some of the cache methods via JMX. By using a JMX client to take care of all the MBean server invocations, it became easy for us to manipulate the cache from our tests. CacheJMXClient is an example of this. We used this client to populate and clear the cache between test scenarios.
1 CacheJMXClient cacheClient = new CacheJMXClient("localhost", "10000");
2
3 cacheClient.populateGrocery(101, "Apple", "0.30");
4
5 cacheClient.clearGroceries();
And that is it! Hopefully this post demonstrates how simple it is to get a gemfire cache server up and running for test environments.