sábado, 16 de março de 2013

Mais um pouco de ScalaTest

No post anterior, sobre Mockito, falei um pouco sobre ScalaTest, sem, no entanto, entrar em detalhes.
No post atual, pretendo então relatar alguma experiência que tive com a linguagem Scala e com a biblioteca ScalaTest.


Um pouco sobre Scala



Conforme o site oficial:
Scala é uma linguagem de programação de propósito geral projetada para expressar padrões de programação comuns de uma forma concisa, elegante e type-safe. Suavemente integra recursos de linguagens orientadas a objetos e funcional, permitindo que programadores Java e outros a serem mais produtivos. Tamanhos de código são normalmente reduzidos por um fator de 2-3 em relação a uma aplicação Java equivalente.


Adicionalmente, Scala é uma linguagem que executa sobre a Java Virtual Machine, assim como a própria linguagem Java, apontando, na verdade, o que tem sido uma grande tendência no universo Java: Uma plataforma multi-linguagem com ferramentas adequadas para diversas situações, mantendo por baixo de tudo o seu mecanismo de OO conhecido como a plataforma mais madura atualmente.
Se você é desenvolvedor Java, vale a pena (se não for já um dever) conhecer essas linguagens que compartilham a JVM, pois em inúmeras situações Java não será a linguagem mais adequada, e aí, como bons desenvolvedores que fazem o "tema de casa", temos que saber discernir.
Algumas peculiaridades percebidas de cara são a ausência de ponto-e-vírgula, a desobrigação de () na chamada de métodos que não recebem parâmetros, o tratamento de classes como objetos, etc.



ScalaTest



As devidas apresentações já foram feitas no post anterior então vamos à sua estrutura.
ScalaTest oferece algumas Traits padrão para serem implementadas/estendidas que abrangem diferentes abordagens de testes. Dificilmente você vai usar todas, ou até muitas delas, na maioria dos casos, vai utilizar uma ou duas. Uma dá uma flexibilidade maior de implementação das próprias assertions, outra é para testes semelhantes (quase idênticos) aos com RSpec para Ruby (com describe, it, etc.), outra é mais no estilo JUnit, outra xUnit, dentre outras. A lista completa com uma breve descrição pode ser conferida neste link.



Scala e ScalaTest com Eclipse IDE



Para a IDE top-of-mind, Eclipse, é possível instalar plugins que tornam natural a criação de projetos Scala, testes com ScalaTest e tudo mais. Os dois plugins que utilizei para Scala na versão Juno da IDE seguem abaixo. Para instalá-los, utilize estas URLs no menu Help -> Install New Software.

  • Scala IDE: http://download.scala-ide.org/nightly-update-juno-master-29x
  • Scala Worksheet: http://download.scala-ide.org/nightly-build-worksheet-scalaide21-29/site

Para ScalaTest, siga as dicas do site oficial:



Finalmente, exemplos



No meu caso, achei conveniente, e extremamente interessante, explorar uma combinação de JUnit com should matchers.
A trait JUnitSuite estende org.scalatest.Suite e org.scalatest.junit.AssertionsForJUnit, e a trait ShouldMatchersForJUnit estende org.scalatest.matchers.ShouldMatchers e org.scalatest.junit.AssertionsForJUnit. Dessa forma, recursos valiosos da biblioteca ScalaTest são combinados para utilização com JUnit, como veremos em seguida.


package scalatest

import java.nio.charset.Charset
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.StandardOpenOption
import java.util.ArrayList

import org.junit.AfterClass
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.mockito.Matchers.anyListOf
import org.mockito.Matchers.anyObject
import org.mockito.Matchers.anyString
import org.mockito.Mockito.doCallRealMethod
import org.mockito.Mockito.mock
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.scalatest.junit.JUnitSuite
import org.scalatest.junit.ShouldMatchersForJUnit

import com.adornes.somefun.Controller
import com.adornes.somefun.Util
import com.adornes.somefun.exception.MissingAgeException
import com.adornes.somefun.exception.MissingNameException

// Changing the object of the class
// Equivalent to the 'static' Java approach.
object ReaderTest {
  
  val TEST_FILE = "test_file"

  @BeforeClass 
  def createTestFile {
      
    var lines = new ArrayList[String]
    lines.add("Alice 25 years old")
    lines.add("Bob 26 years old")
    lines.add("Carol 27 years old")

    Files.write(FileSystems.getDefault.getPath(".", TEST_FILE), 
                lines, Charset.defaultCharset, StandardOpenOption.CREATE)
  }
 
  @AfterClass 
  def deleteTestFile {
    var testFile = FileSystems.getDefault.getPath(".", TEST_FILE)
    Files.delete(testFile)
  }
}

...
...


Nas linhas acima temos a primeira parte do nosso código de exemplo. Primeiro, uma porção de imports. Perceba já aí que os ponto-e-virgula não são necessários, e nem a palavra-chave static para importação de métodos estáticos.
Na linha 30, a definição object ReaderTest indica que o que vem em seguida são alterações no objeto da própria classe ReaderTest, o que na mentalidade da linguagem Java é equivalente à declaração de atributos e métodos estáticos. JUnit exige que os métodos marcados como @BeforeClass e @AfterClass sejam estáticos, por isso em Scala eles devem ser declarados dentro de um bloco object.
Na linha 32, a palavra-chave val indica, em Scala, uma constante. Nos métodos que seguem após a linha 32, podemos ver a utilização de annotations exatamente da mesma forma que faríamos em um código JUnit escrito em Java. Nas linhas 42 e 49, um pouquinho de NIO.2, nova biblioteca de I/O do Java 7.


Nas linhas abaixo, vemos a continuação do mesmo código acima:
...
...

class ReaderTest extends JUnitSuite with ShouldMatchersForJUnit{
  
  var controller : Controller = _

  @Before 
  def setUp {
    controller = new Controller
  }
 
  @Test
  def testProcess {
    var mockReader = mock(classOf[Reader])

    doCallRealMethod.when(mockReader).readFile(anyString)
    doCallRealMethod.when(mockReader).readLines(anyListOf(classOf[String]))
  
    var lines = mockReader.readFile(ReaderTest.TEST_FILE)
    mockReader.readLines(lines)
    verify(mockReader, times(lines.size)).buildPerson(anyString)
    verify(mockReader, times(lines.size)).addPerson(anyObject())
  }

  @Test
  def testBuildPersonFail {
    intercept[MissingAgeException]{
      controller.buildPerson("Dave") // How old is he??
    }
    intercept[MissingNameException]{
      controller.buildPerson("100 years old") // Who is 100 years old??
    }
  }

  @Test 
  def testBuildPersonSuccess {
    var per1 = controller.buildPerson("John 30 years old")
    var per2 = controller.buildPerson("Peter 31 years old")

    per1.age should be (30)
    per2.age should be (31)
  }
}


Dessa vez começamos com uma declaração class, o que significa que estamos definindo atributos e métodos para as instância dessa classe (e não mais estáticos como na declaração object). Na linha 14 temos um método com a mesma ideia que exemplifiquei no post anterior usando Mockito.
Nas linhas 28 e 31, a palavra-chave intercept verifica se a exceção especificada está sendo lançada.
E nas linhas 41 e 42, quase desapercebido, aparece o uso dos should matchers. Que por baixo dos panos traduz a validação para JUnit. Fantástico, não?



Performance



Todos os meus testes foram feitos inicialmente com JUnit e Java somente. Depois migrei para Scala com ScalaTest e pude ter a experiência que relatei aqui. No entanto, pelo que medi, o desempenho caiu pela metade na execução dos testes. Para minha aplicação, foi de mais ou menos 1 segundo para meio segundo, ou seja, perda insignificante, no entanto, para uma aplicação real com talvez centenas de cenários de teste pode haver uma perda significativa. Aí se alguém tiver essa experiência, pode compartilhar aí!


Conclusão



Este post misturou bastante coisa, e não aprofundou muito em algumas, mas espero que tenha gerado aquele interesse de olhar mais nos links que referenciei no decorrer do texto. Fica minha recomendação para a utilização da linguagem Scala e, para testes, acho que vale a pena dar uma olhada em ScalaTest sem abandonar o bom e velho JUnit.


Nenhum comentário:

Postar um comentário