Add test infrastructure for tests that use logging and mocking

Change-Id: I5a38a5fd3256ca05e91d234c2bec7d18c06a75a2
This commit is contained in:
Christian Aistleitner
2013-06-19 17:54:01 +02:00
committed by David Pursehouse
parent d1b1a8801b
commit e362959d05
10 changed files with 786 additions and 1 deletions

View File

@@ -0,0 +1,177 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.testutil;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import org.eclipse.jgit.util.FileUtils;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
public abstract class FilesystemLoggingMockingTestCase extends LoggingMockingTestCase {
private Collection<File> toCleanup = Lists.newArrayList();
/**
* Asserts that a given file exists.
*
* @param file The file to test.
*/
protected void assertExists(File file) {
assertTrue("File '" + file.getAbsolutePath() + "' does not exist",
file.exists());
}
/**
* Asserts that a given file does not exist.
*
* @param file The file to test.
*/
protected void assertDoesNotExist(File file) {
assertFalse("File '" + file.getAbsolutePath() + "' exists", file.exists());
}
/**
* Asserts that a given file exists and is a directory.
*
* @param file The file to test.
*/
protected void assertDirectory(File file) {
// Although isDirectory includes checking for existence, we nevertheless
// explicitly check for existence, to get more appropriate error messages
assertExists(file);
assertTrue("File '" + file.getAbsolutePath() + "' is not a directory",
file.isDirectory());
}
/**
* Asserts that creating a directory from the given file worked
*
* @param file The directory to create
*/
protected void assertMkdirs(File file) {
assertTrue("Could not create directory '" + file.getAbsolutePath() + "'",
file.mkdirs());
}
/**
* Asserts that creating a directory from the specified file worked
*
* @param parent The parent of the directory to create
* @param name The name of the directoryto create (relative to {@code parent}
* @return The created directory
*/
protected File assertMkdirs(File parent, String name) {
File file = new File(parent, name);
assertMkdirs(file);
return file;
}
/**
* Asserts that creating a file worked
*
* @param file The file to create
*/
protected void assertCreateFile(File file) throws IOException {
assertTrue("Could not create file '" + file.getAbsolutePath() + "'",
file.createNewFile());
}
/**
* Asserts that creating a file worked
*
* @param parent The parent of the file to create
* @param name The name of the file to create (relative to {@code parent}
* @return The created file
*/
protected File assertCreateFile(File parent, String name) throws IOException {
File file = new File(parent, name);
assertCreateFile(file);
return file;
}
/**
* Creates a file in the system's default folder for temporary files.
*
* The file/directory automatically gets removed during tearDown.
*
* The name of the created file begins with 'gerrit_test_', and is located
* in the system's default folder for temporary files.
*
* @param suffix Trailing part of the file name.
* @return The temporary file.
* @throws IOException If a file could not be created.
*/
private File createTempFile(String suffix) throws IOException {
String prefix ="gerrit_test_";
if (!Strings.isNullOrEmpty(getName())) {
prefix += getName() + "_";
}
File tmp = File.createTempFile(prefix, suffix);
toCleanup.add(tmp);
return tmp;
}
/**
* Creates a file in the system's default folder for temporary files.
*
* The file/directory automatically gets removed during tearDown.
*
* The name of the created file begins with 'gerrit_test_', and is located
* in the system's default folder for temporary files.
*
* @return The temporary file.
* @throws IOException If a file could not be created.
*/
protected File createTempFile() throws IOException {
return createTempFile("");
}
/**
* Creates a directory in the system's default folder for temporary files.
*
* The directory (and all it's contained files/directory) automatically get
* removed during tearDown.
*
* The name of the created directory begins with 'gerrit_test_', and is be
* located in the system's default folder for temporary files.
*
* @return The temporary directory.
* @throws IOException If a file could not be created.
*/
protected File createTempDir() throws IOException {
File tmp = createTempFile(".dir");
if (!tmp.delete()) {
throw new IOException("Cannot delete temporary file '" + tmp.getPath()
+ "'");
}
tmp.mkdir();
return tmp;
}
private void cleanupCreatedFiles() throws IOException {
for (File file : toCleanup) {
FileUtils.delete(file, FileUtils.RECURSIVE);
}
}
public void tearDown() throws Exception {
cleanupCreatedFiles();
super.tearDown();
}
}

View File

@@ -0,0 +1,134 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.testutil;
import com.google.common.collect.Lists;
import com.google.gerrit.testutil.log.LogUtil;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import org.junit.After;
import java.util.Iterator;
/**
* Testcase capturing associated logs and allowing to assert on them.
*
* For a test case SomeNameTest, the log for SomeName gets captured. Assertions
* on logs run against the coptured log events from this logger. After the
* tests, the logger are set back to their original settings.
*/
public abstract class LoggingMockingTestCase extends MockingTestCase {
private String loggerName;
private LogUtil.LoggerSettings loggerSettings;
private java.util.Collection<LoggingEvent> loggedEvents;
/**
* Assert a logged event with a given string.
* <p>
* If such a event is found, it is removed from the captured logs.
*
* @param needle The string to look for.
*/
protected final void assertLogMessageContains(String needle) {
LoggingEvent hit = null;
Iterator<LoggingEvent> iter = loggedEvents.iterator();
while (hit == null && iter.hasNext()) {
LoggingEvent event = iter.next();
if (event.getRenderedMessage().contains(needle)) {
hit = event;
}
}
assertNotNull("Could not find log message containing '" + needle + "'",
hit);
assertTrue("Could not remove log message containing '" + needle + "'",
loggedEvents.remove(hit));
}
/**
* Assert a logged event whose throwable contains a given string
* <p>
* If such a event is found, it is removed from the captured logs.
*
* @param needle The string to look for.
*/
protected final void assertLogThrowableMessageContains(String needle) {
LoggingEvent hit = null;
Iterator<LoggingEvent> iter = loggedEvents.iterator();
while (hit == null && iter.hasNext()) {
LoggingEvent event = iter.next();
if (event.getThrowableInformation().getThrowable().toString()
.contains(needle)) {
hit = event;
}
}
assertNotNull("Could not find log message with a Throwable containing '"
+ needle + "'", hit);
assertTrue("Could not remove log message with a Throwable containing '"
+ needle + "'", loggedEvents.remove(hit));
}
/**
* Assert that all logged events have been asserted
*/
// As the PowerMock runner does not pass through runTest, we inject log
// verification through @After
@After
public final void assertNoUnassertedLogEvents() {
if (loggedEvents.size() > 0) {
LoggingEvent event = loggedEvents.iterator().next();
String msg = "Found untreated logged events. First one is:\n";
msg += event.getRenderedMessage();
if (event.getThrowableInformation() != null) {
msg += "\n" + event.getThrowableInformation().getThrowable();
}
fail(msg);
}
}
@Override
public void setUp() throws Exception {
super.setUp();
loggedEvents = Lists.newArrayList();
// The logger we're interested is class name without the trailing "Test".
// While this is not the most general approach it is sufficient for now,
// and we can improve later to allow tests to specify which loggers are
// to check.
loggerName = this.getClass().getCanonicalName();
loggerName = loggerName.substring(0, loggerName.length()-4);
loggerSettings = LogUtil.logToCollection(loggerName, loggedEvents);
}
@Override
protected void runTest() throws Throwable {
super.runTest();
// Plain JUnit runner does not pick up @After, so we add it here
// explicitly. Note, that we cannot put this into tearDown, as failure
// to verify mocks would bail out and might leave open resources from
// subclasses open.
assertNoUnassertedLogEvents();
}
@Override
public void tearDown() throws Exception {
if (loggerName != null && loggerSettings != null) {
Logger logger = LogManager.getLogger(loggerName);
loggerSettings.pushOntoLogger(logger);
}
super.tearDown();
}
}

View File

@@ -0,0 +1,155 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.testutil;
import junit.framework.TestCase;
import org.easymock.EasyMock;
import org.easymock.IMocksControl;
import org.junit.After;
import org.junit.runner.RunWith;
import org.powermock.api.easymock.PowerMock;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.ArrayList;
import java.util.Collection;
/**
* Test case with some support for automatically verifying mocks.
*
* This test case works transparently with EasyMock and PowerMock.
*/
public abstract class MockingTestCase extends TestCase {
private Collection<Object> mocks;
private Collection<IMocksControl> mockControls;
private boolean mocksReplayed;
private boolean usePowerMock;
/**
* Create and register a mock control.
*
* @return The mock control instance.
*/
protected final IMocksControl createMockControl() {
IMocksControl mockControl = EasyMock.createControl();
assertTrue("Adding mock control failed", mockControls.add(mockControl));
return mockControl;
}
/**
* Create and register a mock.
*
* Creates a mock and registers it in the list of created mocks, so it gets
* treated automatically upon {@code replay} and {@code verify};
* @param toMock The class to create a mock for.
* @return The mock instance.
*/
protected final <T> T createMock(Class<T> toMock) {
return createMock(toMock, null);
}
/**
* Create a mock for a mock control and register a mock.
*
* Creates a mock and registers it in the list of created mocks, so it gets
* treated automatically upon {@code replay} and {@code verify};
* @param toMock The class to create a mock for.
* @param control The mock control to create the mock on. If null, do not use
* a specific control.
* @return The mock instance.
*/
protected final <T> T createMock(Class<T> toMock, IMocksControl control) {
assertFalse("Mocks have already been set to replay", mocksReplayed);
final T mock;
if (control == null) {
if (usePowerMock) {
mock = PowerMock.createMock(toMock);
} else {
mock = EasyMock.createMock(toMock);
}
assertTrue("Adding " + toMock.getName() + " mock failed",
mocks.add(mock));
} else {
mock = control.createMock(toMock);
}
return mock;
}
/**
* Set all registered mocks to replay
*/
protected final void replayMocks() {
assertFalse("Mocks have already been set to replay", mocksReplayed);
if (usePowerMock) {
PowerMock.replayAll();
} else {
EasyMock.replay(mocks.toArray());
}
for (IMocksControl mockControl : mockControls) {
mockControl.replay();
}
mocksReplayed = true;
}
/**
* Verify all registered mocks
*
* This method is called automatically at the end of a test. Nevertheless,
* it is safe to also call it beforehand, if this better meets the
* verification part of a test.
*/
// As the PowerMock runner does not pass through runTest, we inject mock
// verification through @After
@After
public final void verifyMocks() {
if (!mocks.isEmpty() || !mockControls.isEmpty()) {
assertTrue("Created mocks have not been set to replay. Call replayMocks "
+ "within the test", mocksReplayed);
if (usePowerMock) {
PowerMock.verifyAll();
} else {
EasyMock.verify(mocks.toArray());
}
for (IMocksControl mockControl : mockControls) {
mockControl.verify();
}
}
}
@Override
public void setUp() throws Exception {
super.setUp();
usePowerMock = false;
RunWith runWith = this.getClass().getAnnotation(RunWith.class);
if (runWith != null) {
usePowerMock = PowerMockRunner.class.isAssignableFrom(runWith.value());
}
mocks = new ArrayList<Object>();
mockControls = new ArrayList<IMocksControl>();
mocksReplayed = false;
}
@Override
protected void runTest() throws Throwable {
super.runTest();
// Plain JUnit runner does not pick up @After, so we add it here
// explicitly. Note, that we cannot put this into tearDown, as failure
// to verify mocks would bail out and might leave open resources from
// subclasses open.
verifyMocks();
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.testutil;
import com.google.gwtorm.client.KeyUtil.Encoder;
public class PassThroughKeyUtilEncoder extends Encoder {
@Override
public String encode(String e) {
return e;
}
@Override
public String decode(String e) {
return e;
}
}

View File

@@ -0,0 +1,55 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.testutil;
import com.google.common.collect.Sets;
import org.easymock.EasyMock;
import org.easymock.IArgumentMatcher;
import java.util.Set;
/**
* Match for Iterables via set equals
*
* Converts both expected and actual parameter to a set and compares those two
* sets via equals to determine whether or not they match.
*/
public class SetMatcher<T> implements IArgumentMatcher {
public static <S extends Iterable<T>,T> S setEq(S expected) {
EasyMock.reportMatcher(new SetMatcher<T>(expected));
return null;
}
Set<T> expected;
public SetMatcher(Iterable<T> expected) {
this.expected = Sets.newHashSet(expected);
}
@Override
public boolean matches(Object actual) {
if (actual instanceof Iterable<?>) {
Set<?> actualSet = Sets.newHashSet((Iterable<?>)actual);
return expected.equals(actualSet);
}
return false;
}
@Override
public void appendTo(StringBuffer buffer) {
buffer.append(expected);
}
}

View File

@@ -0,0 +1,58 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.testutil.log;
import com.google.common.collect.Lists;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.spi.LoggingEvent;
import java.util.Collection;
import java.util.LinkedList;
/**
* Log4j appender that logs into a list
*/
public class CollectionAppender extends AppenderSkeleton {
private Collection<LoggingEvent> events;
public CollectionAppender() {
events = new LinkedList<LoggingEvent>();
}
public CollectionAppender(Collection<LoggingEvent> events) {
this.events = events;
}
@Override
public boolean requiresLayout() {
return false;
}
@Override
protected void append(LoggingEvent event) {
if (! events.add(event)) {
throw new RuntimeException("Could not append event " + event);
}
}
@Override
public void close() {
}
public Collection<LoggingEvent> getLoggedEvents() {
return Lists.newLinkedList(events);
}
}

View File

@@ -0,0 +1,88 @@
// Copyright (C) 2013 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.testutil.log;
import org.apache.log4j.Appender;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.spi.LoggingEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
public class LogUtil {
/**
* Change logger's setting so it only logs to a collection.
*
* @param logName Name of the logger to modify.
* @param collection The collection to log into.
* @return The logger's original settings.
*/
public static LoggerSettings logToCollection(String logName,
Collection<LoggingEvent> collection) {
Logger logger = LogManager.getLogger(logName);
LoggerSettings loggerSettings = new LoggerSettings(logger);
logger.removeAllAppenders();
logger.setAdditivity(false);
CollectionAppender listAppender = new CollectionAppender(collection);
logger.addAppender(listAppender);
return loggerSettings;
}
/**
* Capsule for a logger's settings that get mangled by rerouting logging to a collection
*/
public static class LoggerSettings {
private final boolean additive;
private final List<Appender> appenders;
/**
* Read off logger settings from an instance.
*
* @param logger The logger to read the settings off from.
*/
private LoggerSettings(Logger logger) {
this.additive = logger.getAdditivity();
Enumeration<?> appenders = logger.getAllAppenders();
this.appenders = new ArrayList<Appender>();
while (appenders.hasMoreElements()) {
Object appender = appenders.nextElement();
if (appender instanceof Appender) {
this.appenders.add((Appender)appender);
} else {
throw new RuntimeException("getAllAppenders of " + logger
+ " contained an object that is not an Appender");
}
}
}
/**
* Pushes this settings back onto a logger.
*
* @param logger the logger on which to push the settings.
*/
public void pushOntoLogger(Logger logger) {
logger.setAdditivity(additive);
logger.removeAllAppenders();
for (Appender appender : appenders) {
logger.addAppender(appender);
}
}
}
}