如何使用Mockito模拟一个字符串?

43

我需要模拟一种测试情况,即调用String对象的getBytes()方法时会出现UnsupportedEncodingException。

我尝试使用以下代码来实现:

String nonEncodedString = mock(String.class);
when(nonEncodedString.getBytes(anyString())).thenThrow(new UnsupportedEncodingException("Parsing error."));

问题在于当我运行测试用例时,我会收到一个MockitoException的异常,指出我不能mock一个java.lang.String类。

是否有一种方法可以使用mockito来mock String对象,或者另外一种方式,使我的String对象在调用getBytes方法时抛出UnsupportedEncodingException异常?


以下是更多细节以说明问题:

这是我想要测试的类:

public final class A {
    public static String f(String str){
        try {
            return new String(str.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException e) {
            // This is the catch block that I want to exercise.
            ...
        }
    }
}

这是我的测试类(我正在使用JUnit 4和Mockito):

public class TestA {

    @Test(expected=UnsupportedEncodingException.class)
    public void test(){
        String aString = mock(String.class);
        when(nonEncodedString.getBytes(anyString())).thenThrow(new UnsupportedEncodingException("Parsing error."));
        A.f(aString);
    }
}

项目要求单元测试覆盖率百分比必须高于给定值。为了达到此覆盖率百分比,测试必须涵盖与 UnsupportedEncodingException 相关的 catch 块。 - Alceu Costa
11
如果你问我,现在是时候质疑那些项目需求了。测试覆盖率应该被明智地使用,就像所有指标一样。 - Nick Holt
17
+1 取消 duffymo 的负评。仅仅因为你不认为这样做有理由,并不意味着 OP 没有理由。 - Peter Recore
15个回答

48

问题在于Java中的String类被标记为final,因此您无法使用传统的mocking框架来模拟它。根据Mockito FAQ所述,这也是该框架的一个限制。


2
使用Mockito 2,你可以模拟final类 - Josh M.

13
如果你在catch块中要做的全部都是抛出运行时异常,那么你可以通过使用Charset对象来指定字符集名称,从而节省一些打字。
public final class A{
    public static String f(String str){
        return new String(str.getBytes(Charset.forName("UTF-8")));
    }
}
这样做可以避免因编译器提示而捕获永远不会发生的异常。

+1 绝对是最好的答案(除非使用早于1.6版本的JDK - 但在2016年这是不太可能的 - 因为getBytes(Charset)只在Java 6中添加)。 - Rogério

13

你可以创建一个带有错误编码名称的String,试试看。

public String(byte bytes[], int offset, int length, String charsetName)

模拟String几乎肯定是个坏主意。


7
正如其他人所指出的,您无法使用Mockito来模拟一个final类。然而,更重要的是这个测试并不特别有用,因为它只是表明String.getBytes()可能会抛出异常,这显然是肯定的。如果您十分关心测试这个功能,我想您可以向f()添加一个编码参数,并在测试中发送错误值。
同时,由于A是final且f()是静态的,您也会给调用者A.f()带来同样的问题。
本文可能对说服您的同事放低100%代码覆盖率的观点有所帮助:How to fail with 100% test coverage

6
根据其文档,JDave无法从引导类加载器加载的类中删除 "final" 修饰符。这包括所有JRE类(来自java.lang、java.util等)。
一个可以让您模拟任何东西的工具是JMockit
使用JMockit,您的测试可以编写为:
import java.io.*;
import org.junit.*;
import mockit.*;

public final class ATest
{
   @Test(expected = UnsupportedOperationException.class)
   public void test() throws Exception
   {
      new Expectations()
      {
         @Mocked("getBytes")
         String aString;

         {
            aString.getBytes(anyString);
            result = new UnsupportedEncodingException("Parsing error.");
         }
      };

      A.f("test");
   }
}

假设完整的"A"类是:
import java.io.*;

public final class A
{
   public static String f(String str)
   {
      try {
         return new String(str.getBytes("UTF-8"));
      }
      catch (UnsupportedEncodingException e) {
         throw new UnsupportedOperationException(e);
      }
   }
}

我实际在我的机器上执行了这个测试。(注意,我用运行时异常包装了原始的检查异常。)

我通过 @Mocked("getBytes") 进行了部分模拟,以防止 JMockit 模拟 java.lang.String 类中的所有内容(想象一下可能会发生什么)。

现在,这个测试确实是不必要的,因为 "UTF-8" 是一个标准字符集,在所有 JRE 中都需要支持。因此,在生产环境中,catch 块永远不会被执行。

然而,覆盖 catch 块的 "需求" 或者说愿望仍然是有效的。那么,如何在不降低覆盖率的情况下去掉测试呢?这是我的想法:在 catch 块的第一条语句中插入一行 assert false;,并让代码覆盖工具在报告覆盖率时忽略整个 catch 块。这是我对 JMockit 覆盖率的 "待办事项" 之一。8^)


3

您还可以使用PowerMock的Mockito扩展来模拟最终类/方法,即使是在系统类(例如String)中。但是,在这种情况下,我建议不要模拟getBytes,而是尝试设置您的期望,以便实际的String填充了预期数据而不是使用。


1
PowerMock无法模拟String。 - rapt

3

3

Mockito无法模拟 final 类。而 JMock 则可以结合 JDave 库实现。具体使用方法请参考这篇文章

JMock 并没有针对 final 类做任何特殊处理,只是依赖 JDave 库将 JVM 中的所有 final 类“解除”。因此,您可以尝试使用 JDave 的 unfinalizer,看看能否接着使用 Mockito 进行模拟。


1
这是一个项目要求,单元测试覆盖率百分比必须高于给定值。为了达到这样的覆盖率百分比,测试必须涵盖与UnsupportedEncodingException相关的catch块。
那么这个给定的覆盖目标是多少呢?有些人会说追求100%的覆盖率并不总是一个好主意
此外,这种方法无法测试catch块是否被执行。正确的方法是编写一个引发异常的方法,并将观察到异常被抛出作为成功标准。您可以使用JUnit的@Test注释来实现这一点,只需添加“expected”值即可:
@Test(expected=IndexOutOfBoundsException.class) public void outOfBounds() {
   new ArrayList<Object>().get(1);
}

该项目的代码覆盖率为95%。我不是做出这个决定的人... :-) 您可以使用JUnit + EMMA检查是否执行了catch块。 - Alceu Costa
1
是的,但如果您编写JUnit测试以显示在正确条件下引发异常,则无论Emma告诉您什么,您都将拥有更好的测试套件。重点是覆盖率之前要进行良好的测试。 - duffymo

1
你尝试过向getBytes(String)传递一个无效的charsetName吗?
你可以实现一个帮助方法来获取charsetName,并在测试中重写该方法以使用无意义的值。

问题在于我必须更改正在测试的类...但这可能是一个解决方案。我可以为选择字符集使用的属性创建一个setter,仅供测试目的。 - Alceu Costa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接