如何测试使用或不使用 Mocks 调用静态工厂方法的代码?

How do I test code calling static factory methods with or without Mocks?

我在尝试解决以下测试时遇到了难题。 class 是通过 Togglz 库创建的切换。我正在捕获功能管理器方法的执行,因为我正在使用 JDBCStateReporsitory 来读取切换值,如果数据库出现问题,我必须能够 return 切换的默认值 @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();
        }
    }

}

我不知道该怎么做,因为由内部实用程序静态方法创建的对象不允许我存根或模拟它。我刚刚创建了测试的 true 和 false 路径,但是试图覆盖异常路径的测试不起作用,向我抛出 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
    }

}

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

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

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

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 提供存根并检查所有交互。

好吧,首先你的测试不会像预期的那样 运行 因为你的切换 class 捕获 运行time 异常并且 IllegalStateException 是 运行时间异常,所以永远不会抛出。

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

因此,如果您不想 fiddle 在 Spock 内部使用 PowerMock - 模拟静态方法总是一种难闻的气味 - 您仍然可以选择让您的切换 class 更易于测试通过包范围的 setter 方法使功能管理器可注入,然后在测试中使用该方法。试试这个例子:

package de.scrum_master.Whosebug;

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.Whosebug

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 的肮脏技巧,但对于它的价值,这里是如何做的。

Java class再次删除了依赖注入,应该与OP的原始代码相同。

package de.scrum_master.Whosebug;

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 运行ner.

package de.scrum_master.Whosebug

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% 的代码覆盖率,但它并不能保证您的应用程序在生产中的行为符合预期,或者您正在测试正确的东西。