【问题标题】:How do I test code calling static factory methods with or without Mocks?如何在有或没有 Mocks 的情况下测试调用静态工厂方法的代码?
【发布时间】:2018-01-09 01:36:51
【问题描述】:

我在尝试解决以下测试时遇到了两难境地。该类是通过 Togglz 库创建的切换。我正在捕获功能管理器方法执行,因为我正在使用 JDBCStateReporsitory 来读取切换值,如果数据库出现问题,我必须能够使用 @EnabledByDefault 注释返回切换的默认值。

@Slf4j
public enum PocToggle {

    @EnabledByDefault
    USE_MY_FEATURE;

    public boolean isActive() {

        FeatureManager featureManager = FeatureContext.getFeatureManager();

        try {
            return featureManager.isActive(this);
        } catch (RuntimeException ignored) {
            if (log.isWarnEnabled()) {
                log.warn(String.format("Failed to retrieve feature '%s' state", this.name()));
            }

            FeatureMetaData metaData = featureManager.getMetaData(this);
            FeatureState featureState = metaData.getDefaultFeatureState();

            return featureState.isEnabled();
        }
    }

}

我不知道该怎么做,因为由内部实用程序静态方法创建的对象不允许我存根或模拟它。我刚刚创建了测试的真假路径,但试图覆盖异常路径的测试不起作用,向我抛出了 Expected exception of type 'java.lang.IllegalStateException', but no exception was thrown 消息。

class PocToggleSpecification extends Specification {

    @Rule
    private TogglzRule toggleRule = TogglzRule.allEnabled(PocToggle.class)

    def "Should toggle to use my feature when it is enabled"() {

        when:
        toggleRule.enable(USE_MY_FEATURE)

        then:
        USE_MY_FEATURE.isActive()
    }

    def "Should toggle to not to use my feature when it is disabled"() {

        when:
        toggleRule.disable(USE_MY_FEATURE)

        then:
        !USE_MY_FEATURE.isActive()
    }

    def "Should throw an exception when something goes wrong"() {

        given:
        toggleRule.enable(USE_MY_FEATURE)

        FeatureManager featureManager = Stub()
        featureManager.isActive() >> { throw new IllegalStateException() }

        def featureContext = Spy(FeatureContext)
        featureContext.getFeatureManager() >> featureManager

        when:
        USE_MY_FEATURE.isActive()

        then:
        thrown IllegalStateException
    }

}

你能帮我解决这种测试吗?

【问题讨论】:

  • 我希望您不介意我编辑了您的主题,以便更好地反映问题的含义。 “我如何解决这种测试”迫使每个人阅读您的问题并分析您的问题,以了解该问题是否与他们相关。
  • NP,我打算这样做,但我看不到在哪里编辑标题。谢谢。

标签: java testing groovy tdd spock


【解决方案1】:

首先,您的测试不会按预期运行,因为您的切换类捕获了运行时异常,而IllegalStateException 是运行时异常,所以它永远不会被抛出。

其次,Spock 不能模拟 Java 类的静态方法,只能模拟 Groovy 类。

因此,如果您不想在 Spock 中摆弄 PowerMock - 模拟静态方法总是难闻的 - 您仍然可以选择通过使功能管理器可通过包范围注入来使切换类更具可测试性setter 方法,然后从测试中使用该方法。试试这个例子:

package de.scrum_master.stackoverflow;

import org.togglz.core.Feature;
import org.togglz.core.annotation.EnabledByDefault;
import org.togglz.core.context.FeatureContext;
import org.togglz.core.manager.FeatureManager;
import org.togglz.core.metadata.FeatureMetaData;
import org.togglz.core.repository.FeatureState;

public enum PocToggle implements Feature {
  @EnabledByDefault
  USE_MY_FEATURE;

  private FeatureManager customFeatureManager;

  void setFeatureManager(FeatureManager featureManager) {
    this.customFeatureManager = featureManager;
  }

  public boolean isActive() {
    FeatureManager featureManager = customFeatureManager != null
      ? customFeatureManager
      : FeatureContext.getFeatureManager();

    try {
      return featureManager.isActive(this);
    } catch (RuntimeException ignored) {
      System.err.println(String.format("Failed to retrieve feature '%s' state", this.name()));
      FeatureMetaData metaData = featureManager.getMetaData(this);
      FeatureState featureState = metaData.getDefaultFeatureState();
      return featureState.isEnabled();
    }
  }
}
package de.scrum_master.stackoverflow

import org.junit.Rule
import org.togglz.junit.TogglzRule
import org.togglz.testing.TestFeatureManager
import spock.lang.Specification

import static PocToggle.USE_MY_FEATURE

class PocToggleTest extends Specification {
  @Rule
  TogglzRule toggleRule = TogglzRule.allEnabled(PocToggle.class)

  def "Feature is active when enabled"() {
    when:
    toggleRule.enable(USE_MY_FEATURE)

    then:
    USE_MY_FEATURE.isActive()
  }

  def "Feature is inactive when disabled"() {
    when:
    toggleRule.disable(USE_MY_FEATURE)

    then:
    !USE_MY_FEATURE.isActive()
  }

  def "Feature defaults to active upon feature manager error"() {
    setup: "inject error-throwing feature manager into Togglz rule"
    def featureManagerSpy = Spy(TestFeatureManager, constructorArgs: [PocToggle]) {
      isActive(_) >> { throw new IllegalStateException() }
    }

    when: "feature is disabled and feature manager throws an error"
    toggleRule.disable(USE_MY_FEATURE)
    USE_MY_FEATURE.featureManager = featureManagerSpy

    then: "feature is reported to be active by default"
    USE_MY_FEATURE.isActive()

    cleanup: "reset Togglz rule feature manager"
    USE_MY_FEATURE.featureManager = null
  }
}

运行最后一个测试,你会看到如预期的日志消息Failed to retrieve feature 'USE_MY_FEATURE' state。我的测试覆盖率工具也显示它有效:


2018-01-17 更新:使用 PowerMock 的解决方案变体(使用 1.6.6 和 1.7.3 测试)

好的,我需要 PowerMock 是出于另一个原因,并很快让您的代码进行了测试。

免责声明:我确实更喜欢上面的第一个解决方案,即重构依赖注入而不是使用 PowerMock 的肮脏技巧,但是对于它的价值,这里是如何做到的。 p>

Java类又去掉了依赖注入,应该和OP的原代码一样。

package de.scrum_master.stackoverflow;

import org.togglz.core.Feature;
import org.togglz.core.annotation.EnabledByDefault;
import org.togglz.core.context.FeatureContext;
import org.togglz.core.manager.FeatureManager;
import org.togglz.core.metadata.FeatureMetaData;
import org.togglz.core.repository.FeatureState;

public enum PocToggle implements Feature {
  @EnabledByDefault
  USE_MY_FEATURE;

  public boolean isActive() {
    FeatureManager featureManager = FeatureContext.getFeatureManager();

    try {
      return featureManager.isActive(this);
    } catch (RuntimeException ignored) {
      System.err.println(String.format("Failed to retrieve feature '%s' state", this.name()));
      FeatureMetaData metaData = featureManager.getMetaData(this);
      FeatureState featureState = metaData.getDefaultFeatureState();
      return featureState.isEnabled();
    }
  }
}

请参阅PowerMock wiki 了解说明。顺便说一句,Sputnik 是 Spock 的 JUnit 跑步者。

package de.scrum_master.stackoverflow

import org.junit.Rule
import org.junit.runner.RunWith
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.powermock.modules.junit4.PowerMockRunnerDelegate
import org.spockframework.runtime.Sputnik
import org.togglz.core.context.FeatureContext
import org.togglz.junit.TogglzRule
import org.togglz.testing.TestFeatureManager
import spock.lang.Specification

import static PocToggle.USE_MY_FEATURE
import static org.powermock.api.mockito.PowerMockito.mockStatic
import static org.powermock.api.mockito.PowerMockito.when

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([FeatureContext.class])
class PocToggleTest extends Specification {
  @Rule
  TogglzRule toggleRule = TogglzRule.allEnabled(PocToggle.class)

  // ...

  def "Feature defaults to active upon feature manager error (power-mocked)"() {
    setup: "inject error-throwing feature manager into Togglz rule"
    def featureManagerSpy = Spy(TestFeatureManager, constructorArgs: [PocToggle]) {
      isActive(_) >> { throw new IllegalStateException() }
    }
    mockStatic(FeatureContext)
    when(FeatureContext.getFeatureManager()).thenReturn(featureManagerSpy)

    when: "feature is disabled and feature manager throws an error"
    toggleRule.disable(USE_MY_FEATURE)

    then: "feature is reported to be active by default"
    USE_MY_FEATURE.isActive()
  }
}

更新 2 (2018-01-19): 我想再提一个问题:也许您注意到我使用 Spy 而不是 StubMock。这是因为您的代码捕获了功能管理器 (FM) 抛出的异常,但随后在 catch 块中再次使用了 FM。这可能很危险,因为如果 FM 由于网络或数据库故障而损坏,它也可能在您下次调用时失败。因此,虽然测试为您提供了 100% 的代码覆盖率,但它并不能保证您的应用程序在生产环境中的行为符合预期,或者您正在测试正确的东西。

【讨论】:

  • 更新: 添加了 PowerMock 示例
  • 非常感谢@kriegaex,我会分析您的答案,如果适用,则标记为已解决。你知道,我现在有很多工作;)。
  • 我们都有很多工作要做,加布里埃尔。这是专业人士对专业人士的帮助。如果你有足够的时间提出问题并让其他人利用他们的空闲时间来回答,你应该表现出足够的尊重,并在合理的时间内检查答案。我相信你会的,祝你好运。
  • 我不明白你的激进回答,因为我没有说我会花半年时间来检查你的答案,但无论如何。我对你的技术回答的问题是:总是当你使用一个实用程序时,你会为这种测试使用注入,或者只是为了这种情况。如果您总是将响应注入此实用程序,我无法达到使用实用程序的想法。谢谢您,感谢您抽出宝贵的时间来回答。
  • 围绕你的答案旋转并对FeatureContext.getFeatureManager() 调用进行更好的分析,我可以看到这个类更像是一个工厂,而不仅仅是一个实用程序,所以你的答案很有意义。谢谢@kriegaex,这对我帮助很大。
【解决方案2】:

我认为原因是因为 FeatureContext.getFeatureManager() 方法是静态的,所以你不能放置间谍,如果你在测试中的 FeatureManager 存根的创建行放置一个断点并检查在调试器中取出创建对象的地址,然后在尝试使用此功能管理器的测试中的一行中放置一个断点,您会看到这是两个不同的对象,因此抛出异常的期望不适用在这里。

就解决方案而言: 我建议尽可能多地摆脱静态代码,因为它不是真正的单元测试。例如,您可以创建一个这样的接口(好的,您在这里使用枚举,但它可以很容易地重构为从枚举中使用的类,以便您可以对其进行测试):

public interface FeatureManagerProvider {
    FeatureManager getFeatureManager();
}

public class DefaultFeatureManagerProviderImpl implements FeatureManagerProvider {
     .... // inject this in real use cases
}

有了这个抽象,代码可以这样重构:

public class Activator {
  private FeatureManagerProvider featureManagerProvider;
  public Activator(FeatureManagerProvider provider) {
      this.featureManagerProvider = provider;
  }

  public boolean isActive() {
       ...
       FeatureManager fm = featureManagerProvider.getFeatureManager();
       ...  
  }
}

在测试期间,您可以为FeatureManagerProvider 提供一个存根并检查所有交互。

【讨论】:

  • 非常感谢@Mark Bramnik,我会分析您的答案,如果适用,则标记为已解决。 =]
  • 我对你的回答的问题是:这不是为了使用实用程序而过度设计的吗,谢谢。
  • 我无法真正回答您的问题,因为过度工程是主观的。我能说的是,在许多现代语言中,不支持或至少不鼓励使用静态作为概念。我也可以说静态实用程序通常不利于测试使用它们的代码(除非您不介意在测试代码中调用这些实用程序。对我来说,更容易测试事物的能力足以不使用静态方法,但又是一种主观意见
  • 通常编写可测试代码意味着您使用依赖注入。所以通常你有一个 DI 容器(如 spring、guice 等)。所以它管理一个非静态版本的代码,提供一个干净的生命周期,它只实例化代码一次。静态实用程序根本没有生命周期:它们什么时候首先实例化?静态的东西什么时候“死”?谁知道...在 DI 版本中,您有一个接口(可选)和一个由 DI 容器管理的实现
  • IMO 马克是对的,加布里埃尔。他的回答和我的回答都朝着同一个方向发展。因此,虽然您使用的工具 Togglz 使用静态方法不是您的错,但至少您可以通过 DI 将您的代码与它分离。 (但这也适用于非静态依赖项。)尽管我在编辑的答案中也提供了 PowerMock 解决方案,但正如我所说,我认为这是一种气味而不是一件好事。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-08-05
  • 2017-12-11
  • 2012-11-03
  • 1970-01-01
  • 1970-01-01
  • 2018-07-08
相关资源
最近更新 更多