【问题标题】:How to mock static member variables如何模拟静态成员变量
【发布时间】:2012-11-23 17:45:58
【问题描述】:

我有一个依赖于 ClassToMock 的 ClassToTest 类。

public class ClassToMock {

  private static final String MEMBER_1 = FileReader.readMemeber1();

  protected void someMethod() {
    ...
  }
}

ClassToTest 的单元测试用例。

public class ClassToTestTest {
  private ClassToMock _mock;

  @Before
  public void setUp() throws Exception {
     _mock = mock(ClassToMock.class)
  }

}

在setUp()方法中调用mock时,FileReader.readMemeber1();被执行。有没有办法避免这种情况?我认为一种方法是在方法中初始化 MEMBER_1 。还有其他选择吗?

谢谢!

【问题讨论】:

  • 我认为有一些工具可以让你模拟出静态变量,但它们涉及很多技巧。因为这个类很难测试,听听测试想告诉你什么,让你的类更松耦合到FileReader
  • 问题:您是否只关心FileReader.readMember 被执行(即单元测试不应该命中文件系统),或者您是否还希望不同的测试具有不同的MEMBER_1 值?跨度>
  • 是的。我不希望 FileReader.readMember 被执行。这是另一个依赖项。
  • @pkrish 我在答案中添加了另一个步骤,这应该可以解决您的问题。

标签: unit-testing junit mocking mockito


【解决方案1】:

Powermock 核心提供了一种方便的实用方法,可用于此目的。

powermock-core 添加到您的项目中。

testImplementation group: 'org.powermock', name: 'powermock-core', version: '2.0.9'
FileReader fileReader = mock(FileReader.class);
Whitebox.setInternalState(ClassToMock.class, "MEMBER_1", fileReader);

Whitebox.setInternalState 只是一种使用反射设置字段值的便捷方法。因此它可以与任何 Mockito 测试一起使用。

【讨论】:

    【解决方案2】:

    您的ClassToMockFileReader 紧密结合,这就是您无法测试/模拟它的原因。而不是使用工具来破解字节码,以便您可以模拟它。我建议你做一些简单的重构来打破依赖。

    步骤 1. 封装全局引用

    Michael Feathers 的精彩书中也介绍了这种技术:Working Effectively with Legacy Code

    标题几乎是自我解释的。不是直接引用全局变量,而是将其封装在方法中。

    在您的情况下,ClassToMock 可以重构为:

    public class ClassToMock {
      private static final String MEMBER_1 = FileReader.readMemeber1();
    
      public String getMemberOne() {
        return MEMBER_1;      
      }
    }
    

    那么您就可以轻松地使用 Mockito 来模拟 getMemberOne()

    已更新 旧步骤 1 无法保证 Mockito 安全地模拟,如果 FileReader.readMemeber1() 抛出异常,那么测试将惨遭失败。所以我建议添加另一个步骤来解决它。

    步骤 1.5。添加 Setter 和 Lazy Getter

    因为问题是FileReader.readMember1(),所以一旦ClassToMock 被加载,就会被调用。我们必须推迟它。所以我们懒惰地调用FileReader.readMember1(),并打开一个setter。

    public class ClassToMock {
      private static String MEMBER_1 = null;
    
      protected String getMemberOne() {
        if (MEMBER_1 == null) {
          MEMBER_1 = FileReader.readMemeber1();
        }
        return MEMBER_1;      
      }
    
      public void setMemberOne(String memberOne) {
        MEMBER_1 = memberOne;
      }
    }
    

    现在,即使没有Mockito,您也应该可以伪造ClassToMock但是,这不应该是您的代码的最终状态,一旦您准备好测试,您应该继续第 2 步。

    步骤 2. 依赖注入

    一旦你准备好你的测试,你应该进一步重构它。现在而不是自己阅读MEMBER_1。这个类应该从外部接收MEMBER_1。您可以使用 setter 或构造函数来接收它。下面是使用setter的代码。

    public class ClassToMock {
      private String memberOne;
      public void setMemberOne(String memberOne) {
        this.memberOne = memberOne;
      }
    
      public String getMemberOne() {
        return memberOne;
      }
    }
    

    这两步重构真的很容易做到,即使手头没有测试也可以做到。如果代码不是那么复杂,你可以做第2步。然后你可以很容易地测试ClassToTest


    更新 12/8:回答评论

    在这个问题中查看我的另一个答案。

    【讨论】:

    • 如果可能的话,我会支持构造函数注入,因为它不会破坏 OOP 原则,并且不要让对象在创建后半初始化(使用无参数构造函数)。这些步骤仍然是要走的路!
    • 其实我发现step1不能保证Mocktiomock正确。我添加了另一个步骤来解决该问题。
    • @RangiLin 如果 FileReader 是非常基本的东西,比如 Logging,需要在每个类中都有。你会建议我在那里采用同样的方法吗?
    • @pkrish > 在这种情况下,我会建议其他方式。请阅读我的另一个答案。
    【解决方案3】:

    更新 12/8:回答评论

    问题:如果 FileReader 是非常基本的东西,比如 Logging,需要 在每个班级都在那里。你会建议我采用同样的方法吗 那里?

    视情况而定。

    在进行这样的大规模重构之前,您可能需要考虑一些事情。

    1. 如果我将FileReader 移到外面,我是否有一个合适的类可以从文件中读取并将结果提供给每个需要它们的类

      李>
    2. 除了让课程更易于测试之外,我还有其他好处吗?

    3. 我有时间吗?

    如果任何一个答案是“否”,那么你最好不要这样做。

    但是,我们仍然可以通过最小的更改来打破所有类和FileReader 之间的依赖关系。

    根据您的问题和评论,我假设您的系统使用 FileReader 作为从属性文件中读取内容的全局参考,然后将其提供给系统的其余部分。

    Michael Feathers 的精彩书中也介绍了这种技术:Working Effectively with Legacy Code,再次。

    步骤 1. 将 FileReader 静态方法委托给实例。

    改变

    public class FileReader {
      public static FileReader getMemberOne() {
        // codes that read file.
      }
    }
    

    public class FileReader {
      private static FileReader singleton = new FileReader();
      public static String getMemberOne() {
        return singleton.getMemberOne();
      }
    
      public String getMemberOne() {
        // codes that read file.
      }
    }
    

    通过这样做,FileReader 中的静态方法现在不知道如何getMemberOne()

    步骤 2. 从FileReader 提取接口

    public interface AppProperties {
      String getMemberOne();
    }
    
    public class FileReader implements AppProperties {
      private static AppProperties singleton = new FileReader();
      public static String getMemberOne() {
        return singleton.getMemberOne();
      }
    
      @Override
      public String getMemberOne() {
        // codes that read file.
      }
    }
    

    我们将所有方法提取到AppProperties,而FileReader 中的静态实例现在使用AppProperties

    第 3 步。静态设置器

    public class FileReader implements AppProperties {
      private static AppProperties singleton = new FileReader();
    
      public static void setAppProperties(AppProperties prop) {
        singleton = prop;
      }
    
      ...
      ...
    }
    

    我们在 FileReader 中打开了一个接缝。通过这样做,我们可以在FileReader 中设置更改底层实例,它永远不会注意到。

    第 4 步。清理

    现在FileReader 有两个职责。一种是读取文件并提供结果,另一种是为系统提供全局参考。

    我们可以将它们分开并给它们一个好的命名。结果如下:

    // This is the original FileReader, 
    // now is a AppProperties subclass which read properties from file.
    public FileAppProperties implements AppProperties {
      // implementation.
    }
    
    // This is the class that provide static methods.
    public class GlobalAppProperties {
    
      private static AppProperties singleton = new FileAppProperties();
    
      public static void setAppProperties(AppProperties prop) {
        singleton = prop;
      }
    
      public static String getMemberOne() {
        return singleton.getMemberOne();
      }
      ...
      ...
    }
    

    结束。

    在此重构之后,无论何时您想进行测试。您可以将模拟 AppProperties 设置为 GlobalAppProperties

    如果您想要做的只是打破许多类中的相同全局依赖关系,我认为这种重构会更好。

    【讨论】:

      猜你喜欢
      • 2018-07-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多