Publisher

Publisher is a Java utility library to make private methods public in a type safe manner very easily. Type safety is created by using public substitute interfaces (or substitute interface for short). Substitute interface is a Java interface where all the wanted private methods from the class to be substituted are duplicated. Publishing a substitute interface creates a public substitute object (or substitute object for short) which can be used to write unit tests for private methods of the substituted class.

Using Publisher is a very convenient way to write unit tests for private methods and still maintain type safety.

Using Publisher

Normal usage

To use private methods of the instantiated class type safely a substitute interface must be introduced. The substitute interface will have all the needed private methods with exactly the same signature but with different access modifiers. The access modifier in the substitute interface must be always public. The substitute interface must be published with publish(Class, Object) along respective instantiated class to create a substitute object. For example, if the class is like this:

class HashGenerator {
    ...
    private String createDigest(String algorithm, String input)
    {
        // Some implementation.
    }
    ...
    private byte[] concat(byte[] left, byte[] right)
    {
        byte[] retVal = new byte[left.length + right.length];
        System.arraycopy(left, 0, retVal, 0, left.length);
        System.arraycopy(right, 0, retVal, left.length, right.length);
        return retVal;
    }
    ...
}

A substitute interface for the class would be:

interface SHashGenerator
{
    public String createDigest(String algorithm, String input);
    public byte[] concat(byte[] left, byte[] right);
}

And the unit test code could be:

@Test
public void concatTest()
{
    HashGenerator hashGenerator = new HashGenerator();
    SHashGenerator substitute = Publisher.publish(SHashGenerator.class, hashGenerator);
    byte[] left = { 1, 2, 3, 4 };
    byte[] right = { 10, 20, 30, 40, 50 };
    byte[] result = substitute.concat(left, right);
    assertEquals(9, result.length);
    int total = 0;
    for(byte b : result)
        total += b;
    assertEquals(160, total);
}

Usage with loops

If there is a need to create multiple instances of the substituted class (i.e. the one having private methods) repetitively then create(Class) and publish(Object) methods can be used together and achieve better performance than using publish(Class, Object) alone. Here is a continuation to the example above:

@Test
public void digestTest()
{
    Publisher<SHashGenerator> publisher = Publisher.create(SHashGenerator.class);
    for(int i = 0; i < _table.length; i++) {
        HashGenerator hashGenerator = new HashGenerator(i);
        SHashGenerator substitute = publisher.publish(hashGenerator);
        String result = substitute.createDigest("sha-1", _table[i]);
        // Make assertions.
    }
}

Static methods

If there is a need to test only private static methods of the substituted class publish(Class, Class) method can be used. The benefit is that there is no need to create an instance of the substituted class. Let's change the concatTest() example test method above to use publish(Class, Class) method. A substitute interface would be:

interface SHashGenerator
{
    public byte[] concat(byte[] left, byte[] right);
}

And the unit test:

@Test
public void concatTest()
{
    SHashGenerator substitute = Publisher.publish(SHashGenerator.class, HashGenerator.class);
    byte[] left = { 1, 2, 3, 4 };
    byte[] right = { 10, 20, 30, 40, 50 };
    byte[] result = substitute.concat(left, right);
    assertEquals(9, result.length);
    int total = 0;
    for(byte b : result)
        total += b;
    assertEquals(160, total);
}

Exceptions

If a substitute interface has a method which does not exist in the substituted class then SubstituteMethodNameConflictError is thrown. Most frequently this error is thrown because there is a typo in the substitute interface. So, if SubstituteMethodNameConflictError is thrown check the error message carefully and start comparing your substitute interface and substituted class.

Recommended conventions

Where to put substitute interfaces?

The recommendation is that substitute interfaces are created as a nested interface inside the test class. For example:

public class HashGeneratorTest
{
    interface SHashGenerator
    {
        public String createDigest(String algorithm, String input);
        public byte[] concat(byte[] left, byte[] right);
    }

    @Test
    public void digestTest()
    {
        ...
    }

    ...
}

Naming

A programmer is absolutely free to select whatever name he/she sees fit. There are no limitations other than the Java specification states. However, it is recommended to use one of the following prefixes for the substitute interface:

  • S, which stands for substitute
  • SI, which stands for substitute interface
  • PS, which stands for public substitute
  • PSI, which stands for public substitute interface

This way the unit test writing is simple and the substitute interface and the original class can be easily identified.

Performance

Because the implementation relies on java.lang.reflect.Proxy the performance is not as good as with direct calls (which, of course, cannot be made to private methods). To improve performance Publisher can be directed to use different caching policies. A caching policy is selected with annotations. There are several different publishing policies implemented but only one of them is really recommended and it is called normal caching (the one created with Id annotation). The others can be used but at the user's own risk.

There is also a simple trick to improve performance with a NoCaching annotation. For more information see Disabling caching.

Normal caching

If it seems that there is a performance issue with a normal usage the performance can be improved by introducing a caching policy for published methods. This is done by introducing an index id for each of the methods in the substitute interface. Id annotation is used for this purpouse. For example:

interface SHashGenerator
{
    @Id(0) public String createDigest(String algorithm, String input);
    @Id(1) public byte[] concat(byte[] left, byte[] right);
}

Constraints for Id values are:

  • Every method in the substitute interface must have an Id annotation to introduce a normal caching policy.
  • Same value is not allowed for multiple methods.
  • Value cannot be negative.
  • Values must be sequential.
  • The first value must be zero (0).

If any constraint is broken then IdAnnotationError is thrown.

Normal caching is the recommended caching policy, if caching is needed.

Disabling caching

To disable caching there are two options; removing all the Id annotations or using NoCaching annotation. If the latter is used the other annotations are completely ignored and no caching is used. For example:

@NoCaching
interface SHashGenerator
{
    @Id(0) public String createDigest(String algorithm, String input);
    @Id(1) public byte[] concat(byte[] left, byte[] right);
}

Notice also that using a NoCaching annotation can improve performance because other annotations are not checked and thus the time used for finding a proper publishing policy is greatly diminished. This is true even if no Id annotations are used.

Identity hash caching

* Identity hash caching policy IS NOT RECOMMENDED. Use at your own risk!!! * See the explanation below.

Identity hash caching is turned on by marking the substitute interface with IdentityHashCaching annotation. Id annotations are completely ignored and there is no need to remove possibly existing Id annotations. IdentityHashCaching overrides ConcurrentIdentityHashCaching and is overridden by NoCaching.

Here is an example:

@IdentityHashCaching
interface SHashGenerator
{
    public String createDigest(String algorithm, String input);
    public byte[] concat(byte[] left, byte[] right);
}

Comparing to the normal caching the identity hash caching has roughly twice as good performance and it is much simpler to use because the only annotation needed is IdentityHashCaching annotation. However, the problem with the identity hash caching is that Java does not have any public identity for objects. The closest alternative to the object identity is System.identityHashCode(Object). Usually this works but there is no guarantee that the System.identityHashCode(Object) returns a different value for different objects (i.e. substitute interface Methods in this case) and thus the caching policy may fail.

When you start seeing ClassCastExceptions, NullPointerExceptions, UndeclaredThrowableExceptions or AmbiguousMethodNameErrors then it means that there is a hash key conflict and you must change your caching policy. There are three options available:

Concurrent identity hash caching

* Concurrent hash caching policy IS NOT RECOMMENDED. Use at your own risk!!! * See the explanation from identity hash caching.

Concurrent identity hash caching is turned on by marking the substitute interface with ConcurrentIdentityHashCaching annotation. Id annotations are completely ignored and there is no need to remove possibly existing Id annotations. ConcurrentIdentityHashCaching is overridden by IdentityHashCaching and NoCaching.

Here is an example.

@ConcurrentIdentityHashCaching
interface SHashGenerator
{
    public String createDigest(String algorithm, String input);
    public byte[] concat(byte[] left, byte[] right);
}

This and identity hash caching have the same caching policy except concurrent identity hash caching policy uses ConcurrentMap for caching. Concurrent caching policy is a little bit slower but otherwise all which is true for identity hash caching is true for concurrent hash caching policy, also.

Requirements

  • Java 5 or later

Download

Download from Java Tools page.

Maven repository

[http://hapi.github.com/maven2/]

  • groupId: com.hapiware.util
  • artifactId: publisher
  • version: [1.1.0,)

License

MIT License

Copyright (c) 2010 Hapi, http://www.hapiware.com

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License