There was a query on the TDD mailing list about how to test drive aspects. Here is an edited version of my reply to that list.
Just as for regular classes, TDD can drive aspects to a better design.
Assume that I’m testing a logging aspect that logs when certain methods are called. Here’s the JUnit 4 test:
package logging;
import static org.junit.Assert.*;
import org.junit.Test;
import app.TestApp;
public class LoggerTest {
@Test
public void FakeLoggerShouldBeCalledForAllMethodsOnTestClasses() {
String message = "hello!";
new TestApp().doFirst(message);
assertTrue(FakeLogger.messageReceived().contains(message));
String message2 = "World!";
new TestApp().doSecond(message, message2);
assertTrue(FakeLogger.messageReceived().contains(message));
}
}
Already, you might guess that FakeLogger
will be a test-only version of something, in this case, my logging aspect. Similarly, TestApp
is a simple class that I’m using only for testing. You might choose to use one or more production classes, though.
package app;
@Watchable
public class TestApp {
public void doFirst(String message) {}
public void doSecond(String message1, String message2) {}
}
and @Watchable
is a marker annotation that allows me to define pointcuts in my logger aspect without fragile coupling to concrete names, etc. You could also use an interface.
package app;
public @interface Watchable {}
I made up @Watchable
as a way of marking classes where the public methods might be of “interest” to particular observers of some kind. It’s analogous to the EJB 3 annotations that mark classes as “persistable” without implying too many details of what that might mean.
Now, the actual logging is divided into an abstract base aspect and a test-only concrete sub-aspect>
package logging;
import org.aspectj.lang.JoinPoint;
import app.Watchable;
abstract public aspect AbstractLogger {
// Limit the scope to the packages and types you care about.
public abstract pointcut scope();
// Define how messages are actually logged.
public abstract void logMessage(String message);
// Notice the coupling is to the @Watchable abstraction.
pointcut watch(Object object):
scope() && call(* (@Watchable *).*(..)) && target(object);
before(Object watchable): watch(watchable) {
logMessage(makeLogMessage(thisJoinPoint));
}
public static String makeLogMessage(JoinPoint joinPoint) {
StringBuffer buff = new StringBuffer();
buff.append(joinPoint.toString()).append(", args = ");
for (Object arg: joinPoint.getArgs())
buff.append(arg.toString()).append(", ");
return buff.toString();
}
}
and
package logging;
public aspect FakeLogger extends AbstractLogger {
// Only match on calls from the unit tests.
public pointcut scope(): within(logging.*Test);
public void logMessage(String message) {
lastMessage = message;
}
static String lastMessage = null;
public static String messageReceived() {
return lastMessage;
}
}
Pointcuts in aspects are like most other dependencies, best avoided ;) ... or at least minimized and based on abstractions, just like associations and inheritance relationships.
So, my test “pressure” drove the design in terms of where I needed abstraction in the Logger aspect: (i) how a message is actually logged and (ii) what classes get “advised” with logging behavior.
Just as for TDD of regular classes, the design ends up with minimized dependencies and flexibility (abstraction) where it’s most useful.
I can now implement the real, concrete logger, which will also be a sub-aspect of AbstractLogger
. It will define the scope()
pointcut to be a larger section of the system and it will send the message to the real logging subsystem.