当前位置: 首页 > 工具软件 > Scala Check > 使用案例 >

scala 单元测试_Scala中的法律测试简介

姬凡
2023-12-01

scala 单元测试

Property-based law testing is one of the most powerful tools in the scala ecosystem. In this post, I’ll explain how to use law testing and the value it’ll give you using in-depth code examples.

基于财产的法律测试是scala生态系统中最强大的工具之一。 在这篇文章中,我将通过深入的代码示例来说明如何使用法律测试,以及它将为您带来的价值。

This post is aimed for Scala developers who want to improve their testing knowledge and skills. It assumes some knowledge of Scala, cats, and other functional libraries.

这篇文章面向想要提高测试知识和技能的Scala开发人员。 它假定您对Scala,猫和其他功能库有所了解。

介绍 (Introduction)

  • You might be familiar with types which are a set of values (for example Int values are : 1,2,3… String values are : “John Doe” etc).

    您可能熟悉作为一组值的类型(例如,Int值是: 1,2,3 …字符串值是: “John Doe”等)。

  • You also might be familiar with functions which is a mapping from Input type to Output type.

    您可能还熟悉从输入类型到输出类型的映射的函数。
  • A property is defined on a type or a function and describes its desired behavior.

    属性是在类型或函数上定义的,并描述其所需的行为。

So what is a Law? Keep on reading!

那么什么是法律? 继续阅读!

一个具体的例子 (A concrete example)

Here is our beloved Person data type:

这是我们心爱的Person数据类型:

case class Person(name: String, age: Int)

And serialization code using the Play-Json , a library that allows transforming your Person type into JSON :

以及使用Play-Json序列化的代码,该库允许将您的Person类型转换为JSON :

val personFormat: OFormat[Person] = new OFormat[Person] {
  override def reads(json: JsValue): JsResult[Person] = {
    val name = (json \ "name").as[String]
    val age = (json \ "age").as[Int]
    JsSuccess(Person(name, age))
  }
override def writes(o: Person): JsObject =
    JsObject(Seq("name" -> JsString(o.name), 
                 "age" -> JsNumber(o.age)))
}

We can now test this serialization function on a specific input like this:

现在,我们可以在特定的输入上测试此序列化功能,如下所示:

import org.scalatest._
class PersonSerdeSpec extends WordSpecLike with Matchers {
  "should serialize and deserialize a person" in {
    val person = Person("John Doe", 32)
    val actualPerson =
      personFormat.reads(personFormat.writes(person))
    actualPerson.asOpt.shouldEqual(Some(person))
  }
}

But, we now need to ask ourselves, whether all people will serialize successfully? What about a person with invalid data (such as negative age)? Will we want to repeat this thought process of finding edge-cases for all our test data?

但是,我们现在需要问自己,是否所有人都会成功进行序列化? 一个数据无效的人(例如负数年龄)怎么办? 我们是否要重复为所有测试数据寻找边缘情况的思考过程?

And most importantly, will this code remain readable over time? (e.g.: changing the person data type [adding a LastName field], repeated tests for other data types, etc)

而且最重要的是,随着时间的流逝,此代码是否仍然可读? (例如:更改person数据类型[添加LastName字段],对其他数据类型进行重复测试等)

“ We can solve any problem by introducing an extra level of indirection”.
“我们可以通过引入额外的间接级别来解决任何问题”。

基于属性的测试 (Property-based testing)

The first weapon in our disposal is Property-based testing (PBT). PBT works by defining a property, which is a high-level specification of behavior that should hold for all values of the specific type.

我们使用的第一个武器是基于属性的测试(PBT)。 PBT通过定义属性来工作,该属性是行为的高级规范,应适用于特定类型的所有值。

In our example, the property will be:

在我们的示例中,该属性为:

  • For every person p, if we serialize and deserialize them, we should get back the same person.

    对于每个人p,如果我们对它们进行序列化和反序列化,我们应该找回同一个人。

Writing this property using scala check looks like this:

使用scala check编写此属性如下所示:

object PersonSerdeSpec extends org.scalacheck.Properties("PersonCodec") {
  property("round trip consistency") = 
org.scalacheck.Prop.forAll { a: Person =>
    personFormat.reads(personFormat.writes(a)).asOpt.contains(a)
  }
}

The property check requires a way to generate Persons. This is done by using an Arbitrary[Person] which can be defined like this:

属性检查需要一种生成人员的方法。 这是通过使用Arbitrary[Person] ,可以这样定义:

implicit val personArb: Arbitrary[Person] = Arbitrary {
  for {
    name <- Gen.alphaStr
    age  <- Gen.chooseNum(0, 120)
  } yield Person(name, age)
}

Furthermore, we can use “scalacheck-shapeless”- an amazing library which eliminates (almost) all needs for the verbose (quite messy and highly bug-prone) arbitrary type definition by generating it for us!

此外,我们可以使用“scalacheck-shapeless” -一个令人惊叹的库,它通过为我们生成(几乎)消除了对冗长(相当混乱且容易出错的高度)任意类型定义的所有需求!

This can be done by adding:

可以通过添加以下内容来完成:

libraryDependencies += "com.github.alexarchambault" %% "scalacheck-shapeless_1.14" % "1.2.0"

and importing the following in our code:

并在我们的代码中导入以下内容:

import org.scalacheck.ScalacheckShapeless._

And then we can remove the personArb instance we defined earlier.

然后我们可以删除personArb 实例 我们之前定义的。

编解码法 (The Codec Law)

Let’s try to abstract further, by defining the laws of our data type:

让我们通过定义数据类型的规律来进一步抽象:

trait CodecLaws[A, B] {
  def serialize: A => B
  def deserialize: B => A
  def codecRoundTrip(a: A): Boolean = serialize.
andThen(deserialize)(a) == a
}

This means That given

这意味着给定

  • The types A, B

    类型A, B

  • A function from A to B

    A to B功能

  • A function from B to A

    B to A功能

We define a function called “codecRoundTrip” which takes an “a: A” and takes it through the functions and makes sure we get the same value of type A back.

我们定义了一个名为“ codecRoundTrip ”的函数,该函数采用“a: A”并通过这些函数,并确保返回与A类型相同的值。

This Law states (without giving away any implementation details), that the roundtrip we do on the given input does not “lose” any information.

该法律规定(不提供任何实现细节 ),我们在给定输入上进行的往返操作不会“丢失”任何信息。

Another way of saying just that is by claiming that our types A and B are isomorphic.
换句话说,就是宣称我们的类型A和B是同构的。

We can abstract even more, by using the cats-laws library with the IsEq case-class for defining an Equality description.

通过使用带有IsEq案例类的cats-laws库来定义平等描述,我们可以进一步抽象。

import cats.laws._
trait CodecLaws[A, B] {
  def serialize: A => B
  def deserialize: B => A
  def codecRoundTrip(a: A): cats.laws.IsEq[A] = serialize.andThen(deserialize)(a) <-> a
}
/** Represents two values of the same type that are expected to be equal. */
final case class IsEq[A](lhs: A, rhs: A)

What we get from this type and syntax is a description of equality between the two values instead of the equality result like before.

我们从这种类型和语法中得到的是两个值之间相等性的描述 而不是像以前一样的相等性结果。

编解码器测试 (The Codec Test)

It is time to test the laws we just defined. In order to do that, we will use the “discipline” library.

现在该测试我们刚刚定义的法律了。 为此,我们将使用“ 学科 ”库。

import cats.laws.discipline._
import org.scalacheck.{ Arbitrary, Prop }
trait CodecTests[A, B] extends org.typelevel.discipline.Laws {
  def laws: CodecLaws[A, B]
  def tests(
    implicit
    arbitrary: Arbitrary[A],
    eqA: cats.Eq[A]
  ): RuleSet =
    new DefaultRuleSet(
      name   = name,
      parent = None,
      "roundTrip" -> Prop.forAll { a: A =>
        laws.codecRoundTrip(a)
      }
    )
}

We define a CodecTest trait that takes 2 type parameters A and B, which in our example will be Person and JsResult.

我们定义一个CodecTest特征,它接受2个类型参数A and B ,在我们的示例中为PersonJsResult

The trait holds an instance of the laws and defines a test method that takes an Arbitrary[A] and an equality checker (of type Eq[A]) and returns a rule-set for scalacheck to run.

该特征包含一个法律实例,并定义一个测试方法,该方法采用Arbitrary[A]和相等性检查器(类型Eq[A] )并返回rule-set以供scalacheck运行。

Note that no tests actually run here. This gives us the power to run these tests which are defined just once for all the types we want

请注意,此处没有实际运行测试。 这使我们能够运行这些测试,这些测试针对我们想要的所有类型只定义了一次

We can now commit to a specific type and implementation (like Play-Json serialization) by instantiating a CodecTest with the proper types.

现在,我们可以通过实例化具有适当类型的CodecTest来承诺特定的类型和实现(例如Play-Json序列化)。

object JsonCodecTests {
  def apply[A: Arbitrary](implicit format: Format[A]): CodecTests[A, JsValue] =
    new CodecTests[A, JsValue] {
      override def laws: CodecLaws[A, JsValue] =
        CodecLaws[A, JsValue](format.reads, format.writes)
    }
}

(类型)绕行 (A (type) detour)

But now we get the error:

但是现在我们得到了错误:

Error:(11, 38) type mismatch;
 found   : play.api.libs.json.JsResult[A]
 required: A

We expected the types to flow from:

我们期望类型来自:

A  =>  B  =>  A

But Play-Json types go from:

但是Play-Json类型来自:

A  =>  JsValue  =>  JsResult[A]

This means that our deserialize function can succeed or fail and will not always return an A, but rather a container of A.

这意味着我们的反序列化功能可以成功或失败,并且不会总是返回A,而是返回A的容器。

In order to abstract over the types, we now need to use the F[_] type constructor syntax:

为了抽象类型,我们现在需要使用F[_]类型构造函数语法:

trait CodecLaws[F[_],A, B] {
  def serialize: A => B
  def deserialize: B => F[A]
  def codecRoundTrip(a: A)(implicit app:Applicative[F]): IsEq[F[A]] =
    serialize.andThen(deserialize)(a) <-> app.pure(a)
}

The Applicative instance is used to take a simple value of type A and lift it into the Applicative context which returns a value of type F[A].

Applicative实例用于获取类型A的简单值,并将其提升到Applicative上下文中,该上下文返回类型为F[A]

This process is similar to taking some value x and lifting it to an Option context using Some(x), or in our concrete example taking a value a:A and lifting it to the JsResult type using JsSuccess(a).

此过程类似于获取一些值x并使用Some(x)将其提升到Option上下文,或者在我们的具体示例中,获取值a:A并使用JsSuccess (a)将其提升为JsResult类型。

We can now finish the implementation for CodecTests and JsonCodecTests like this:

现在,我们可以像这样完成CodecTestsJsonCodecTests的实现:

trait CodecTests[F[_], A, B] extends org.typelevel.discipline.Laws {
  def laws: CodecLaws[F, A, B]
  def tests(
    implicit
    arbitrary: Arbitrary[A],
    eqA: cats.Eq[F[A]],
    applicative: Applicative[F]
  ): RuleSet =
    new DefaultRuleSet(
      name   = name,
      parent = None,
      "roundTrip" -> Prop.forAll { a: A =>
        laws.codecRoundTrip(a)
      }
    )
}
object JsonCodecTests {
  def apply[A: Arbitrary](implicit format: Format[A]): CodecTests[JsResult, A, JsValue] =
    new CodecTests[JsResult, A, JsValue] {
      override def laws: CodecLaws[JsResult, A, JsValue] =
        CodecLaws[JsResult, A, JsValue](format.reads, format.writes)
    }
}

And to define a working Person serialization test in 1 line of code:

并在1行代码中定义一个工作Person序列化测试

import JsonCodecSpec.Person
import play.api.libs.json._
import org.scalacheck.ScalacheckShapeless._
import org.scalatest.FunSuiteLike
import org.scalatest.prop.Checkers
import org.typelevel.discipline.scalatest.Discipline
class JsonCodecSpec extends Checkers with FunSuiteLike with Discipline { 
  checkAll("PersonSerdeTests", JsonCodecTests[Person].tests)
}

抽象的力量 (The power of abstraction)

We were able to define our tests and laws without giving away any implementation details. This means we can switch to using a different library for serialization tomorrow and all our laws and tests will still hold.

我们能够定义测试和法律,而无需给出任何实施细节。 这意味着我们明天可以切换到使用其他库进行序列化,并且所有法律和测试仍然有效。

另一个例子 (Another example)

We can test this theory by adding support to BSON serialization using the reactive-mongo library:

我们可以通过使用react reactive-mongo库为BSON序列化添加支持来测试该理论:

import cats.Id
import io.bigpanda.laws.serde.{ CodecLaws, CodecTests }
import org.scalacheck.Arbitrary
import reactivemongo.bson.{ BSONDocument, BSONReader, BSONWriter }
object BsonCodecTests {
  def apply[A: Arbitrary](
    implicit
    reader: BSONReader[BSONDocument, A],
    writer: BSONWriter[A, BSONDocument]
  ): CodecTests[Id, A, BSONDocument] =
    new CodecTests[Id, A, BSONDocument] {
      override def laws: CodecLaws[Id, A, BSONDocument] =
        CodecLaws[Id, A, BSONDocument](reader.read, writer.write)
override def name: String = "BSON serde tests"
    }
}

The types here flow from

这里的类型来自

A => BsonDocument => A

and not F[A] as we had expected. Luckily for us, we have a solution and use the Id-type to represent just that.

而不是我们所期望的F[A] 。 对我们来说幸运的是,我们有一个解决方案,并使用Id类型来表示。

And given the (very long) serializer definition:

并给出(很长)的序列化器定义:

implicit val personBsonFormat
  : BSONReader[BSONDocument, Person] with BSONWriter[Person, BSONDocument] =
  new BSONReader[BSONDocument, Person] with BSONWriter[Person, BSONDocument] {
    override def read(bson: BSONDocument): Person =
      Person(bson.getAs[String]("name").get, bson.getAs[Int]("age").get)
override def write(t: Person): BSONDocument =
      BSONDocument("name" -> t.name, "age" -> t.age)
  }

we can now define BsonCodecTests in all its 1 line of logic glory.

我们现在可以在所有1条逻辑荣耀中定义BsonCodecTests。

class BsonCodecSpec extends Checkers with FunSuiteLike with Discipline {
    checkAll("PersonSerdeTests", BsonCodecTests[Person].tests)
}

我们代码的(一阶)逻辑观点 (A (First-order) logic perspective on our code)

Our first test attempt can be described as follows:

我们的第一次测试尝试可以描述如下:

∃p:Person,s:OFormat that holds : s.read(s.write(p)) <-> p

Meaning, for the specific person p(“John Doe”,32) and for the format s, the following statement is true: decode(encode(p)) <-> p.

意思是, for the specific person p(“John Doe”,32)for the format s ,以下说法是正确的: decode(encode(p)) <- > p。

The second attempt (using PBT) can be:

第二次尝试(使用PBT )可以是:

∃s:OFormat, ∀p:Person the following should hold :  s.read(s.write(p)) <-> p

Which means, for all persons p and for the specific format s, the following is true: decode(encode(p))<->p.

这意味着, for all persons pfor the specific format s ,以下条件是正确的: decode(encode(p))< -> p。

The third (and most powerful statement thus far) using law testing:

使用law testing的第三个(也是迄今为止最有力的陈述):

∀s:Encoder, ∀p:Person the the following should hold :  s.read(s.write(p)) <-> p

Which means, for all formats s, and for all persons p, the following is true: decode(encode(p))<->p.

这意味着, for all formats sfor all persons p ,以下内容均成立: decode(encode(p))< -> p。

摘要 (Summary)

  • Law testing allows you to reason about your data-types and functions in a mathematical and concise way and provides a totally new paradigm for testing your code!

    法律测试使您能够以数学简洁的方式推理数据类型和函数,并为测试代码提供了全新的范例!
  • Most of the type level libraries you use (like cats, circe and many more) use law testing internally to test their data-types.

    您使用的大多数类型级别库(例如catscirce等)在内部都使用法律测试来测试其数据类型。

  • Avoid writing specific test-cases for your data-types and functions and try to generalize them using property-based law tests.

    避免为您的数据类型和函数编写特定的测试用例,并尝试使用基于属性的法律测试来概括它们。

Thank you for reaching this far! I am super excited about finding more abstract and useful laws that I can use in my code! Please let me know about any you’ve used or can think of.

谢谢您达成目标! 我为能在代码中使用更多抽象和有用的法律而感到非常兴奋! 请让我知道您已经使用或想到的任何内容。

More inspiring and detailed content can be found in the cats-laws site or circe.

cat -laws网站或circe上可以找到更多启发性和详细的内容。

The complete code examples can be found here.

完整的代码示例可在此处找到。

翻译自: https://www.freecodecamp.org/news/an-introduction-to-law-testing-in-scala-4243d72272f9/

scala 单元测试

 类似资料: