Skip to content

Commit

Permalink
log
Browse files Browse the repository at this point in the history
  • Loading branch information
Johnlon committed Dec 8, 2015
1 parent eedad6f commit f8805dc
Show file tree
Hide file tree
Showing 20 changed files with 1,120 additions and 0 deletions.
193 changes: 193 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
# SLF4JTesting

SLF4TTesting provides a "Testing Double" of SLF4J LoggerFactory to facilitate log testing but also as a utility
for use in unit and integration tests to conveniently configure logging on a test by test basis.

SLF4TTesting has been designed to:

- support unit and integration testing
- support concurrent test execution such as [TestNG parallel suites](http://testng.org/doc/documentation-main.html#parallel-suites) or [Scalatest parallel test execution](http://doc.scalatest.org/2.0/index.html#org.scalatest.ParallelTestExecution)
- promote dependency injection of the logging implementation
- support testing of multi-threaded components
- support testing of assemblies
- provide a simple console logging facility
- allow selective suppression of individual console logging messages (eg to hide any expected errors)
- support integration with mocking libraries (eg Mockito)

SLT4FTesting differs from some other solutions because it:

* does not pollute the classpath with any StaticLoggerBinder stuff
* does not compete with other SLF4J implementations resident on the classpath
* does not rely on cluttering the classpath with special logback.xml/logback-test.xml files
* does not use statics
* does not rely on singletons anywhere
* does not need thread-locals etc

## Usage

### Setting up

Include the jar as a test dependency.

### Basic Example

Here we use constructor injection using the SLF4J logger factory interface.

```java
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;

public class Example1 {

private final Logger logger;

public Example1(ILoggerFactory lf) {
this.logger = lf.getLogger(Example1.class.getName());
}

public void aMethodThatLogs() {
logger.info("Hello World!");
}
}
```

The `.getName()` is a tiny bit of boilerplate that's only necessary because SLF4J's'
(ILoggerFactory)[http://www.slf4j.org/api/org/slf4j/ILoggerFactory.html] interface does not provide a by-class convenience
method like (LoggerFactory)[http://www.slf4j.org/api/org/slf4j/LoggerFactory.html] does.

Constructor injection is not that arduous, even less so if you use a dependecy injection framework, but it provides the seam that allows us to
inject out testing double.

```java
import org.junit.Test;
import static org.junit.Assert.assertTrue;

public class Example1UnitTest {

@Test
public void testAMethodThatLogs() throws Exception {
TestLoggerFactory loggerFactory = new TestLoggerFactory();

Example1 sut = new Example1(loggerFactory);
sut.aMethodThatLogs();

TestLogger logger = loggerFactory.getLogger(Example1.class.getName());
assertTrue(logger.contains(".*Hello.*"));
}
}
```


## Motivation

### Improved construction patterns and testability

I strongly believe in dependency injection (DI) so anything that looks like a static or singleton offends me,
statics and singletons are an impediment to testable code.

We can't conveniently substitute statics for during testing and in the case of SLF4J we end up resorting to
nasties like classpath manipulation to get over this.

Anything global or static leaks information between tests and typically requires before and after setup
and tear down which is more test boilerplate.
This pattern means that running tests classes or class methods concurrently leads to interference as all threads interact with the
shared mutable global.

These problems can be avoided if we do not permit this share state between tests and instead use POJO's and DI; this library attempts
to make it easy to use a POJO/DI pattern with SLF4J and write tests for that code.

### Console logging during tests

Typically we don't want logging going to the console in tests. A possible exception to this is that it's useful
if any unexpected errors are logged to the console. Sometimes however we are performing negative tests and we expect
certain error messages but we do not want to see these on the console.

SLF4JTesting supports optional console logging but also a regex based scheme to suppress certain expected log lines.
This helps us keep our CI console clean without losing the benefits of logging of unexpected conditions that would help
us diagnose failing tests more easily.

### Multithreaded code

Sometimes we have an integration test where the test subject performs logging from another thread than the foreground test thread.
SLF4JTesting collects all logging occurring in the test subject regardless of thread.

### Assembly integration testing

Sometimes we want to test an assembly where the assembly constructs objects internally.
If each parent object is injected with the logger factory and any objects the parent creates are also injected with the
logger factory then this pattern allows the entire logging implementation to be substituted in tests.

```java
import org.slf4j.ILoggerFactory;
import org.slf4j.Logger;

public class AssemblyExample {

private final ChildObject child;

public AssemblyExample(ILoggerFactory lf) {
child = new ChildObject(lf);
}
}

class ChildObject {
private final Logger logger;
private final GrandchildObject grandchild;

ChildObject(ILoggerFactory lf) {
logger = lf.getLogger(AssemblyExample.class.getName());
grandchild = new GrandchildObject(lf);
}

public void aMethodThatLogs() {
logger.info("Hello World!");
}
}

class GrandchildObject {
private final Logger logger;

GrandchildObject (ILoggerFactory lf) {
logger = lf.getLogger(AssemblyExample.class.getName());
}

public void aMethodThatLogs() {
logger.info("Bye World!");
}
}
```

### Making assertions

It is possible to make some assertions about what was logged by using methods provided by TestLogger`.`

```
TestLogger logger = loggerFactory.getLogger(Example1.class.getName());
assertTrue(logger.contains(".*Hello.*"));
```

### Mocking

You can make further assertions by using a mocking framework.

SLF4JLogging allows you to hook a mock (eg Mockito or other) up to the logging implementation so that
you can use the power of your chosen mocking framework directly to test the logging.

SLF4J logging does not impose any particular mocking framework.

```java
public class MockingExampleUnitTest {
@Test
public void testAMethodThatLogsWithAMock() throws Exception {
Logger mockLogger = Mockito.mock(Logger.class);

Settings cfg = new Settings().delegate(BasicExample.class.getName(), mockLogger);
TestLoggerFactory loggerFactory = new TestLoggerFactory(cfg);

BasicExample sut = new BasicExample(loggerFactory);
sut.aMethodThatLogs();

Mockito.verify(mockLogger).info("Hello World!");
}
}
```
19 changes: 19 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
group 'portingle'
version '0.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.7

repositories {
mavenCentral()
}

dependencies {
compile 'org.slf4j:slf4j-api:1.7.5'

testCompile 'junit:junit:4.11'
testCompile 'org.mockito:mockito-all:1.10.19'


}
Binary file added gradle/wrapper/gradle-wrapper.jar
Binary file not shown.
6 changes: 6 additions & 0 deletions gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#Wed Dec 02 23:12:13 GMT 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip
160 changes: 160 additions & 0 deletions gradlew
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env bash

##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################

# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""

APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

warn ( ) {
echo "$*"
}

die ( ) {
echo
echo "$*"
echo
exit 1
}

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
esac

# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar

# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi

# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi

# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:code=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi

# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`

# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option

if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi

# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
function splitJvmOpts() {
JVM_OPTS=("$@")
}
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"

exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
Loading

0 comments on commit f8805dc

Please sign in to comment.