代码之家  ›  专栏  ›  技术社区  ›  Eugene

@部分模拟的取消静态初始化

  •  0
  • Eugene  · 技术社区  · 6 年前

    我有一个奇怪的例子,我想测试“一些”功能,而不去碰另一个。。。我很难选择一个合适的描述,我希望下面的代码是自我描述的。

    假设我有一门课有一些策略:

    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 只需尝试找到正确的注册类型并调用 消费 上面有供应的 ConsumerContext .

    现在的问题是:我非常想测试我关心的所有策略都注册了,并且我可以调用 accept 在他们身上-这就是我真正想要测试的。

    通常,我会用 @SuppressStaticInitializationFor TypeStrategy 使用 WhiteBox::setInternalState 我需要什么就放什么 CREATE_CONSUMER MODIFY_CONSUMER ;但在这种情况下我不能,因为 MAP 也会被跳过,我真的不想这样,我只关心这两个策略- 需要 这个 地图 保持现状。

    除了一些讨厌的重构,这确实让我有点想去哪里,我不知道如何才能做到这一点。在最好的情况下我希望 @取消静态初始化 会支持一些“部分”跳过,在这里你可以指定一些过滤器,你到底想跳过什么,但那不是一个选项,真的。

    我也可以测试电话链上的“所有”其他东西-也就是测试 接受 是应该这样做的,但这增加了接近70行的嘲笑在这个测试,它成为一个噩梦,以了解它真的想测试一个非常小的一块。

    0 回复  |  直到 6 年前
        1
  •  1
  •   Morfic    6 年前

    从您的描述来看,黑盒测试似乎不是一个选项,所以也许我们可以通过模拟用户的构造函数并验证其交互来依赖一些白盒测试。

    在下面,您可以找到从初始样本推断出的完整示例,包括 .orElseThrow(strategyMissing(type)) .

    TypeStrategy 完好无损,这意味着将执行映射的静态初始化块。因此,我们需要特别注意消费者模拟实例。我们需要确保在初始模拟阶段添加到映射中的相同模拟实例在所有测试中都可用,否则验证将失败。因此,我们不会为每个测试创建mock,而是为所有测试创建一次mock。虽然在单元测试中不建议这样做(测试应该是独立的和独立的),但我相信在这种特殊情况下,这是一个可以接受的不错的折衷方案。

    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 {
        }
    }