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