27 August 2011

这个题目对搜索引擎应该是比较友好的,虽然土了些。

PowerMock是个好东西,在某种程度上拯救了架构设计,使得我们不用过分考虑如何用复杂的架构设计来达到可以单元测试的目的。

Java里面如何使用PowerMock就不用多说了,随便google一下满屏幕都是,如果是同事还可以跟我要codekata的材料。但是这事儿换成Scala,情况就完全不同了。

首先要解决的问题是Scala用什么测试框架。比较流行的有ScalaTestSpecs,二者都支持BDD、ATDD,也就是这个BDD、ATDD把我折磨了一个晚上。

撇开ATDD不谈,我非常想认真尝试一下BDD,但是ScalaTest的BDD需要使用它自己的Runner,Specs的虽然支持JUnit,但是也需要用一个特殊的Runner,这对于PowerMock来说几乎是致命的。PowerMock+Junit,必须使用PowerMockRunner才能mockStatic,mockNew,PowerMock+TestNG倒是不需要Runner,但是ScalaTest不支持用TestNG来做BDD,而Specs根本就不支持TestNG。

有点儿混乱,总之一句话,现阶段,Scala+BDD+PowerMock这条路完全走不通。如果我的判断有问题,望指正。

那么就退一步,还是JUnit。从一个例子来说明我遭受到的痛苦以及整个解决过程。

1
2
3
4
5
6
7
8
9
10
11
12
public class MyStatic {
    public static String hello() {
        // connect to twitter for example
        return tweet;
    }
}
 
public class HelloWorld {
    public String helloWorld() {
        return MyStatic.hello();
    }
}

上面一段代码,Java+PowerMock很容易处理:

1
2
3
4
5
6
7
8
9
@RunWith(PowerMockRunner.class)
@PrepareForTest(MyStatic.class)
public HelloWorldTest {
    @Test
    public testHelloWorld() {
        mockStatic(MyStatic.class);
        ....
    }
}

如果是Scala,问题就接踵而至了。先看功能相似的Scala代码片段:

1
2
3
4
5
6
7
8
9
10
object MyStatic {
  def hello = {
    // connect to twitter for example
    tweet
  }
}
 
class HelloWorld {
  def helloWorld() = MyStatic.hello
}

@RunWith没有问题:

1
@RunWith(classOf[PowerMockRunner])

@PrepareForTest就开始出状况了。

首先貌似Scala对annotation的解释有些不太好,不能简单地传一个东西给@PrepareForTest,因为这玩意儿有两个参数,每个都是数组

1
2
3
4
5
6
7
8
9
@Target( { ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface PrepareForTest {
    Class[] value() default IndicateReloadClass.class;
 
    String[] fullyQualifiedNames() default "";
}

所以不能:

1
@PrepareForTest(classOf[SomeClass])

必须要:

1
@PrepareForTest(Array(classOf[SomeClass]))

这个还算是比较容易解决,但是对于Scala里的object,情况又有变化。object不支持classOf这个运算符,classOf[MyStatic]是非法的;MyStatic.getClass倒是可以拿到类,但这个类是另一个,名字是MyStatic$(后面会详细解释这两个类以及它们之间的关系)。但即使getClass得到的类是正确的,也没法直接传给annotation,因为annotation要求参数必须是一个constant,不能是一个方法调用,因此@PrepareForTest(Array(MyStatic.getClass))也是不行的。

怎么办呢?还好@PrepareForTest还有第二个参数,我们可以这样:

1
@PrepareForTest(fullyQualifiedNames = Array("com.honnix.test.MyStatic"))

虽然比较恶心,而且不利于重构,但是聊胜于无吧。

然后就是mockStatic的问题。怎么写呢?

1
mockStatic(MyStatic.getClass)

这肯定不行,因为之前说了getClass得到的类是MyStatic$。那么怎么样才能mock MyStatic呢?答案就是,反射。

1
mockStatic(Class.forName("com.honnix.test.MyStatic"))

没有其他任何办法得到一个object的class信息,这里有一个很古老的bug

好吧,两个问题都“解决”了,跑跑看。结果却是我想mock的东西根本就没有被mock。要命了。

事情还得从头说起。Scala对于一个object编译的结果是生成两个类,一个MyStatic,一个MyStatic$:

1
2
3
4
5
6
7
public final class MyStatic
{
  public static final String hello()
  {
    return MyStatic$.MODULE$.hello();
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public final class MyStatic$
  implements ScalaObject
{
  public static final  MODULE$;
 
  static
  {
    new ();
  }
 
  public String hello()
  {
    return "hello";
  }
 
  private MyStatic$()
  {
    MODULE$ = this;
  }
}

这是反汇编的结果,稍微有点乱,凑合着看吧。不用多解释,代码已经很明白了。

问题是,我对MyStatic进行mock,理论上说也应该可以的,也就是mock之后就无视MyStatic$了。为什么不行呢?还得来看看调用地方的反汇编代码:

1
2
3
4
5
public testHelloWorld {
    ...
    MyStatic$.MODULE$.hello();
    ...
}

这里面压根儿就没有MyStatic的事儿了!MyStatic被华丽的忽视了!

到此为止终于真相大白了。咱们得这样来mock。

1
2
3
4
5
6
7
8
9
10
@RunWith(classOf[PowerMockRunner])
@PrepareForTest(fullyQualifiedNames = Array("com.honnix.test.MyStatic$"))
class HelloWorldTest {
  @Test
  def testStartAndStop() {
    val mockMyStatic = mock(MyStatic.getClass)
    Whitebox.setInternalState(MyStatic.getClass, mockMyStatic)
    ...
  }
}

想法很简单,把MyStatic$里面的MODULE$给换喽。

代码很丑陋,所以需要强调一下PowerMock应该被用在那些确实不需要复杂的依赖注入的地方来帮助简化架构设计,对于需要做依赖注入的地方,我们还是绝对不能松懈的,不能因为PowerMock啥都能mock就乱写代码。

顺便说一句,Specs对于Mockito已经有很好的支持了,加上Scala那种类似DSL的表达式,用起来很爽。希望PowerMock的作者能考虑支持Scala。



blog comments powered by Disqus