单元测试用SpringRunner可以吗?

Is it OK to use SpringRunner in unit tests?

我们正在与我的同事争论这种方法。他们说只能在集成或功能级别上使用 SpringRunner。

问题是在以下级别使用它的利弊是什么?

例如我有一个简单的bean:

public class RewardDurationCalculator {

    private Clock clock;

    public OptionalLong calculate(DurationType durationType, List<Pass> passes) {
        long now = Instant.now(clock).getEpochSecond();
        switch (durationType) {
            case FULL_PASS:
                return getCurrentPassDuration(passes, now);
            case TILL_THE_END_OF_THE_CURRENT_ACTIVE_PASS:
                return getTimeInCurrentPassLeft(passes, now);
        }
        return OptionalLong.empty();
    }

    private OptionalLong getCurrentPassDuration(List<Pass> passes, long now) {
        return passes.stream()
                .filter(currentPass(now))
                .mapToLong(Pass::getDuration)
                .findFirst();
    }

    private OptionalLong getTimeInCurrentPassLeft(List<Pass> passes, long now) {
        return passes.stream()
                .filter(currentPass(now))
                .mapToLong(pass -> getEndTs(pass) - now)
                .findFirst();
    }

    private Predicate<Pass> currentPass(long now) {
        return pass -> pass.getStartTs() >= now && now <= getEndTs(pass);
    }

    private long getEndTs(Pass pass) {
        return pass.getStartTs() + pass.getDuration();
    }

}

那是在做一些计算逻辑。为此,我还有 spring 配置:

@Configuration
public class RewardDurationCalculatorConfiguration {

    @Bean
    public RewardDurationCalculator rewardDurationCalculator(Clock clock) {
        return new RewardDurationCalculator(clock);
    }

}

那么为什么我不能像这样为它编写单元测试:

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = RewardDurationCalculatorConfiguration.class)
public class RewardDurationCalculatorTest {

    @MockBean
    private Clock clock;
    @Autowired
    private RewardDurationCalculator rewardDurationCalculator;

    @Test
    public void testCalculateCurrentPassDurationShouldBeReturnedIfPassWasCreatedRightNow() {
        rewardDurationCalculator.calculate(DurationType.FULL_PASS, Collections.emptyList());
    }

}

使用这种方法我会面临哪些缺点?

我倾向于同意你同事的观点。

单元测试应该只测试小的代码单元,通常是单个 class。他们应该只执行被测单元而不执行单元依赖项中的任何代码。

这样做的一个原因是单元测试应该尽可能快地执行。在测试驱动开发 (TDD) 中,您希望能够 运行 套件,或至少它的一个相关子集,通常,在您做出每一个小的更改之后,以验证您没有破坏任何现有的行为。这只有在测试反馈是即时的时才可行。如果您必须等待测试结果的时间过长,那么您 运行 获得结果的频率就会降低,这意味着反馈循环会变长。

在较小的 Spring 项目中,Spring 上下文加载速度非常快,因此您可能认为没有太大区别,但如果您 运行 经常进行测试,甚至短暂的延迟加起来。在大型和较旧的代码库中,可能需要一段时间来提升 Spring 上下文,并且在某些时候它会变得如此缓慢,以至于您不想 运行 整个测试套件一次以上天,这会大大损害您练习 TDD 的能力。

坚持测试小代码单元的另一个方面是,它迫使您将代码结构化为高度解耦(即可测试)的单元。如果您不能为 class 编写仅执行 class 而没有其他任何内容的单元测试,则可能暗示有机会改进代码的设计。

总而言之,当你为测试提升整个 Spring 上下文时,根据这个定义,它不再是单元测试,而是集成测试,因为你也在测试整个 Spring 配置、引导、自动装配等,i。 e.将您的 classes 集成到 Spring 应用程序中。

如果你class使用任何bean依赖,那么应该使用@SpringRunner,如果你class是独立的,没有任何应用程序上下文依赖,最好不要使用@SpringRunner。如果我看到你 class 'RewardDurationCalculator' 它不使用任何依赖项,它不应该使用 @SpringRunner 甚至事实上 Mocks...

进一步阅读:

  1. https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/test/context/junit4/SpringRunner.html