Hello Ottomata,
I'd like you to do a code review. Please visit
https://gerrit.wikimedia.org/r/90167
to review the following change.
Change subject: Add basic test infrastructure
......................................................................
Add basic test infrastructure
Change-Id: I3ba4b78d32fc22a45b8ef43ae6d3794e38803866
---
M kraken-etl/pom.xml
A
kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/LoggingMockingTestCase.java
A
kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/MockingTestCase.java
A
kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/CollectionAppender.java
A
kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/LogUtil.java
5 files changed, 446 insertions(+), 0 deletions(-)
git pull ssh://gerrit.wikimedia.org:29418/analytics/kraken
refs/changes/67/90167/1
diff --git a/kraken-etl/pom.xml b/kraken-etl/pom.xml
index 3446124..4fcc6d6 100644
--- a/kraken-etl/pom.xml
+++ b/kraken-etl/pom.xml
@@ -27,6 +27,27 @@
<version>0.1.0-SNAPSHOT-wmf-1</version>
<scope>compile</scope>
</dependency>
+
+ <dependency>
+ <groupId>org.easymock</groupId>
+ <artifactId>easymock</artifactId>
+ <version>3.0</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.powermock</groupId>
+ <artifactId>powermock-module-junit4</artifactId>
+ <version>1.5</version>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.powermock</groupId>
+ <artifactId>powermock-api-easymock</artifactId>
+ <version>1.5</version>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build>
diff --git
a/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/LoggingMockingTestCase.java
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/LoggingMockingTestCase.java
new file mode 100644
index 0000000..fc199a0
--- /dev/null
+++
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/LoggingMockingTestCase.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2013 Wikimedia Foundation
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
USA.
+
+package org.wikimedia.analytics.kraken.etl.testutil;
+
+import com.google.common.collect.Lists;
+
+import org.wikimedia.analytics.kraken.etl.testutil.log.LogUtil;
+import org.apache.log4j.spi.LoggingEvent;
+import org.junit.After;
+
+import java.util.Iterator;
+
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+
+public abstract class LoggingMockingTestCase extends MockingTestCase {
+ private String loggerName;
+ private LogUtil.LoggerSettings loggerSettings;
+ private java.util.Collection<LoggingEvent> loggedEvents;
+
+ protected final void assertLogMessageContains(String needle, Level level) {
+ LoggingEvent hit = null;
+ Iterator<LoggingEvent> iter = loggedEvents.iterator();
+ while (hit == null && iter.hasNext()) {
+ LoggingEvent event = iter.next();
+ if (event.getRenderedMessage().contains(needle)) {
+ if (level == null || level.equals(event.getLevel())) {
+ hit = event;
+ }
+ }
+ }
+ assertNotNull("Could not find log message containing '" + needle + "'",
+ hit);
+ assertTrue("Could not remove log message containing '" + needle + "'",
+ loggedEvents.remove(hit));
+ }
+
+ protected final void assertLogMessageContains(String needle) {
+ assertLogMessageContains(needle, null);
+ }
+
+ 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));
+ }
+
+ // 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();
+ }
+}
diff --git
a/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/MockingTestCase.java
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/MockingTestCase.java
new file mode 100644
index 0000000..f2cb3c7
--- /dev/null
+++
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/MockingTestCase.java
@@ -0,0 +1,155 @@
+// Copyright (C) 2013 Wikimedia Foundation
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
USA.
+
+package org.wikimedia.analytics.kraken.etl.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.
+ */
+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();
+ }
+}
diff --git
a/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/CollectionAppender.java
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/CollectionAppender.java
new file mode 100644
index 0000000..948aa28
--- /dev/null
+++
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/CollectionAppender.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 Wikimedia Foundation
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
USA.
+
+package org.wikimedia.analytics.kraken.etl.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);
+ }
+}
diff --git
a/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/LogUtil.java
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/LogUtil.java
new file mode 100644
index 0000000..12bb3fd
--- /dev/null
+++
b/kraken-etl/src/test/java/org/wikimedia/analytics/kraken/etl/testutil/log/LogUtil.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2013 Wikimedia Foundation
+//
+// This program is free software; you can redistribute it and/or
+// modify it under the terms of the GNU General Public License
+// as published by the Free Software Foundation; either version 2
+// of the License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with this program; if not, write to the Free Software
+// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
USA.
+
+package org.wikimedia.analytics.kraken.etl.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);
+ }
+ }
+ }
+}
--
To view, visit https://gerrit.wikimedia.org/r/90167
To unsubscribe, visit https://gerrit.wikimedia.org/r/settings
Gerrit-MessageType: newchange
Gerrit-Change-Id: I3ba4b78d32fc22a45b8ef43ae6d3794e38803866
Gerrit-PatchSet: 1
Gerrit-Project: analytics/kraken
Gerrit-Branch: master
Gerrit-Owner: QChris <[email protected]>
Gerrit-Reviewer: Ottomata <[email protected]>
_______________________________________________
MediaWiki-commits mailing list
[email protected]
https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits