Is there a way to print out Logcat (Log.i, Log.d) messages when running a JUnit (method) test in Android Studio?
I can see System.out.print message but no logcat pri
Logcat output will not be seen in unit tests because Logcat is Android feature - JUnit tests can use only standard Java, therefore Android features won't work.
What you can do in unit tests is inject "test doubles" into tested components. But Log.x
calls are static, so you can't override them (without resolving to e.g. PowerMock, which you should avoid at all costs).
Therefore, the first step will be to introduce a non-static class that will behave as a proxy for Log.x
calls:
/**
* This class is a non-static logger
*/
public class Logger {
public void e(String tag, String message) {
Log.e(tag, message);
}
public void w(String tag, String message) {
Log.w(tag, message);
}
public void v(String tag, String message) {
Log.v(tag, message);
}
public void d(String tag, String message) {
Log.d(tag, message);
}
}
Use this class in every place you have Log.x
calls now.
Second step would be to write a test-double implementation of Logger
that redirects to standard output:
public class UnitTestLogger extends Logger{
@Override
public void e(String tag, String message) {
System.out.println("E " + tag + ": " + message);
}
// similar for other methods
}
The last step is to inject UnitTestLogger
instead of Logger
in unit tests:
@RunWith(MockitoJUnitRunner.class)
public class SomeClassTest {
private Logger mLogger = new UnitTestLogger();
private SomeClass SUT;
@Before
public void setup() throws Exception {
SUT = new SomeClass(/* other dependencies here */ mLogger);
}
}
If you want to be rigorously strict about OOP concepts, you can extract common interface for Logger
and UnitTestLogger
.
That said, I never encountered a need to investigate Log.x
calls in unit tests. I suspect that you don't need it either. You can run unit tests in debug mode and step over the code line-by-line in debugger, which is much faster than attempting to investigate logcat output...
General advice:
If the code you are testing contains Log.x
static calls and your unit tests do not crash - you have a problem.
I'd guess that either all tests are being run with Robolectric
, or you have this statement in build.gradle: unitTests.returnDefaultValues = true
.
If you run all the tests with Robolectric
, then it is just unefficient, but if all Android calls return default values, then you test suite is not reliable. I suggest you fix this issue before proceeding any further because it will bite you in the future one way or another.
I would go with @Vasiliy's approach but with a small change. Instead of using System.out.println
for logging, we can actually use Java Logger configured with ConsoleHandler to log messages on the test output screen.
This can be achieved in below listed simple steps
Step 1: Define your own Log Level
public enum LogLevel {
VERBOSE,
DEBUG,
INFO,
WARNING,
ERROR,
SILENT
}
Step 2: Define Logger abstraction
public abstract class Logger {
....
public abstract void debug(String tag, String message);
public abstract void error(String tag, String message);
....
}
Step 3: java.util.Logging based implementation for Logger
public class JavaLogger extends Logger {
private java.util.logging.Logger mLogger;
public JavaLogger(LogLevel logLevel, String name) {
mLogger = java.util.logging.Logger.getLogger(name);
ConsoleHandler handler = new ConsoleHandler();
Level level = mapLogLevel(logLevel);
handler.setLevel(level);
mLogger.addHandler(handler);
mLogger.setLevel(level);
}
@Override
public void debug(String tag, String message) {
if (isLoggable(LogLevel.DEBUG)) {
mLogger.finer(message);
}
}
@Override
public void error(String tag, String message) {
if (isLoggable(LogLevel.ERROR)) {
mLogger.severe(message);
}
}
....
private Level mapLogLevel(LogLevel logLevel) {
Level level = Level.OFF;
switch (logLevel) {
case INFO:
level = Level.INFO;
break;
case WARNING:
level = Level.WARNING;
break;
case ERROR:
level = Level.SEVERE;
break;
case VERBOSE:
level = Level.FINEST;
break;
case DEBUG:
level = Level.FINER;
break;
case SILENT:
level = Level.OFF;
break;
default:
// no impl
}
return level;
}
}
Step 4: android.util.Log based implementation for Logger
public class AndroidLogger extends Logger {
public AndroidLogger(LogLevel logLevel) {
super(logLevel);
}
....
@Override
public void debug(String tag, String message) {
if (isLoggable(LogLevel.DEBUG)) {
Log.d(tag, message, null);
}
}
@Override
public void error(String tag, String message) {
if (isLoggable(LogLevel.ERROR)) {
Log.e(tag, message, tr);
}
}
....
}
Step 5: Create a simple wrapper class for over Logger implementation
public class AppLogger {
private static Logger sLogger;
public static void init(Logger logger) {
sLogger = logger;
}
public static void debug(final String tag, String message) {
sLogger.debug(tag, message);
}
public static void error(final String tag, String message) {
sLogger.error(tag, message, null);
}
public static void error(final String tag, String message, Throwable t) {
sLogger.error(tag, message, t);
}
...
}
Step 6: Initialization
Android
Application onCreate method
AppLogger.init(new AndroidLogger(LogLevel.DEBUG));
Junit
AppLogger can be initialized in either @BeforeClass or @Before
AppLogger.init(new JavaLogger(LogLevel.DEBUG, "test-logger"));
Now you can observe the log message in Test Run Window of Android Studio
If you still prever to go with the hacking solution (Using PowerMockito), I implemented following class to mock all android log functions.
import android.util.Log;
import org.junit.BeforeClass;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.HashMap;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.anyString;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest({Log.class})
public abstract class AndroidLogMock {
private static HashMap<Integer, String> LOG_LEVELS = new HashMap<Integer, String>(){{
put(Log.VERBOSE, "VERBOSE");
put(Log.DEBUG, "DEBUG");
put(Log.INFO, "INFO");
put(Log.WARN, "WARN");
put(Log.ERROR, "ERROR");
put(Log.ASSERT, "ASSERT");
}};
private static Answer<?> getLogInvocation(int logLevel) {
return (InvocationOnMock invocation) -> {
Object[] args = invocation.getArguments();
if(args.length > 1 && args.length < 3) {
AndroidLogMock.log(logLevel, args[0].toString(), args[1].toString());
} else if (args.length > 1 && args[2] instanceof Throwable) { //cause I'm paranoid
AndroidLogMock.log(logLevel, args[0].toString(), args[1].toString(), (Throwable) args[2]);
} else {
new Exception("no matching function found with correct number and/or type of arguments");
}
return null;
};
}
private static void log(int logLevel, String tag, String message) {
System.out.println("[" + LOG_LEVELS.get(logLevel) + "}" + " Tag:" + tag + " Msg: " + message);
}
private static void log(int logLevel, String tag, String message, Throwable throwable) {
AndroidLogMock.log(logLevel, tag, message);
System.out.println("Exception: ");
throwable.printStackTrace();
}
@BeforeClass
public static void setupLogMocks() {
PowerMockito.mockStatic(Log.class);
when(Log.v(anyString(), anyString())).thenAnswer(AndroidLogMock.getLogInvocation(Log.VERBOSE));
when(Log.v(anyString(), anyString(), any(Throwable.class))).thenAnswer(AndroidLogMock.getLogInvocation(Log.VERBOSE));
when(Log.d(anyString(), anyString())).thenAnswer(AndroidLogMock.getLogInvocation(Log.DEBUG));
when(Log.d(anyString(), anyString(), any(Throwable.class))).thenAnswer(AndroidLogMock.getLogInvocation(Log.DEBUG));
when(Log.i(anyString(), anyString())).thenAnswer(AndroidLogMock.getLogInvocation(Log.INFO));
when(Log.i(anyString(), anyString(), any(Throwable.class))).thenAnswer(AndroidLogMock.getLogInvocation(Log.INFO));
when(Log.w(anyString(), anyString())).thenAnswer(AndroidLogMock.getLogInvocation(Log.WARN));
when(Log.w(anyString(), anyString(), any(Throwable.class))).thenAnswer(AndroidLogMock.getLogInvocation(Log.WARN));
when(Log.e(anyString(), anyString())).thenAnswer(AndroidLogMock.getLogInvocation(Log.ERROR));
when(Log.e(anyString(), anyString(), any(Throwable.class))).thenAnswer(AndroidLogMock.getLogInvocation(Log.ERROR));
when(Log.wtf(anyString(), anyString())).thenAnswer(AndroidLogMock.getLogInvocation(Log.ASSERT));
when(Log.wtf(anyString(), anyString(), any(Throwable.class))).thenAnswer(AndroidLogMock.getLogInvocation(Log.ASSERT));
}
}
with imports:
testImplementation 'junit:junit:4.12'
testImplementation 'org.powermock:powermock-core:2.0.4'
testImplementation 'org.powermock:powermock-module-junit4:2.0.4'
testImplementation 'org.powermock:powermock-api-mockito2:2.0.4'
for usage, extend your UnitTest class:
public class SomeUnitTest extends AndroidLogMock {
@Test
public void testSomething() {
Log.w("My Tag", "This is a warning");
Log.e("My Tag", "This is an error", new Exception("Error"));
}
}
But @Vasiliy is rigth, folling the OOP way is cleaner.
I was searching for this same thing and didn't ever find a straight answer. I know this question is over a year old but still, it'd be good to have an answer here for future reference.
The android.util.Log class logs directly to Logcat and the implementation for android.util.Log is not available when running the unit tests on the local JVM. You'll receive an error when trying to use the Log class in your unit tests because "the android.jar file used to run unit tests does not contain any actual code (those APIs are provided only by the Android system image on a device)."
See Android Documentation on Unit Testing
So if you really want to use android.util.Log you'll need to mock it locally and utilize System.out.print to print to the console. To start, add PowerMockito to your project. If you're using Gradle, you can just add the following dependencies:
testCompile 'junit:junit:4.12'
testCompile 'org.powermock:powermock:1.6.5'
testCompile 'org.powermock:powermock-module-junit4:1.6.5'
testCompile 'org.powermock:powermock-api-mockito:1.6.5'
Next I used Steve's answer here to figure out how to return a parameter passed into a mock object using Mockito.
The result was something like:
import android.util.Log;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import static org.mockito.Matchers.anyString;
import static org.powermock.api.mockito.PowerMockito.when;
@RunWith(PowerMockRunner.class)
@PrepareForTest({Log.class})
public class SomeUnitTest {
@Test
public void testSomething() {
System.out.println("Running test");
PowerMockito.mockStatic(Log.class);
// Log warnings to the console
when(Log.w(anyString(), anyString())).thenAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
if (args.length > 1) { //cause I'm paranoid
System.out.println("Tag:" + args[0] + " Msg: " + args[1]);
}
return null;
}
});
Log.w("My Tag", "This is a warning");
}
}
Hope this helps someone!