【问题标题】:Intercepting object returned by private method in a public method在公共方法中拦截私有方法返回的对象
【发布时间】:2017-02-20 11:43:05
【问题描述】:

我需要对一个方法进行单元测试,并且我想模拟该行为,以便我可以测试方法中代码的必要部分。

为此,我想访问由我尝试测试的方法中的私有方法返回的对象。我创建了一个示例代码来大致了解我想要实现的目标。

主类

Class Main {
  public String getUserName(String userId) {
    User user = null;
    user = getUser(userId);
    if(user.getName().equals("Stack")) {
      throw new CustomException("StackOverflow");
    }

    return user.getName();

 }

 private User getUser(String userId) {
  // find the user details in database
  String name = ""; // Get from db
  String address = ""; // Get from db
  return new User(name, address);
 }
}

测试类

@Test (expected = CustomException.class)
public void getUserName_UserId_ThrowsException() {
  Main main = new Main();
  // I need to access the user object returned by getUser(userId)
  // and spy it, so that when user.getName() is called it returns Stack
  main.getUserName("124");
}  

【问题讨论】:

    标签: java junit mockito junit4 powermock


    【解决方案1】:

    访问私有只有两种方式:

    1. 使用反射
    2. 扩大范围
    3. 也许正在等待 Java 9 使用新的作用域机制?

    我会将范围修饰符从私有更改为包范围。使用反射对于重构来说是不稳定的。是否使用 PowerMock 之类的助手并不重要。它们只会减少反射周围的样板代码。

    但最重要的一点是,你不应该在 whitbox 测试中测试得太深。这会使测试设置爆炸。尝试将您的代码分割成更小的部分。

    方法“getUserName”需要来自用户对象的唯一信息是名称。它将验证名称并抛出异常或返回它。所以应该没有必要在测试中引入 User-object。

    所以我的建议是你应该将检索用户对象名称的代码提取到一个单独的方法中,并使这个方法包范围。现在不需要只模拟主对象的用户对象。但是该方法只有最少的信息才能正常工作。

    class Main {
    
        public String getUserName(String userId) {
            String username = getUserNameFromInternal(userId);
            if (userName.equals("Stack")) {
                throw new CustomException("StackOverflow");
            }
            return user.getName();
        }
    
        String getUserNameFromInternal(String userId) {
            User user = getUser(userId);
            return user.getName();
        }
    
        ...
    
    }
    

    测试:

    @Test (expected = CustomException.class)
    public void getUserName_UserId_ThrowsException() {
      Main main = Mockito.mock(new Main());
      Mockito.when(main.getUserNameInternal("124")).thenReturn("Stack");
      main.getUserName("124");
    }
    

    【讨论】:

    • 我帖子中的代码只是一个示例。想象一下 getUserName 以多种方式使用 User 对象的情况。假设我们在 getUserName 中调用了大约 15 个 User 对象的方法。但我只想模拟其中一个方法调用的行为。我该怎么办?
    • @bharathp 如果 one 方法正在对某个对象进行 15 次其他调用,那么这再次表明设计不佳并且有人不了解如何使用 OO; -)
    • 如果您在 getUserName 中调用 15 个方法,这些方法不会影响对方法进行测试的方法。但正如@GhostCat 提到的,这是一个糟糕设计的指标,你应该质疑你是否违反了单一责任原则。但是,将方法分割并进行测试仍然是潜在重构的第一步。
    • @GhostCat 这只是一个例子,也许是一个不好的例子:D 假设我正在尝试构建一个对象的 JSON 表示字符串,这需要我在我的buildJSON 方法。那也会被认为是糟糕的设计吗?假设我不能使用任何 json 库。
    【解决方案2】:

    您在私有方法中调用new 的问题。

    答案是不要求助于 PowerMock;或更改该方法的可见性。

    合理的答案是将对“给我一个用户对象的东西”的依赖“提取”到它自己的类中;并将该类的实例提供给您的“主”类。因为这样你就可以简单地模拟那个“工厂”对象;让它做任何你想做的事情。

    含义:您当前的代码很难测试。与其解决由此引起的问题,不如花时间学习如何编写易于测试的代码;例如通过观看这些videos 作为起点。

    鉴于您的最新评论:当您处理遗留代码时,您真的希望使用 PowerMockito。要理解的关键部分:您不要“嘲笑”该私有方法;您宁愿考虑嘲笑对new User() 的调用;如here所述。

    【讨论】:

    • 能否请您详细说明您的第三段。不幸的是,我正在测试的代码是遗留代码,我不会花时间更新它。非常感谢您提供指向播放列表的链接。
    • 我的意思是:如果您要在这里编写自己的代码;那么你可能想退后一步学习......如何以“更好”的方式做到这一点。但是在处理您无法触及的现有代码时,PowerMock 是您唯一的答案。请参阅我的更新答案及其第 4 段 ;-)
    • 我不认为这是遗留代码和“新”代码的问题。在这种情况下,问题是白盒测试或黑盒测试。问题是:您关心控制流,还是关心在不知道 HOW 的情况下产生特定结果的输入参数。如果你关心控制流,你就会隐含关心代码质量。如果您只想确保对象方法产生与规范匹配的结果,则没有真正的句柄来强制执行代码质量,因为每个控制流都成为“实现细节”。
    • 当然扩大范围一般不是一个好主意。但是,只要 JAVA 没有引入新的语言机制以在不扩展方法范围的情况下使白盒测试更容易,这是详细执行代码质量的唯一方法。您在测试情况下还有其他访问要求,而 JAVA 目前没有解决这个问题。代码质量不仅与对象依赖有关,还与控制流有关。黑盒测试本质上拒绝查看内部结构。
    【解决方案3】:

    您可以使用 PowerMock 的 mockPrivate,但我不推荐它。 如果你有这样的问题,通常意味着你的设计很糟糕。 为什么不保护方法?

    【讨论】:

    • 我不会完全说使方法受到保护可以提高代码质量,而是正确地模拟“从数据库获取”部分。
    • @Egor 我不确定我是否完全遵循,但模拟私有方法如何帮助我创建用户对象的间谍?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-14
    • 1970-01-01
    • 2014-11-20
    • 2013-04-05
    • 2014-12-31
    • 2011-07-21
    相关资源
    最近更新 更多