package com.equalexperts.logging;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mutabilitydetector.unittesting.MutabilityAssertionError;
import java.util.*;
import java.util.function.Function;
import static org.hamcrest.core.StringContains.containsString;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
public class OpsLoggerTestDoubleTest {
private final OpsLogger<TestMessages> logger = new OpsLoggerTestDouble<>();
@Captor
private ArgumentCaptor<OpsLoggerTestDouble<TestMessages>> captor;
@Mock
private Function<OpsLogger<TestMessages>, OpsLogger<TestMessages>> mockSpyFunction;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
//region tests for log
@Test
public void log_shouldAllowValidCalls() throws Exception {
logger.log(TestMessages.Foo);
}
@Test
public void log_shouldThrowAnException_givenAnInvalidFormatStringWithTheRightArguments() throws Exception {
try {
logger.log(TestMessages.BadFormatString, 42);
fail("expected an exception");
} catch (IllegalFormatException e) {
//this exception is expected
}
}
@Test
public void log_shouldThrowAnException_whenNotEnoughFormatStringArgumentsAreProvided() throws Exception {
try {
logger.log(TestMessages.Bar);
fail("expected an exception");
} catch (IllegalFormatException e) {
//this exception is expected
}
}
@Test
public void log_shouldThrowAnExceptionWhenTooManyFormatStringArgumentsAreProvided() throws Exception {
try {
logger.log(TestMessages.Bar, "Foo", "Bar");
fail("expected an exception");
} catch (IllegalArgumentException e) {
//this exception is expected
assertEquals("Too many format string arguments provided", e.getMessage());
}
}
@Test
public void log_shouldCorrectlyAllowLogMessagesWithTwoOrMoreFormatStringArguments() throws Exception {
logger.log(TestMessages.MessageWithMultipleArguments, "Foo", "Bar");
}
@Test
public void log_shouldAllowALogMessageWithAUUIDAsAnArgument() throws Exception {
logger.log(TestMessages.Bar, UUID.randomUUID());
}
@Test
public void log_shouldThrowAnException_givenANullLogMessage() throws Exception {
try {
logger.log(null);
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("LogMessage must be provided"));
}
}
@Test
public void log_shouldThrowAnException_givenANullMessageCode() throws Exception {
try {
logger.log(TestMessages.InvalidNullCode);
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("MessageCode must be provided"));
}
}
@Test
public void log_shouldThrowAnException_givenAnEmptyMessageCode() throws Exception {
try {
logger.log(TestMessages.InvalidEmptyCode);
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("MessageCode must be provided"));
}
}
@Test
public void log_shouldThrowAnException_givenANullMessagePattern() throws Exception {
try {
logger.log(TestMessages.InvalidNullFormat);
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("MessagePattern must be provided"));
}
}
@Test
public void log_shouldThrowAnException_givenAnEmptyMessagePattern() throws Exception {
try {
logger.log(TestMessages.InvalidEmptyFormat);
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("MessagePattern must be provided"));
}
}
@Test
public void log_shouldThrowAnException_givenAMutableFormatStringArgument() throws Exception {
try {
logger.log(TestMessages.MessageWithMultipleArguments, "foo", new StringBuilder("bar"));
fail("expected an exception");
} catch (MutabilityAssertionError e) {
assertThat(e.getMessage(), containsString("StringBuilder"));
}
}
@Test
public void log_shouldNotCallAnOverloadedMethod() throws Exception {
//calling another method inside this log method can cause trouble with spying frameworks
OpsLogger<TestMessages> logger = spy(this.logger);
logger.log(TestMessages.Foo);
verify(logger).log(TestMessages.Foo);
verifyNoMoreInteractions(logger);
}
//endregion
//region tests for logThrowable
@Test
public void logThrowable_shouldAllowValidCalls_givenAThrowable() throws Exception {
logger.logThrowable(TestMessages.Foo, new RuntimeException());
}
@Test
public void logThrowable_shouldThrowAnException_givenAnInvalidFormatStringWithTheRightArgumentsAndAThrowable() throws Exception {
try {
logger.logThrowable(TestMessages.BadFormatString, new RuntimeException(), 42);
fail("expected an exception");
} catch (IllegalFormatException e) {
//this exception is expected
}
}
@Test
public void logThrowable_shouldThrowAnException_givenNotEnoughFormatStringArgumentsAndAThrowable() throws Exception {
try {
logger.logThrowable(TestMessages.Bar, new RuntimeException());
fail("expected an exception");
} catch (IllegalFormatException e) {
//this exception is expected
}
}
@Test
public void logThrowable_shouldThrowAnException_givenTooManyFormatStringArgumentsAndAThrowable() throws Exception {
try {
logger.logThrowable(TestMessages.Bar, new RuntimeException(), "Foo", "Bar");
fail("expected an exception");
} catch (IllegalArgumentException e) {
//this exception is expected
assertEquals("Too many format string arguments provided", e.getMessage());
}
}
@Test
public void logThrowable_shouldAllowCorrectLogMessages_givenTwoOrMoreFormatStringArgumentsAndThrowable() throws Exception {
logger.logThrowable(TestMessages.MessageWithMultipleArguments, new RuntimeException(), "Foo", "Bar");
}
@Test
public void logThrowable_shouldThrowAnException_givenANullLogMessageAndThrowable() throws Exception {
try {
logger.logThrowable(null, new RuntimeException());
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("LogMessage must be provided"));
}
}
@Test
public void logThrowable_shouldThrowAnException_givenANullThrowable() throws Exception {
try {
logger.logThrowable(TestMessages.Bar, null, "a");
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("Throwable instance must be provided"));
}
}
@Test
public void logThrowable_shouldThrowAnException_givenAThrowableAndNullMessageCode() throws Exception {
try {
logger.logThrowable(TestMessages.InvalidNullCode, new RuntimeException());
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("MessageCode must be provided"));
}
}
@Test
public void logThrowable_shouldThrowAnException_givenAThrowableAndAnEmptyMessageCode() throws Exception {
try {
logger.logThrowable(TestMessages.InvalidEmptyCode, new RuntimeException());
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("MessageCode must be provided"));
}
}
@Test
public void logThrowable_shouldThrowAnException_givenAThrowableAndNullMessageFormat() throws Exception {
try {
logger.logThrowable(TestMessages.InvalidNullFormat, new RuntimeException());
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("MessagePattern must be provided"));
}
}
@Test
public void logThrowable_shouldThrowAnException_givenAThrowableAndAnEmptyMessageFormat() throws Exception {
try {
logger.logThrowable(TestMessages.InvalidEmptyFormat, new RuntimeException());
fail("expected an exception");
} catch (AssertionError e) {
assertThat(e.getMessage(), containsString("MessagePattern must be provided"));
}
}
@Test
public void logThrowable_shouldThrowAnException_givenAThrowableAndAMutableFormatStringArgument() throws Exception {
try {
logger.logThrowable(TestMessages.MessageWithMultipleArguments, new RuntimeException(), "foo", new StringBuilder("bar"));
fail("expected an exception");
} catch (MutabilityAssertionError e) {
assertThat(e.getMessage(), containsString("StringBuilder"));
}
}
@Test
public void logThrowable_shouldNotCallAnOverloadedMethod_givenAThrowable() throws Exception {
//calling another method inside this log method can cause trouble with spying frameworks
OpsLogger<TestMessages> logger = spy(this.logger);
RuntimeException ex = new RuntimeException();
logger.logThrowable(TestMessages.Foo, ex);
verify(logger).logThrowable(TestMessages.Foo, ex);
verifyNoMoreInteractions(logger);
}
//endregion
//region tests for withSpyFunction
@SuppressWarnings("unchecked")
@Test
public void withSpyFunction_shouldReturnAnOpsLoggerTestDoubleWrappedByTheSpyFunction() throws Exception {
OpsLogger<TestMessages> expectedResult = (OpsLogger<TestMessages>) mock(OpsLogger.class);
doReturn(expectedResult).when(mockSpyFunction).apply(any(OpsLoggerTestDouble.class));
OpsLogger<TestMessages> result = OpsLoggerTestDouble.withSpyFunction(mockSpyFunction);
//noinspection unchecked
verify(mockSpyFunction).apply(captor.capture());
verifyNoMoreInteractions(mockSpyFunction);
assertSame(expectedResult, result);
assertEquals(mockSpyFunction, captor.getValue().getNestedLoggerDecorator());
}
//endregion
//region tests for with
@SuppressWarnings("unchecked")
@Test
public void with_shouldReturnANestedOpsLoggerTestDoubleWrappedByTheSpyFunction() throws Exception {
OpsLogger<TestMessages> expectedResult = (OpsLogger<TestMessages>) mock(OpsLogger.class);
doReturn(expectedResult).when(mockSpyFunction).apply(any(OpsLoggerTestDouble.class));
OpsLoggerTestDouble<TestMessages> logger = new OpsLoggerTestDouble<>(mockSpyFunction);
OpsLogger<TestMessages> result = logger.with(Collections::emptyMap);
//noinspection unchecked
verify(mockSpyFunction).apply(captor.capture());
verifyNoMoreInteractions(mockSpyFunction);
assertSame(expectedResult, result);
assertEquals(mockSpyFunction, captor.getValue().getNestedLoggerDecorator());
}
@Test
public void with_shouldReturnTheSameNestedLogger_givenAnEquivalentContextSupplier() throws Exception {
OpsLoggerTestDouble<TestMessages> logger = new OpsLoggerTestDouble<>(Function.identity());
Map<String, String> context = new HashMap<>();
context.put("foo", "bar");
HashMap<String, String> equivalentContext = new HashMap<>(context); //a different context with the same contents
DiagnosticContextSupplier supplier = () -> context;
DiagnosticContextSupplier equivalentSupplier = () -> equivalentContext;
assertNotEquals("precondition: suppliers should not be equal", supplier, equivalentSupplier);
assertSame(logger.with(supplier), logger.with(equivalentSupplier));
}
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
@Test
public void with_shouldReturnADifferentNestedLogger_givenADifferentContextSupplier() throws Exception {
OpsLoggerTestDouble<TestMessages> logger = new OpsLoggerTestDouble<>(Function.identity());
Map<String, String> context = new HashMap<>();
context.put("foo", "bar");
HashMap<String, String> differentContext = new HashMap<>();
differentContext.put("foo", "baz");
DiagnosticContextSupplier supplier = () -> context;
DiagnosticContextSupplier differentSupplier = () -> differentContext;
assertNotSame(logger.with(supplier), logger.with(differentSupplier));
}
//endregion
@Test
public void close_shouldThrowAnException() throws Exception {
/*
Application code shouldn't normally close a real logger, so throw an Exception in the test double
to discourage it
*/
try {
logger.close();
fail("Expected an exception");
} catch (IllegalStateException e) {
assertThat(e.getMessage(), containsString("OpsLogger instances should not be closed by application code."));
}
}
enum TestMessages implements LogMessage {
Foo("CODE-Foo", "No Fields"),
Bar("CODE-Bar", "One Field: %s"),
BadFormatString("CODE-BFS", "%++d"),
InvalidNullCode(null, "Blah"),
InvalidEmptyCode("", "Blah"),
InvalidNullFormat("CODE-InvalidNullFormat", null),
InvalidEmptyFormat("CODE-InvalidEmptyFormat", ""),
MessageWithMultipleArguments("CODE-MultipleArguments", "Multiple Format String Arguments: %s %s");
//region LogMessage implementation guts
private final String messageCode;
private final String messagePattern;
TestMessages(String messageCode, String messagePattern) {
this.messageCode = messageCode;
this.messagePattern = messagePattern;
}
@Override
public String getMessageCode() {
return messageCode;
}
@Override
public String getMessagePattern() {
return messagePattern;
}
//endregion
}
}