Back to Repositories

Testing Database Discovery Client Memory Cache Implementation in Apollo Config

This test suite validates the memory cache decorator implementation for Apollo’s database discovery client, focusing on caching behavior and error handling during service instance discovery.

Test Coverage Overview

The test suite provides comprehensive coverage of the DatabaseDiscoveryClientMemoryCacheDecorator implementation.

Key areas tested include:
  • Cache initialization and management
  • Service instance retrieval behavior
  • Database error handling and recovery
  • Cache update mechanisms
  • Memory consistency during concurrent operations

Implementation Analysis

The testing approach employs JUnit and Mockito frameworks to validate the caching decorator pattern.

Technical implementation includes:
  • Mock-based service instance simulation
  • Cache state verification
  • Exception handling validation
  • Database interaction patterns

Technical Details

Testing infrastructure includes:
  • JUnit Jupiter for test execution
  • Mockito for service simulation
  • Custom ServiceInstanceFactory for test data generation
  • Assert statements for validation
  • Exception testing utilities

Best Practices Demonstrated

The test suite exemplifies high-quality testing practices:

  • Comprehensive error scenario coverage
  • Isolated test cases with clear objectives
  • Proper mock usage and verification
  • Edge case handling
  • Clean test method organization

apolloconfig/apollo

apollo-biz/src/test/java/com/ctrip/framework/apollo/biz/registry/DatabaseDiscoveryClientMemoryCacheDecoratorImplTest.java

            
/*
 * Copyright 2024 Apollo Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */
package com.ctrip.framework.apollo.biz.registry;

import static com.ctrip.framework.apollo.biz.registry.ServiceInstanceFactory.newServiceInstance;
import static org.junit.jupiter.api.Assertions.*;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

class DatabaseDiscoveryClientMemoryCacheDecoratorImplTest {

  @Test
  void init() {
    DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class);
    DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator
        = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client);
    decorator.init();
  }

  @Test
  void updateCacheTask_empty() {
    DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class);
    DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator
        = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client);
    decorator.updateCacheTask();

    Mockito.verify(client, Mockito.never()).getInstances(Mockito.any());
  }

  @Test
  void updateCacheTask_exception() {
    final String serviceName = "a-service";
    DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class);
    Mockito.when(client.getInstances(serviceName))
        .thenReturn(
            Arrays.asList(
                newServiceInstance(serviceName, "http://10.240.34.56:8080/", "beijing"),
                newServiceInstance(serviceName, "http://10.240.34.56:8081/", "beijing"),
                newServiceInstance(serviceName, "http://10.240.34.56:8082/", "beijing")
            )
        );
    DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator
        = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client);
    List<ServiceInstance> list = decorator.getInstances(serviceName);
    assertEquals(3, list.size());

    // if database error
    Mockito.when(client.getInstances(serviceName))
            .thenThrow(OutOfMemoryError.class);
    assertThrows(OutOfMemoryError.class, () -> decorator.readFromDatabase(serviceName));

    // task won't be interrupted by Throwable
    decorator.updateCacheTask();

    Mockito.verify(client, Mockito.times(3)).getInstances(serviceName);
  }

  @Test
  void getInstances_from_cache() {
    DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class);
    Mockito.when(client.getInstances("a-service"))
        .thenReturn(
            Arrays.asList(
                newServiceInstance("a-service", "http://10.240.34.56:8080/", "beijing"),
                newServiceInstance("a-service", "http://10.240.34.56:8081/", "beijing")
            )
        );
    Mockito.when(client.getInstances("b-service"))
        .thenReturn(
            Arrays.asList(
                newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"),
                newServiceInstance("b-service", "http://10.240.56.78:8081/", "shanghai"),
                newServiceInstance("b-service", "http://10.240.56.78:8082/", "shanghai")
            )
        );

    DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator
        = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client);
    assertEquals(2, decorator.getInstances("a-service").size());
    assertEquals(2, decorator.getInstances("a-service").size());
    assertEquals(3, decorator.getInstances("b-service").size());
    assertEquals(3, decorator.getInstances("b-service").size());

    // only invoke 1 times because always read from cache
    Mockito.verify(client, Mockito.times(1)).getInstances("a-service");
    Mockito.verify(client, Mockito.times(1)).getInstances("b-service");
  }

  @Test
  void getInstances_from_cache_when_database_updated() {
    DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class);
    Mockito.when(client.getInstances("a-service"))
        .thenReturn(
            Arrays.asList(
                newServiceInstance("a-service", "http://10.240.34.56:8080/", "beijing"),
                newServiceInstance("a-service", "http://10.240.34.56:8081/", "beijing")
            )
        );
    Mockito.when(client.getInstances("b-service"))
        .thenReturn(
            Arrays.asList(
                newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"),
                newServiceInstance("b-service", "http://10.240.56.78:8081/", "shanghai"),
                newServiceInstance("b-service", "http://10.240.56.78:8082/", "shanghai")
            )
        );

    DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator
        = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client);
    assertEquals(2, decorator.getInstances("a-service").size());
    assertEquals(2, decorator.getInstances("a-service").size());
    assertEquals(3, decorator.getInstances("b-service").size());
    assertEquals(3, decorator.getInstances("b-service").size());

    // only invoke 1 times because always read from cache
    Mockito.verify(client, Mockito.times(1)).getInstances("a-service");
    Mockito.verify(client, Mockito.times(1)).getInstances("b-service");

    // instances in database are changed
    Mockito.when(client.getInstances("b-service"))
        .thenReturn(
            Collections.singletonList(
                newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai")
            )
        );

    // read again
    assertEquals(2, decorator.getInstances("a-service").size());
    // cache doesn't update yet, so we still get 3 instances
    assertEquals(3, decorator.getInstances("b-service").size());

    // only invoke 1 times because always read from cache
    Mockito.verify(client, Mockito.times(1)).getInstances("a-service");
    Mockito.verify(client, Mockito.times(1)).getInstances("b-service");

    decorator.updateCacheTask();

    // read again
    assertEquals(2, decorator.getInstances("a-service").size());
    // cache updated already, so we still get 1 instances
    assertEquals(1, decorator.getInstances("b-service").size());

    // invoke 2 times because always read from database again by task
    Mockito.verify(client, Mockito.times(2)).getInstances("a-service");
    Mockito.verify(client, Mockito.times(2)).getInstances("b-service");
  }


  @Test
  void getInstances_from_cache_when_database_crash() {
    DatabaseDiscoveryClient client = Mockito.mock(DatabaseDiscoveryClient.class);
    Mockito.when(client.getInstances("a-service"))
        .thenReturn(
            Arrays.asList(
                newServiceInstance("a-service", "http://10.240.34.56:8080/", "beijing"),
                newServiceInstance("a-service", "http://10.240.34.56:8081/", "beijing")
            )
        );
    Mockito.when(client.getInstances("b-service"))
        .thenReturn(
            Arrays.asList(
                newServiceInstance("b-service", "http://10.240.56.78:8080/", "shanghai"),
                newServiceInstance("b-service", "http://10.240.56.78:8081/", "shanghai"),
                newServiceInstance("b-service", "http://10.240.56.78:8082/", "shanghai")
            )
        );

    DatabaseDiscoveryClientMemoryCacheDecoratorImpl decorator
        = new DatabaseDiscoveryClientMemoryCacheDecoratorImpl(client);
    assertEquals(2, decorator.getInstances("a-service").size());
    assertEquals(2, decorator.getInstances("a-service").size());
    assertEquals(3, decorator.getInstances("b-service").size());
    assertEquals(3, decorator.getInstances("b-service").size());

    // only invoke 1 times because always read from cache
    Mockito.verify(client, Mockito.times(1)).getInstances("a-service");
    Mockito.verify(client, Mockito.times(1)).getInstances("b-service");

    // database crash
    Mockito.when(client.getInstances(Mockito.any()))
        .thenThrow(OutOfMemoryError.class);
    assertThrows(OutOfMemoryError.class, () -> decorator.readFromDatabase("a-service"));
    assertThrows(OutOfMemoryError.class, () -> decorator.readFromDatabase("b-service"));
    
    // read again
    assertEquals(2, decorator.getInstances("a-service").size());
    assertEquals(3, decorator.getInstances("b-service").size());

    Mockito.verify(client, Mockito.times(2)).getInstances("a-service");
    Mockito.verify(client, Mockito.times(2)).getInstances("b-service");
  }
}