如何在有或没有Mocks的情况下测试调用静态工厂方法的代码?

时间:2018-01-09 01:36:51

标签: java testing groovy tdd spock

我试图解决以下测试的困境。该类是通过Togglz库创建的切换。我正在捕捉特征管理器方法执行,因为我使用JDBCStateReporsitory来读取切换值,如果DB出现问题,我必须能够使用{{返回切换的默认值。 1}}注释。

@EnabledByDefault

我不知道怎么做,因为内部实用程序静态方法创建的对象不允许我Stub或Mock它。我刚刚创建了测试的真假路径,但试图覆盖异常路径的测试并没有给我留下@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

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

2 个答案:

答案 0 :(得分:1)

我认为原因在于,因为方法import HighCharts from 'highcharts/highstock'; import React, { Component } from 'react'; class Charts extends Component { render() { var data = [ [Date.UTC(2006, 0, 29, 0, 0, 0), 30.14], [Date.UTC(2006, 0, 29, 1, 0, 0), 34.76], [Date.UTC(2006, 0, 29, 2, 0, 0), 34.34], [Date.UTC(2006, 0, 29, 3, 0, 0), 33.9] ]; return ( HighCharts.stockChart('root', { rangeSelector: { selected: 0 }, title: { text: 'USD to EUR exchange rate' }, tooltip: { style: { width: '100%' }, valueDecimals: 4, shared: true }, yAxis: { title: { text: 'Exchange rate' } }, series: [{ name: 'USD to EUR', data:data, id: 'dataseries' }] }) )} } export default Charts; 是静态的,所以如果你在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(); ... } } 提供存根并检查所有交互。

答案 1 :(得分:1)

好吧,首先你的测试不能按预期运行,因为你的toggle类捕获了运行时异常而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。我的测试覆盖率工具也显示它有效:

Test coverage

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

好的,我需要PowerMock是出于另一个原因,并迅速给你的代码一个旋转。

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

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而不是Stub或{ {1}}。这是因为您的代码捕获了功能管理器(FM)抛出的异常,但随后在catch块中再次使用FM。这可能很危险,因为如果FM由于网络或数据库故障而中断,它可能也会在您下次呼叫时失败。因此,虽然测试为您提供100%的代码覆盖率,但它并不能保证您的应用程序在生产中的行为符合预期,或者您正在测试正确的东西。