A UnitTest in Geode is a test with very narrow and well defined scope. Any complex dependencies and interactions are stubbed or mocked.

  • Should use JUnit 4 or JUnit Jupiter (aka JUnit 5) syntax
  • File name ends with *Test
  • Should be part of the test source set folder (<geode-module-dir>/test/java)
  • Should complete in milliseconds 
  • Should generally test a single class
  • Typically uses white-box testing, uses Mocks/Fakes and helps guarantee internal quality (quality of code and class design)
  • Follows "A Set of Unit Testing Rules" by Michael Feathers
    • A UnitTest should not do any of the following:
      • communicate with a database
      • communicate across the network
      • access the file system
      • prevent the running of other unit tests in parallel
      • require anything special in the environment (such as editing config files or running an external process)

You would typically write a new UnitTest for each class you create. If you create a new class called RebalanceTask in the package org.apache.geode.internal.cache.control then you should also create a new test called RebalanceTaskTest in the same package but under src/test/java. This test should use JUnit 4 syntax and have a JUnit Category of type org.apache.geode.junit.categories.UnitTest. All dependencies should generally be Mocks or Spies created with Mockito or fakes that are coded specifically for use by the test in order to isolate the code being tested to just the class under test. The test should not use the network or file system and it should complete within milliseconds.

If you are identifying and ultimately fixing a bug, then you should write at least one new test that reproduces the bug which will then validate its fix. If possible, write a UnitTest which reproduces some part of the bug and use *RegressionTest as the class name suffix. If the bug involves a concurrency problem in the class RebalanceTask which results in throwing a NullPointerException then you might write a new test class called RebalanceTaskInitializedAtomicallyRegressionTest. If you cannot reproduce the bug in a UnitTest then do so in an IntegrationTest or DistributedTest instead. If you can reproduce the bug in a UnitTest but coverage of the bug and its fix would be even better with an IntegrationTest regression test, then you should write both tests as these will provide complementary but slightly different test coverage.

It's not absolutely necessary to introduce a new *RegressionTest class for covering a bug. If new regression test coverage fits better within an existing test class, then add one or more new tests to that existing test class but include javadocs on the new test methods which includes the GEODE JIRA ticket number and its summary at a minimum.

Preferred Testing Frameworks and Libraries Appropriate for Geode UnitTests

Overall Framework

  • JUnit 4 (including Rules) or JUnit Jupiter (which does not support Rules)
  • Paramerized – JUnit 4.12 has a bug which prevents Parameterized and Category from working together properly
    • Please use CategoryWithParameterizedRunnerFactory from Geode when using Parameterized (see GEODE-1350)

Assertion Library

  • AssertJ – richer API with better failure messages than JUnit 4 Assert

Mocking Library

  • Mockito

Expected Exceptions

  • Use AssertJ or Catch-Exception instead of JUnit 4 Rule ExpectedException
  • AssertJ (assertThatThrownBy) – this is the cleanest and preferred way to test for expected exception
  • Catch-Exception – better support for complex nesting of exceptions or exceptions with unusual APIs

Additional 3rd Party Libraries for UnitTests

  • JUnitParams – provides test method parameterization (usually better to use than JUnit 4 Parameterized)
  • System Rules – powerful set of JUnit 4 Rules
  • Awaitility* – useful for awaiting asynchronous conditions

(*=Concurrency frameworks or rules are typically more appropriate for IntegrationTests)

Custom Geode JUnit 4 Rules and Testing Tools

  • RestoreLocaleRule – restores JVM to original Locale
  • RestoreTCCLRule – restores Thread Context Class Loader
  • ExecutorServiceRule* – provides ExecutorService for concurrent testing

(*=Concurrency frameworks or rules are typically more appropriate for IntegrationTests)

Examples of actual tests

Examples of some good UnitTests:

  • TODO

Examples of good RegressionTests which are UnitTests:

  • CacheServerBridgeClientMembershipRegressionTest

General form of a UnitTest:

package org.apache.geode.internal.cache.control;
 
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;

import org.apache.geode.test.junit.categories.UnitTest;

public class RebalanceTaskTest {
 
  private RebalanceTask rebalanceTask;
  private RebalanceStats rebalanceStats;
 
  @Before
  public void setUp() {
    // arrange: create the SUT (system under test) and its dependencies
    rebalanceStats = mock(RebalanceStats.class);
    rebalanceTask = new RebalanceTask(rebalanceStats);
  }
 
  @After
  public void tearDown() {
    // cleanup anything that might affect a later test or that uses any resources
  }
 
  @Test
  public void execIncrementsRebalanceStats() throws Exception {
    // act: do something like invoke exec
    // assert: assertThat expected behavior or interaction with dependency occurred
  }
}
  • No labels