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
- Learning Scala: Parte 1 - Jogo da Cobra
- Learning Scala: Parte 2 - Folha de tarefas com uploads de imagens
- Scala de Aprendizagem: Parte 3 - Testes de Unidade
Links
Imagem da janela de encaixe de imagens de fontes
E assim, para os testes de unidade, você precisa de 3 libs.
- Biblioteca para a criação de testes
- Biblioteca que irá gerar dados de teste
- 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.