【问题标题】:@SuppressStaticInitializationFor partial mocking@SuppressStaticInitializationFor 部分模拟
【发布时间】:2019-03-09 21:33:54
【问题描述】:

我有一个奇怪的案例,我想测试“某些”功能而不触及另一个……我很难选择合适的描述,我希望我将在下面展示的代码几乎是自我描述的。

假设我有一个保留一些策略的类:

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 在其上调用consume

现在的问题是:我非常想测试我关心的所有策略是否都已注册,我可以在它们上调用accept - 这就是我想要测试的全部内容。

通常,我会在TypeStrategy 上使用@SuppressStaticInitializationFor,而使用WhiteBox::setInternalState 只会为CREATE_CONSUMERMODIFY_CONSUMER 添加我需要的任何内容;但在这种情况下我不能,因为MAP 也会被跳过,我真的不希望这样,我只关心这两个策略 - 我需要 MAP保持原样。

除了一些令人讨厌的重构之外,这确实让我达到了我想成为的地方,我不知道如何才能实现这一目标。在最好的情况下,我希望@SuppressStaticInitializationFor 支持一些“部分”跳过,您可以在其中指定一些过滤器来确定您想要跳过的内容,但这不是一个选项,真的。

我还可以测试调用链上的“所有内容”——即测试 accept 应该做的所有事情,但这会在此测试中增加近 70 行模拟,理解这一点将成为一场噩梦它真的想测试一个很小的部分。

【问题讨论】:

  • 如何为您的消费者提供mocking the constructors,并检查与他们的互动?
  • @Morfic 这确实是答案,如果你用一个小例子来回答,会很高兴接受它

标签: testing java-8 powermock


【解决方案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 {
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-02-15
    • 1970-01-01
    • 2017-12-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-23
    • 1970-01-01
    相关资源
    最近更新 更多