(1) Singletons and testing
Returning to
last weeks topic, lets look at Singletons in the concept of testing.
There’s a lot of
on-line pontificating on how Singletons are the curse of test driven software
development and should be banished. There’s almost as much boosting of the
entirely opposite view point – that they make testing easier.
I not convinced
I subscribe to either view – but what I can say is that code using Singletons
is no harder to test that code that does not use them.
The following
three examples show different ways to “configure” a Singleton at run-time so
your tests can generate specific situations without modifying any code that
consumes the singletons.
(1.1) System Properties
A classic
example of the Singleton is a logger used by all components in a system. This
example has some merit as just that, and example of a Singleton, but in
practice such a simple approach to logging is insufficient in any modern
system.
That said,
consider this:
package logging;
/**
*
Configurable logger.
*
<p>
* Set the
system property:
*
<ul>
*
<li>"logging.Logger.handler"
*
</ul>
* to the
name of an implementation of Handler to modify the
* logging
behaviour.
*/
public final
class Logger
{
private static Handler handler= createHandler();
private static Handler createHandler() {
(3)
String clazz= System.getProperty(
"logging.Logger.handler", "logging.DefaultHandler");
Handler h= null;
try {
h= (Handler) Class.forName(clazz).newInstance();
} finally {
return h != null ? h : new DefaultHandler();
}
}
(1) private
Logger() { }
/**
* Dispatch <tt>message</tt> to the configured
<tt>Handler</tt>.
* No additional formatting is done on <tt>message</tt>.
*/
(2) public
static void log(String message) {
handler.log(message);
}
}
(1)
Private constructor. Bit of a give away for a Singleton
(2)
public static methods on the class – by some accounts this is not a true
singleton (I tend to think it is) but is an example of how to implement the
pattern
(3)
A system property is examined to determine how to handle log messages.
So, in normal
operation, the Logger will use a DefaultHandler.
In test mode, we
may not want this, so we can implement our own Handler implementation and use
that:
package logging;
import
junit.framework.TestCase;
public class
TestLogger
extends TestCase
{
public static class TestHandler extends Handler {
public void log(String message) {
logMessages += message +", ";
}
}
private static String logMessages;
protected void setUp() throws Exception {
System.setProperty(
"logging.Logger.handler",
"logging.TestLogger$TestHandler");
logMessages = "";
}
public void testLogger() {
Logger.log("hello world");
Logger.log("hello mum");
Logger.log("hello kitty");
assertTrue(logMessages.equals(
"hello world, hello mum, hello kitty, "));
}
}
In this case the
custom handler is used to assist in the test, in other cases a “null” handler
could be used to simply discard the messages.
(1.2) Mock Objects
The custom
handler in the above example is, essentially, a mock object. If we want to be
more explicit about things, we can provide programmatic methods for assigning
the mock.
For example, if
we have a singleton Database. How to we mock its persistence layer? This is a
very common thing to do in testing.
Take this
example:
package data;
public final
class Database
{
(4) private
static Persistence persistence= new FilePersistence();
private static Database instance;
static void setPersistence(Persistence persistence) {
Database.persistence= persistence;
}
(5) public
static synchronized Database getInstance() {
if (instance == null)
instance= new Database();
return instance;
}
(1) private
Database() { }
/**
* @param id the user id
* @return the user identified by <tt>id</tt>,
<tt>null</tt>
* if not found.
*/
public User getUser(int id) {
(3)
return persistence.getUser(id);
}
}
(1)
Private constructor again – certainly smells like a Singleton
(2)
Public static lazily instantiated accessor for the single instance.
(3)
Persistence delegate is used here
(4)
defaulted here
(5)
and re-assigned here
So, we have the
production configuration that uses a real persistence mechanism, but we have
the opportunity to use a mock one.
The following
test case does just that (the logic class it tests and associated classes can
be found in the attached .zip file):
package logic;
import
junit.framework.TestCase;
import
data.User;
import
data.MockPersistence;
public class
TestIsHeTheDevilInDisguise
extends TestCase
{
/**
* The real devil should NOT be identified as being in disguise. He is
* who he is, and proud of it, no doubt.
*/
public void testTheDevil() {
User user= new User(666, "The Devil");
(1)
new MockPersistence(new User[] { user });
assertFalse(new LogicBean().isHeTheDevilInDisguise(user.getId()));
}
/**
* Even if he changes his name, the real devil should not be identified
* as being in disguise.
*/
public void testFakeDevil() {
User user= new User(666, "A Fake Devil?");
(1)
new MockPersistence(new User[] { user });
assertFalse(new LogicBean().isHeTheDevilInDisguise(user.getId()));
}
/**
* The well known alias "Lucifer" should be identified as being the
* devil in disguise.
*/
public void testLucifer() {
User user= new User(999, "Lucifer");
(1)
new MockPersistence(new User[] { user });
assertTrue(new LogicBean().isHeTheDevilInDisguise(user.getId()));
}
/**
* The well known alias "Beelzebub" should also be identified as being
* the evil one in a wig.
*/
public void testBeelzebub() {
User user= new User(333, "Beelzebub");
(1)
new MockPersistence(new User[] { user });
assertTrue(new LogicBean().isHeTheDevilInDisguise(user.getId()));
}
/**
* Sir Cliff (bless him) is rarely identified as Satan.
*/
public void testCliffRichard() {
User user= new User(111, "Cliff Richard");
(1)
new MockPersistence(new User[] { user });
assertFalse(new LogicBean().isHeTheDevilInDisguise(user.getId()));
}
}
(1)
Here a mock persistence object is created (it attaches itself to the Database).
This mock has exactly what we want in it so the test fails or works as expected
each time.
(1.3) Configuration Singleton
Ok, both the two
earlier methods are easy to use and flexible to various degrees. However, they
do introduce a security problem.
They leave
testing artefacts in the production code.
These artefacts
could introduce security problems!
For code running
inside the bank, this is not a big issue, but for the code that runs outside
the bank, on client machines, we should really try to minimise this risk.
So, what to do?
If we assume
good deployment practice that requires heavy obfuscation plus jar sealing and
signing. We can have production code query configuration Singletons that do not
delegate and always return the same values.
In a testing
scenario, one-for-one replacements for the configuration singletons can be
placed on the class path ahead of the real ones.
In production,
the fact that the jars have been sealed prevents third parties from dong this
themselves.
Take this
example, where a socket factor would normally return secure sockets can be
configured to return plain ones for testing:
package net;
import
java.net.Socket;
import
java.io.IOException;
public final
class SecureSocketFactory {
private SecureSocketFactory() { }
/**
* Create a secure socket.
*
* @param host machine name or ip
address of the machine an SSL
*
secured server is running on.
* @param port port the server is
listening on.
* @param pkcs12file absolute or relative pathname to a PKCS12 encoded
*
certificate chain to use as the client certificate.
* @param passwd plain text password for the
PKCS12 certificate file.
* @return an secure socket
* @throws java.io.IOException if the PKCS12 file could not be read, a
* valid certificate could not
be extracted from it or the socket
* create failed.
*/
public static Socket createSecureSocket(
String host, int port, String pkcs12file, String passwd)
throws IOException {
Socket socket = null;
if (Config.usePlainSockets()) {
socket = new Socket(host, port);
} else {
// create a real SSL socket
}
return socket;
}
}
Where the Config
class is:
package net;
final class
Config {
static boolean usePlainSockets() {
return false;
}
}
If we have a
test that wants to use plain sockets, we do something like this:
package net;
import
java.net.Socket;
import
java.net.ServerSocket;
import
junit.framework.TestCase;
public class
TestSecureSocketFactory
extends TestCase
{
public void testCreateSecureSocket() throws Exception {
new Thread() {
public void run() {
try {
new ServerSocket(666).accept();
} finally {
return;
}
}
}.start();
Thread.sleep(500);
Socket socket= SecureSocketFactory.createSecureSocket(
"localhost", 666,
"some-file-that-does-not-exist.p12", "foo");
assertTrue(socket != null);
}
}
final class
Config {
static boolean usePlainSockets() {
return true;
}
}
NOTE:
Taking this
approach is not fail-safe. Following this pattern, obfuscating the deployed
code, sealing and signing the jars are all important steps, but all they do is
“raise the bar” to a level where it would take a highly skilled practitioner to
break into the client code.
If we assume
that the client side can be subverted, we must ensure the server is secure –
which has less to do with software and more to do with procedure and correct
separation of responsibilities in the organisation.