Learning Scala: Part 3 - Unit Tests





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





Links



Sources

Images docker image



And so for unit tests you need 3 libs.



  1. Library for creating tests
  2. Library that will generate test data
  3. 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.



All Articles