如何模拟静态成员变量

时间:2012-12-05 19:36:24

标签: unit-testing junit mocking mockito

我有一个类ClassToTest,它依赖于ClassToMock。

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。还有其他选择吗?

谢谢!

3 个答案:

答案 0 :(得分:11)

你的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后立即调用。我们要推迟它。所以我们懒洋洋地调用getter 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;
  }
}

现在,即使没有ClassToMock,您也应该制作假Mockito但是,这不应该是代码的最终状态,一旦准备好测试,就应该继续执行第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:回答评论

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

答案 1 :(得分:3)

更新12/8:回答评论

  

问题:如果FileReader是像Logging那样非常基本的东西,那该怎么办?   每个班级都在那里。你能建议我采用同样的方法吗?   有?

取决于。

在你做这样大规模的重构之前,你可能想要考虑一些事情。

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

  2. 除了让课程更容易测试外,我还能获得其他任何好处吗?

  3. 我有时间吗?

  4. 如果任何答案为“否”,那么你最好不要这样做。

    但是,我们仍然可以通过最少的更改来打破所有类和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将所有方法提取到FileReaderAppProperties中的静态实例。

    步骤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();
      }
      ...
      ...
    }
    

    END。

    重构后,无论何时你想测试。您可以将模拟AppProperties设置为GlobalAppProperties

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

答案 2 :(得分: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 测试一起使用。