在这种奇怪的情况下,我想测试“某些”功能而不接触其他功能……这对我来说很难选择正确的描述,我希望下面将展示的代码具有自我描述性。
假设我有一个保留一些策略的课程:
class TypeStrategy {
private static final CreateConsumer CREATE_CONSUMER = new CreateConsumer();
private static final ModifyConsumer MODIFY_CONSUMER = new ModifyConsumer();
private static final Map<Type, Consumer<ConsumerContext>> MAP = Map.of(
Type.CREATE, CREATE_CONSUMER,
Type.MODIFY, MODIFY_CONSUMER
);
public static void consume(Type type, ConsumerContext context) {
Optional.ofNullable(MAP.get(nodeActionType))
.orElseThrow(strategyMissing(type))
.accept(context);
}
}
这个想法非常简单-某些Type
已注册了一些策略;方法consume
只会尝试查找适当的注册类型,并使用提供的consume
在其上调用ConsumerContext
。
现在的问题是:我非常想测试我关心的所有策略是否已注册,并且可以在它们上调用accept
,这实际上是我要测试的全部。
通常,我会在@SuppressStaticInitializationFor
上使用TypeStrategy
,而使用WhiteBox::setInternalState
只会放置CREATE_CONSUMER
和MODIFY_CONSUMER
所需的内容;但是在这种情况下,我不能这样做,因为MAP
也将被跳过,而我真的不希望那样,我只关心这两种策略-我 {{ 1}}保持原样。
除了一些令人讨厌的重构之外,它确实使我得到了想要的位置,我不知道该如何实现这一目标。在最好的情况下,我希望MAP
支持某些“部分”跳过,您可以在其中确切地指定要跳过的内容的过滤器,但这不是一个选择。
我还可以测试呼叫链上的“其他所有内容”,即测试@SuppressStaticInitializationFor
应该执行的所有操作,但是在此测试中增加了近70行模拟,这成为了梦night知道它真的想测试很小的一块。
答案 0 :(得分:1)
从您的描述看来,黑盒测试似乎不是一种选择,因此也许我们可以通过模拟消费者的构造函数并验证其交互来依靠某些白盒测试。
在下面,您可以找到一个从初始样本推断出的完整示例,其中包括.orElseThrow(strategyMissing(type))
的可能选项。
一个重要的注释/免责声明:由于我们保留TypeStrategy
不变,因此将执行地图的静态初始化块。因此,我们需要特别注意消费者模拟实例。我们需要确保在所有模拟测试中都可以使用在初始模拟阶段添加到映射中的相同模拟实例,否则验证将失败。因此,我们不会为每个测试创建模拟,而是为所有测试创建一次。尽管在单元测试中不建议这样做(测试应该是独立的和独立的),但我相信在这种特殊情况下,这是可以接受的一个不错的权衡。
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import java.util.AbstractMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.*;
import static org.powermock.api.mockito.PowerMockito.whenNew;
// enable powermock magic
@RunWith(PowerMockRunner.class)
@PrepareForTest({MockitoTest.TypeStrategy.class})
public class MockitoTest {
private static CreateConsumer createConsumerMock;
private static ModifyConsumer modifyConsumerMock;
// static initializer in TypeStrategy => mock everything once in the beginning to avoid having new mocks for each test (otherwise "verify" will fail)
@BeforeClass
public static void setup() throws Exception {
// mock the constructors to return mocks which we can later check for interactions
createConsumerMock = mock(CreateConsumer.class);
modifyConsumerMock = mock(ModifyConsumer.class);
whenNew(CreateConsumer.class).withAnyArguments().thenReturn(createConsumerMock);
whenNew(ModifyConsumer.class).withAnyArguments().thenReturn(modifyConsumerMock);
}
@Test
public void shouldDelegateToCreateConsumer() {
checkSpecificInteraction(Type.CREATE, createConsumerMock);
}
@Test
public void shouldDelegateToModifyConsumer() {
checkSpecificInteraction(Type.MODIFY, modifyConsumerMock);
}
private void checkSpecificInteraction(Type type, Consumer<ConsumerContext> consumer) {
ConsumerContext expectedContext = new ConsumerContext();
// invoke the object under test
TypeStrategy.consume(type, expectedContext);
// check interactions
verify(consumer).accept(expectedContext);
}
@Test
public void shouldThrowExceptionForUnsupportedConsumer() {
ConsumerContext expectedContext = new ConsumerContext();
// unsupported type mock
Type unsupportedType = PowerMockito.mock(Type.class);
when(unsupportedType.toString()).thenReturn("Unexpected");
// powermock does not play well with "@Rule ExpectedException", use plain old try-catch
try {
// invoke the object under test
TypeStrategy.consume(unsupportedType, expectedContext);
// if no exception was thrown to this point, the test is failed
fail("Should have thrown exception for unsupported consumers");
} catch (Exception e) {
assertThat(e.getMessage(), is("Type [" + unsupportedType + "] not supported"));
}
}
/* production classes below */
public static class TypeStrategy {
private static final CreateConsumer CREATE_CONSUMER = new CreateConsumer();
private static final ModifyConsumer MODIFY_CONSUMER = new ModifyConsumer();
private static final Map<Type, Consumer<ConsumerContext>> MAP = Stream.of(
new AbstractMap.SimpleEntry<>(Type.CREATE, CREATE_CONSUMER),
new AbstractMap.SimpleEntry<>(Type.MODIFY, MODIFY_CONSUMER)
).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
public static void consume(Type type, ConsumerContext context) {
Optional.ofNullable(MAP.get(type))
.orElseThrow(strategyMissing(type))
.accept(context);
}
private static Supplier<IllegalArgumentException> strategyMissing(Type type) {
return () -> new IllegalArgumentException("Type [" + type + "] not supported");
}
}
public static class CreateConsumer implements Consumer<ConsumerContext> {
@Override
public void accept(ConsumerContext consumerContext) {
throw new UnsupportedOperationException("Not implemented");
}
}
public static class ModifyConsumer implements Consumer<ConsumerContext> {
@Override
public void accept(ConsumerContext consumerContext) {
throw new UnsupportedOperationException("Not implemented");
}
}
public enum Type {
MODIFY, CREATE
}
public static class ConsumerContext {
}
}