scala 笔记(5)

unit test

在 Java 的世界里,unit test 的标准就是 junit,在 JUnit4 里面通过 annotation 标注方法(在 JUnit 3 里面常见的是继承),JUnit 提供 Runner 用来驱动所有的 test,这也使得其他的 framework 可以通过定制化自己的 Runner 实现一些特定的功能(如 Spring 通过 runner 使得 test 可以载入合适的 context XML),当然 JUnit 也提供了不少高级的功能,如

  • @Category 使得用户可以通过 annotation 对 test 分类,从而通过 Categories 这个 runner 仅仅执行指定类别的 test
  • JUnit 自己提供了一些简单的 assert,其本质是所谓的 Matcher,当然就会有人希望断言看起来跟自然语言类似,于是需要所谓的 DSL,JUnit 提供了对 hamcrest 的支持,例子见此
  • JUnit 提供了 Suit 这个 runner,可以指定 test、顺序
  • Parameterized 这个 runner 可以将 @Parameter 指定的一组数据喂给 @Test,这样获得的是参数个数乘以测试个数个测试
  • 通过 assumption 可以描述一些隐含的假定,或者揭示现有的 bug,assumption 失败的时候默认的 runner 会跳过这个 test(这样就不会 fail)
  • @Rule 是一些比较奇怪的东西,比如 TempraryFolder 可以创建临时文件,ExternalResource 可以初始化/释放某些资源,ErrorCollector 允许一个 test 里面出现指定的若干异常,TestName 可以在 @Test 里面获得这个 test 的函数名,Timeout 指定超时(与 @Test 标识的超时策略不大一样),ExpectedException(与 @Test 通过 expected 标识的不大一样,这个可以指定 error message),另有 ClassRule 和 RuleChain
  • Theories 这个 runner 相当于是指定一些 @DataPoint,然后进行逐一测试(@Theory)

了解了这些,其实 scala 也有自己的测试 package,org.scalatest,使用它的一个主要的好处自然是利用 scala 自身语法的优势,让 DSL 显得更接近自然语言,当然 org.scalatest 本身也是一个 unit test 的 package,这里有它的用户向导,如果希望和 JUnit 一起工作,可以通过 org.scalatest.junit.JUnitRunner 同时在 test 里面 mixin 某些 trait,通常是某些 suite,如 JUnitSuite。另外有一套所谓 test as specification 的测试通常用一些 Spec 这类 mixin

  • FlatSpec 是比较常见的 Spec,因为没有啥 nesting,所以写出来左侧按照缩进对其 =.=,结构是 behavior of “something”,之后跟 it should “如何如何” {} 即可
  • FunSpec 结构是 describe (“啥”){} 的嵌套结构,里面加上 it should “如何如何” {}
  • PropSpec 做 property check 用
  • WordSpec 结构为 “something” when { “” should {}}
  • FeatureSpec 这个比较烦,自己看文档吧

在一个 Java 的 project 里面如果 Runner 已经给了 Spring,那么 test case 里面常见的就是通过 mixin 来引入 scala 那些诡异的东西了。使用 BDD 的形式可能看起来美观点,不会到处都是 annotation。

case class 与 matching

case class 除了简化了 new 使得构造树状的结构(通过 factory method)更好看以外,默认构造函数的参数默认为 val,这样可以在外部引用这些参数,添加了 toString、equals、== 的实现,实现了 copy 可以用来做 deep copy。这样一来 case class 可以很方便的 model 封闭的继承结构,因此非常适合做 pattern matching(也就是说非 case class 不能做 matching?)。如果可以确定一个 abstract class 除了提供的几个 case class 以外不会被扩展,常见的做法是使用 sealed 关键字,使得本文件以外的文件无法继承这个 abstract class,这样在 case matching 的时候会给出更好的信息(因为这个限制使得编译器可以很容易检查是否漏掉了什么情况)。如果是一个 singleton 应该使用 case object。

要充分的理解 case class 干了些什么,你可以简单的看看下面的声明通过 scalac -print 被翻译成为了什么

abstract class foo
case object fooA extends foo
case object fooB extends foo
case class  fooC(c:Int) extends foo

非常简单几句话,摘录一些关键性的东西看看

abstract class foo extends java.lang.Object with ScalaObject {
  def this(): foo = {
    foo.super.this();
    ()
  }
};
final case object fooA extends foo with ScalaObject with Product with Serializable {
  def productIterator(): Iterator = scala.Product$class.productIterator(fooA.this);
  @deprecated("use productIterator instead", "2.8.0") def productElements(): Iterator = scala.Product$class.productElements(fooA.this);
  final override def hashCode(): Int = 3148859;
  final override def toString(): java.lang.String = "fooA";
  override def productPrefix(): java.lang.String = "fooA";
  override def productArity(): Int = 0;
  override def productElement(x$1: Int): java.lang.Object = {
    <synthetic> val temp1: Int = x$1;
    {
      throw new java.lang.IndexOutOfBoundsException(scala.Int.box(x$1).toString())
    }
  };
  override def canEqual(x$1: java.lang.Object): Boolean = x$1.$isInstanceOf[object fooA]();
  protected def readResolve(): java.lang.Object = fooA;
  def this(): object fooA = {
    fooA.super.this();
    scala.Product$class./*Product$class*/$init$(fooA.this);
    ()
  }
};
case class fooC extends foo with ScalaObject with Product with Serializable {
  def productIterator(): Iterator = scala.Product$class.productIterator(fooC.this);
  @deprecated("use productIterator instead", "2.8.0") def productElements(): Iterator = scala.Product$class.productElements(fooC.this);
  <caseaccessor> <paramaccessor> private[this] val c: Int = _;
  <stable> <caseaccessor> <accessor> <paramaccessor> def c(): Int = fooC.this.c;
  <synthetic> def copy$default$1(): Int = fooC.this.c();
  <synthetic> def copy(c: Int = c): fooC = new fooC(c);
  override def hashCode(): Int = ScalaRunTime.this._hashCode(fooC.this);
  override def toString(): java.lang.String = ScalaRunTime.this._toString(fooC.this);
  override def equals(x$1: java.lang.Object): Boolean = fooC.this.eq(x$1).||({
    {
      <synthetic> val temp3: java.lang.Object = x$1;
      if (temp3.$isInstanceOf[fooC]())
        {
          <synthetic> val temp4: fooC = temp3.$asInstanceOf[fooC]();
          <synthetic> val temp5: Int = temp4.c();
          val c$1: Int = temp5;
          if (fooC.this.gd1$1(c$1))
            body%03(c$1){
              x$1.$asInstanceOf[fooC]().canEqual(fooC.this)
            }
          else
            {
              false
            }
        }
      else
        {
          false
        }
    }
  });
 override def productPrefix(): java.lang.String = "fooC";
  override def productArity(): Int = 1;
  override def productElement(x$1: Int): java.lang.Object = {
    <synthetic> val temp6: Int = x$1;
    if (temp6.==(0))
      {
        scala.Int.box(fooC.this.c())
      }
    else
      {
        throw new java.lang.IndexOutOfBoundsException(scala.Int.box(x$1).toString())
      }
  };
  override def canEqual(x$1: java.lang.Object): Boolean = x$1.$isInstanceOf[fooC]();
  final <synthetic> private[this] def gd1$1(x$1: Int): Boolean = x$1.==(fooC.this.c());
  def this(c: Int): fooC = {
    fooC.this.c = c;
    fooC.super.this();
    scala.Product$class./*Product$class*/$init$(fooC.this);
    ()
  }
};
final <synthetic> object fooC extends scala.runtime.AbstractFunction1 with ScalaObject with Serializable {
  final override def toString(): java.lang.String = "fooC";
  case <synthetic> def unapply(x$0: fooC): Option = if (x$0.==(null))
    scala.this.None
  else
    new Some(scala.Int.box(x$0.c()));
  case <synthetic> def apply(c: Int): fooC = new fooC(c);
  protected def readResolve(): java.lang.Object = fooC;
  case <synthetic> <bridge> def apply(v1: java.lang.Object): java.lang.Object = fooC.this.apply(scala.Int.unbox(v1));
  def this(): object fooC = {
    fooC.super.this();
    ()
  }
};

简单的说 case class 生成了伴随的 object,这个 object 不仅提供了 apply(这意味着是一个 factory method),还提供了 unapply(这是为什么 case 可以将一个对象分解到构造函数的参数的原因),另外除了前面的若干优点还有实现了 Serializable 接口。注意标注的 final,但是似乎仍然可以用 class 继承,如果用 case class 编译似乎能通过但是给了 warning,一般似乎不建议继承 case class。从这个意义上来看,case class 不适合做一个正常的继承结构,而似乎接近于 Enum 的作用。

pattern matching 除了 case class 以外,还支持 constant、变量(需要用单撇引起来,类似 case object 也是的),如果 matching 的时候 bind 的两个变量有关系,我们需要用 if 作为 guardian,直接写两个变量为同一变量名是不会 work 的。注意做 type matching 的时候因为 type erasure,generic 写上去是没有用的。pattern matching 是有顺序的,因此写出来应该从 specific 到 general 的 rule 保证有包含关系的时候不会屏蔽掉某些 case,同时有 overlap 的时候需要注意先后顺序。

case class 常见的一个用例是 Option,其实诸如 List 之类的 collection 也都用到了这个吧。

——————
And, behold, I am with thee, and will keep thee in all places whither thou goest, and will bring thee again into this land; for I will not leave thee, until I have done that which I have spoken to thee of.

Advertisements
scala 笔记(5)

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s