scala 单元测试_Scala中的法律测试简介
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.alphaStrage <- 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 => Bdef deserialize: B => Adef 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 => Bdef deserialize: B => Adef 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(implicitarbitrary: 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 ,在我們的示例中為Person和JsResult 。
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: AWe expected the types to flow from:
我們期望類型來自:
A => B => ABut 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 => Bdef 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:
現在,我們可以像這樣完成CodecTests和JsonCodecTests的實現:
trait CodecTests[F[_], A, B] extends org.typelevel.discipline.Laws {def laws: CodecLaws[F, A, B]def tests(implicitarbitrary: 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](implicitreader: 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 => Aand 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)) <-> pMeaning, 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)) <-> pWhich means, for all persons p and for the specific format s, the following is true: decode(encode(p))<->p.
這意味著, for all persons p和for 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)) <-> pWhich means, for all formats s, and for all persons p, the following is true: decode(encode(p))<->p.
這意味著, for all formats s和for 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.
您使用的大多數類型級別庫(例如cats , circe等)在內部都使用法律測試來測試其數據類型。
- 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 單元測試
總結
以上是生活随笔為你收集整理的scala 单元测试_Scala中的法律测试简介的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 梦到自己生了个小孩怎么回事
- 下一篇: 怀孕了梦到大蟒蛇意味什么