Scala de Aprendizagem: Parte 3 - Testes de Unidade





Olá, Habr! Não é suficiente escrever um bom código. Ainda precisamos cobri-lo com bons testes de unidade. No último artigo, fiz um servidor web simples. Agora vou tentar escrever quantos testes. Regular, baseado em propriedade e simulado. Para obter detalhes, bem-vindo em cat.



Conteúdo





Links





Imagem da janela de encaixe de imagens de fontes



E assim, para os testes de unidade, você precisa de 3 libs.



  1. Biblioteca para a criação de testes
  2. Biblioteca que irá gerar dados de teste
  3. Uma biblioteca que simulará objetos


Usei a biblioteca ScalaTest para criar testes



"org.scalatest" %% "scalatest" % "3.2.0" % Test


Usei o ScalaCheck para gerar dados de teste para testes baseados em propriedade .



"org.scalacheck" %% "scalacheck" % "1.14.3" % Test


e uma extensão que combina ScalaTest + ScalaCheck ScalaTestPlusScalaCheck



"org.scalatestplus" %% "scalacheck-1-14" % "3.2.0.0" % Test


Usei o ScalaMock para simular objetos



"org.scalamock" %% "scalamock" % "4.4.0" % Test


Uma classe simples que representa um tipo de string preenchida (não vazia). Vamos agora testá-lo.



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
    }
  }
}


Criando uma classe para nossos testes



class FilledStrTests extends AnyFlatSpec with should.Matchers with ScalaCheckPropertyChecks {

}


Criamos um método que irá verificar se ao criar nossa classe a partir das mesmas linhas, receberemos os mesmos dados.



 "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)
  }


No último teste, codificamos em uma string feita à mão. Agora vamos fazer um teste usando os dados gerados. Usaremos uma abordagem baseada em propriedades na qual as propriedades da função são testadas, de modo que, com esses dados de entrada, recebamos esses dados de saída.



  "constructor" should "save expected value" in {
    forAll { s: String =>
//   .          .
      whenever(s.trim.nonEmpty) {
        val a = FilledStr(s).get
        a.value should be(s)
      }
    }
  }


Você pode configurar explicitamente o gerador de dados de teste para usar apenas os dados de que precisamos. Por exemplo, assim:



//   
val evenInts = for (n <- Gen.choose(-1000, 1000)) yield 2 * n
//    
forAll (evenInts) { (n) => n % 2 should equal (0) }


Você também não pode passar explicitamente nosso gerador, mas definir seu implícito por meio de Arbitrário para que seja automaticamente passado como um gerador para testes. Por exemplo, assim:



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) }


Você também pode gerar objetos complexos usando Arbitrário.



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 }


Agora, vamos tentar escrever um teste mais a sério. Vamos simular dependências para TodosService. Ele usa 2 repositórios e o repositório, por sua vez, usa uma abstração sobre a transação UnitOfWork. Vamos testar seu método mais simples.



  def getAll(): F[List[Todo]] =
    repo.getAll().commit()


O que apenas chama o repositório, inicia uma transação nele para ler a lista de tarefas, termina e retorna o resultado. Também no teste, em vez de F [_], é colocada a mônada Id, que simplesmente retorna o valor armazenado nela.



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)
  }
}


Acabou sendo muito agradável escrever testes em Scala, e ScalaCheck, ScalaTest, ScalaMock revelaram-se bibliotecas muito boas. Bem como a biblioteca para a criação de API tapir e a biblioteca para o servidor http4s e a biblioteca para fluxos fs2. Até agora, o ambiente e as bibliotecas do Scala causam apenas emoções positivas em mim. Espero que essa tendência continue.



All Articles