17

我有一个依赖于 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 。还有其他选择吗?

谢谢!

4

3 回答 3

11

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

步骤 1. 封装全局引用

Michael Feathers 的精彩著作《有效地使用遗留代码》中也介绍了这种技术。

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

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

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

  public String getMemberOne() {
    return MEMBER_1;      
  }
}

然后你可以很容易地使用 Mockito 来模拟getMemberOne().

UPDATED Old Step 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:回答评论

请参阅我在此问题中的另一个答案。

于 2012-12-06T01:23:48.430 回答
2

更新 12/8:回答评论

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

这取决于。

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

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

  2. 除了使课程更容易测试之外,我还有其他好处吗?

  3. 我有时间吗?

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

但是,我们仍然可以打破所有类之间的依赖关系,并且FileReader只需进行最小的更改。

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

Michael Feathers 的精彩著作《有效地使用遗留代码》中也介绍了这种技术。

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

结尾。

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

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

于 2012-12-08T05:02:18.987 回答
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 测试一起使用。

于 2021-01-18T11:02:43.020 回答