Hello, Habr! It's not enough to write good code. We still need to cover it with good Unit Tests. In the last article, I made a simple web server. Now I will try to write how many tests. Regular, Property-based and mocked. For details, welcome under cat.
Content
- Learning Scala: Part 1 - Snake Game
- Learning Scala: Part 2 - Todo Sheet with Image Upload Capability
- Learning Scala: Part 3 - Unit Tests
Links
Sources
Images docker image
And so for unit tests you need 3 libs.
- Library for creating tests
- Library that will generate test data
- A library that will mock objects
I used the ScalaTest library to create tests
"org.scalatest" %% "scalatest" % "3.2.0" % Test
I used ScalaCheck to generate test data for Property-based testing .
"org.scalacheck" %% "scalacheck" % "1.14.3" % Test
and an extension that combines ScalaTest + ScalaCheck ScalaTestPlusScalaCheck
"org.scalatestplus" %% "scalacheck-1-14" % "3.2.0.0" % Test
I used ScalaMock to mock objects
"org.scalamock" %% "scalamock" % "4.4.0" % Test
A simple class that represents a type of filled (not empty) string. We will now test it.
package domain.common
sealed abstract case class FilledStr private(value: String) {
def copy(): FilledStr = new FilledStr(this.value) {}
}
object FilledStr {
def apply(value: String): Option[FilledStr] = {
val trimmed = value.trim
if (trimmed.nonEmpty) {
Some(new FilledStr(trimmed) {})
} else {
None
}
}
}
Creating a class for our tests
class FilledStrTests extends AnyFlatSpec with should.Matchers with ScalaCheckPropertyChecks {
}
We create a method that will check that when creating our class from the same lines, we will receive the same data.
"equals" should "return true fro equal value" in {
val str = "1234AB"
val a = FilledStr(str).get
val b = FilledStr(str).get
b.equals(a) should be(true)
}
In the last test, we have coded into a hand-crafted string. Now let's do a test using the generated data. We will use a property-based approach in which the properties of the function are tested, so that with such input data, we will receive such output data.
"constructor" should "save expected value" in {
forAll { s: String =>
// . .
whenever(s.trim.nonEmpty) {
val a = FilledStr(s).get
a.value should be(s)
}
}
}
You can explicitly configure the test data generator to use only the data we need. For example like this:
//
val evenInts = for (n <- Gen.choose(-1000, 1000)) yield 2 * n
//
forAll (evenInts) { (n) => n % 2 should equal (0) }
You can also not explicitly pass our generator, but define its implict through Arbitrary so that it is automatically passed as a generator to tests. For example like this:
implicit lazy val myCharArbitrary = Arbitrary(Gen.oneOf('A', 'E', 'I', 'O', 'U'))
val validChars: Seq[Char] = List('X')
// Arbitrary[Char] .
forAll { c: Char => validChars.contains(c) }
You can also generate complex objects using Arbitrary.
case class Foo(intValue: Int, charValue: Char)
val fooGen = for {
intValue <- Gen.posNum[Int]
charValue <- Gen.alphaChar
} yield Foo(intValue, charValue)
implicit lazy val myFooArbitrary = Arbitrary(fooGen)
forAll { foo: Foo => (foo.intValue < 0) == && !foo.charValue.isDigit }
Now let's try to write a test more seriously. We will mock dependencies for TodosService. It Uses 2 repositories and the repository in turn uses an abstraction over the UnitOfWork transaction. Let's test its simplest method.
def getAll(): F[List[Todo]] =
repo.getAll().commit()
Which just calls the repository, starts a transaction in it to read the Todo list, ends it and returns the result. Also in the test, instead of F [_], the Id monad is put, which simply returns the value stored in it.
class TodoServiceTests extends AnyFlatSpec with MockFactory with should.Matchers {
"geAll" should " " in {
// .
implicit val tr = mock[TodosRepositoryContract[Id, Id]]
implicit val ir = mock[InstantsRepositoryContract[Id]]
implicit val uow = mock[UnitOfWorkContract[Id, List[Todo], Id]]
// . implicit
val service= new TodosService[Id, Id]()
// Id Todo
val list: Id[List[Todo]] = List(Todo(1, "2", 3, Instant.now()))
// getAll uow 1
(tr.getAll _).expects().returning(uow).once()
// commit 1
(uow.commit _).expects().returning(list).once()
// getAll
//
service.getAll() should be(list)
}
}
It turned out to be very pleasant to write tests in Scala, and ScalaCheck, ScalaTest, ScalaMock turned out to be very good libraries. As well as the library for creating API tapir and the library for the http4s server and the library for fs2 streams. So far, the environment and libraries for Scala cause only positive emotions in me. I hope this trend will continue.