Pesquise direto (no 100 1/2 PALAVRA ou na WEB)

Related Posts with Thumbnails

terça-feira, 21 de setembro de 2010

Desenvolvimento dirigido por testes para TV digital utilizando Lua

 

O Desenvolvimento Dirigido por Testes ou em inglês Test Driven Development (TDD) é uma técnica de desenvolvimento de software muito utilizada em projetos que adotam metodologias ágeis, como SCRUM e XP, para gerenciamento.

Em TV digital, o desenvolvimento também pode, e deve, ser ágil. Por isso decidi escrever este artigo, abordando uma solução para a realização de testes unitários em projetos de TV digital.

Apresentarei uma ferramenta para a realização de testes unitários em código Lua e desenvolverei uma solução para possibilitar o uso da mesma ferramenta em projetos de TV digital.

Nosso ambiente de trabalho será composto pelo código a ser testado, os testes e uma ferramenta para execução dos testes e apresentação dos resultados. Vale mencionar que focarei em código Lua porque não encontrei, e acredito que ainda não foi lançada, uma ferramenta para a execução de testes em código NCL. Seria bem interessante utilizarmos Desenvolvimento Dirigidos por Comportamento, ou em inglês Behavior Driven Development (BDD), para projetos em NCL.

Ferramenta para execução dos testes

Existem várias opções de ferramentas para testes unitários em Lua. Nós utilizaremos o Telescope pelo fato de ele possuir funcionalidades interessantes e uma API bem documentada.

Para instalar o Telescope, com o Luarocks previamente instalado, basta digitar:

sudo luarocks build telescope --from=http://luarocks.luaforge.net/rocks-cvs

Também é possível obter o código-fonte a partir do repositório git:

git clone git://github.com/norman/telescope.git

Após a instalação, podemos chamar o Telescope com o comando tsc. O help é bem explicativo e pode ser chamado com o comando abaixo:

tsc --help

Criando os primeiros testes

Neste primeiro exemplo, criaremos um único arquivo com as funções de soma e subtração e os seus testes. O código pode ser observado abaixo:

-- Funcoes que desejamos testar
function soma(a,b)
        return a+b
end

function sub(a,b)
        return a-b
end

-- Testes para a funcao soma
context("Funcao soma(a,b)", function()
        test("Teste 1 + 1 = 2", function()
                assert_equal(soma(1,1), 2)
        end)
        test("Teste 1 + 's' = 0", function()
                assert_equal(soma(1,'s'),0)
        end)
end)
-- Testes para a funcao sub
context("Funcao sub(a,b)", function()
        test("Teste 1 - 1 = 0", function()
                assert_equal(sub(1,1),0)
        end)
        test("Teste 2 - 1 = 1", function()
                assert_equal(sub(2,1),1)
        end)
end)

Até a linha 9 não temos novidades. Na linha 11 iniciamos a declaração do contexto onde iremos organizar os testes para a função soma.

Nesse momento vale abrir uma parênteses e falar um pouco sobre os contextos. Como disse acima, o Telescope apresenta funcionalidades interessantes, e uma delas é a possibilidade de organizarmos os testes em contextos, que ainda podem ser aninhados. No exemplo não utilizamos contextos aninhados porque os testes são bem simples, porém no código abaixo podemos ver o uso de contextos em outro exemplo com uma complexidade um pouco maior:

-- tests
context("Testing servers/api.lua", function()
--------
        context("Teste de conexao", function()
                local s,d,h,c = client.get(string.format("http://%s:%d", host, port), nil, auth)
        -----
                if not s then
                        test("Sem conexao", function()
                                assert_false(s)
                                assert_equal(c, "connection refused")
                                assert_blank(d)
                                assert_nil(h)
                        end)
                end
        -----
                if s then
                        test("Com conexao", function()

                                assert_true(s)
                                assert_type(c, "number") --deve retornar um codigo
                                assert_type(h, "table") -- deve retornar uma tabela com o cabecalho
                        end)
                end
        -----
        end)
----------
        context("Teste de recursos", function()
        ---------
                test("Recurso INVALIDO", function()
                        local s,d,h,c = client.get(string.format("http://%s:%d/%s", host, port,"xyz"), nil, auth)
                        assert_true(s)
                        assert_type(c, "number") --deve retornar um codigo
                        assert_equal(c, 404)
                end)
        --------
                test("Recurso PING disponivel", function()
                        local s,d,h,c = client.get(string.format("http://%s:%d/%s", host, port,"ping"), nil, auth)
                        assert_true(s)
                        assert_type(c, "number") --deve retornar um codigo
                        assert_not_equal(c, 404)
                end)
        --------
                test("Recurso SEARCH disponivel", function()
                        local s,d,h,c = client.get(string.format("http://%s:%d/%s", host, port,"search"), nil, auth)
                        assert_true(s)
                        assert_type(c, "number") --deve retornar um codigo
                        assert_not_equal(c, 404)
                end)
        --------

        end)
----------
end)

Esse código é parte de um arquivo de testes de um servidor Lua que implementa uma API e oferece alguns recursos. Como pode-se notar, os contextos mostram-se bem úteis para a organização dos testes de um projeto quando começamos a trabalhar em algo mais elaborado.

Voltando ao nosso primeiro exemplo, a sintaxe básica, a partir da linha 11, para construirmos nossos testes é:

context("Nome do contexto", function()
        test("Nome do teste", function()
                -- Assertions
        end)
        --Mais testes
end)

O Telescope oferece por padrão as assertions mais básicas (veja a API). Porém, se desejar, você também pode construir as suas.

Para executarmos nosso teste fazemos:

tsc nome-do-arquivo.lua

A saída será algo como:

4 tests 3 passed 3 assertions 0 failed 1 error 0 unassertive 0 pending

Teste 1 + 's' = 0:
teste1.lua:4: attempt to perform arithmetic on local 'b' (a string value)
stack traceback:
   /usr/local/share/lua/5.1//telescope.lua:374: in function 'invoke_test'
  /usr/local/share/lua/5.1//telescope.lua:399: in function 'run'
  ...usr/local/lib/luarocks/rocks/telescope/scm-1/bin/tsc:266: in main chunk
  [C]: ?

Na lina 1 temos o resumo dos testes, na linha 3 o teste que falhou e na linha 4 o motivo da falha. A partir da linha 5 temos a saída do interpretador Lua.

Podemos melhorar nossa saída com o parâmetro -f:

tsc -f nome-do-arquivo.lua

Obtendo com isso uma saída mais detalhada, onde o Telescope apresenta um resumo mais elaborado dos testes realizados (linhas 1 a 9):

------------------------------------------------------------------------
Funcao soma(a,b):
Teste 1 + 1 = 2                                                      [P]
Teste 1 + 's' = 0                                                    [E]
------------------------------------------------------------------------
Funcao sub(a,b):
Teste 1 - 1 = 0                                                      [P]
Teste 2 - 1 = 1                                                      [P]
------------------------------------------------------------------------
4 tests 3 passed 3 assertions 0 failed 1 error 0 unassertive 0 pending

Teste 1 + 's' = 0:
teste1.lua:4: attempt to perform arithmetic on local 'b' (a string value)
stack traceback:
  /usr/local/share/lua/5.1//telescope.lua:374: in function 'invoke_test'
  /usr/local/share/lua/5.1//telescope.lua:399: in function 'run'
  ...usr/local/lib/luarocks/rocks/telescope/scm-1/bin/tsc:266: in main chunk
        [C]: ?

Rebuscando um pouco mais

No exemplo anterior criamos as funções e os testes no mesmo arquivo. Porém, na prática, isso não funciona muito bem e nem deve ser feito.

Vamos separar nossos arquivos encapsulando as funções em um módulo. Assim podemos chamar o módulo diretamente do nosso arquivo de testes.

Nosso módulo de operações matemáticas ficará assim:

-- Modulo com operacoes matematicas
module("matematica")
-- operacao soma
function soma(a,b)
        return a+b
end
-- operacao subtracao
function sub(a,b)
        return a-b
end

E agora os testes em outro arquivo:

-- Chama o modulo matematica
require"matematica"
-- Testes
context("Funcao soma(a,b)", function()
        test("Teste 1 + 1 = 2", function()
                assert_equal(matematica.soma(1,1), 2)
        end)
        test("Teste 1 + 's' = 0", function()
                assert_equal(matematica.soma(1,'s'),0)
        end)
end)
context("Funcao sub(a,b)", function()
        test("Teste 1 - 1 = 0", function()
                assert_equal(matematica.sub(1,1),0)
        end)
        test("Teste 2 - 1 = 1", function()
                assert_equal(matematica.sub(2,1),1)
        end)
end)

Utilizando testes nos projetos de TV digital

Agora que já vimos como construir nossos testes, iremos aplicar nosso conhecimento num exemplo que envolve desenvolvimento para TV digital.

Tomemos uma aplicação muito simples: "Quando o telespectador pressiona o botão vermelho uma mensagem é desenhada na tela e uma âncora é iniciada".

Nesse exemplo teremos um nó Lua com um tratador de eventos que só está interessado em eventos de pressionamento de tecla, mais especificamente só da tecla vermelha. Por não ser o objetivo deste tutorial, não entrarei em detalhes do código NCL.

Nosso tratador de eventos pode ser implementado assim:

function handler (evt)
  if (evt.class == 'key') and (evt.type == 'press') and (evt.key == 'RED') then
      canvas:attrColor('white')
      canvas:attrFont('vera',30)
      canvas:drawText (200, 200, "Botao vermelho pressionado." )

      event.post {
        class  = 'ncl',
        type   = 'presentation',
        area   = 'fim',
        action = 'start',
      }

  end
end
event.register(handler)

Iremos elaborar alguns testes para garantir a consistência na implementação do tratador de eventos. Para exemplificar, vamos definir algumas condições de contorno e, posteriormente, construiremos os testes para elas. Vale a ressalva de que esse procedimento não está 100% de acordo com os "mandamentos" ("...inicialmente o desenvolvedor escreve um teste e depois escreve o código que possa ser validado pelo teste...") do TDD, porém como o objetivo aqui não é ser doutrinador-xiita, vamos focar na ferramenta e na solução; os mais rigorosos podem começar pelos testes sem problemas :-).

Nossas condições de contorno são:


  1. o tratador só deve tratar eventos de pressionamento de tecla;
  2. o tratador só está interessado na tecla vermelha;
  3. quando a tecla vermelha for pressionada, a frase "Botao vermelho pressionado." deve ser escrita na posição 200 x 200 e
  4. um evento (com class = 'ncl', type = 'presentation', area = 'fim' e action = 'start') deve ser postado;
  5. o tratador de eventos deve ser registrado.

Com nossas condições de contorno definidas, podemos continuar. Porém o código do tratador de eventos não irá funcionar fora do middleware. Ao executá-lo, recebemos o erro:

lua: exemplo.lua:16: attempt to index global 'event' (a nil value)
stack traceback:
        tratador.lua:16: in main chunk
        [C]: ?

Isso acontece porque algumas bibliotecas utilizadas não são padrões do Lua e só estão presentes na implementação do middleware. Para contornarmos essa situação, iremos realizar algumas configurações adicionais no arquivo de testes e, assim, garantir que eles funcionem fora do ambiente do middleware.

Logo no início do nosso arquivo de testes criaremos as configurações para simularmos os módulos utilizados pelo tratador de eventos.

O arquivo completo pode ser visto abaixo:

-------
-- Tabelas representando os valores do sistema
-------
system_modified = false
-- Tela
canvas_values = {
        color = '',
        font = '',
        font_size = 0,
        text_x = 0,
        text_y = 0,
        text_message = '',
}
-- Eventos
events ={
        posted = {},
        event_registered = ''
}
-------
-- Configuracao modulo event
-------
event = {}
_G["event"] = event
-- Se recebe um tratador (funcao) registra e retorna true
function event.register(handler)
        if type(handler) == 'function' then
                events.event_registered = handler
                return true
        else return false end
end
-- Posta os eventos
function event.post(tab)
        if type(tab) == 'table' then
                events.posted.class = tab.class
                events.posted.type = tab.type
                events.posted.area = tab.area
                events.posted.action = tab.action
                system_modified = true
                return true
        else return false end
end
-------
-- Configuracao modulo canvas
-------
canvas = {}
_G["canvas"] = canvas
function canvas:attrColor(color)
        canvas_values.color = color
        system_modified = true
end
function canvas:attrFont(font,size)
        canvas_values.font = font
        canvas_values.font_size = size
        system_modified = true
end
function canvas:drawText(x,y,message)
        canvas_values.text_x = x
        canvas_values.text_y = y
        canvas_values.text_message = message
        system_modified = true
end

--------
-- Carrega o tratador de eventos
dofile"main.lua"
--------

--------
-- Inicio dos Testes
--------
context("Exemplo de testes para tratador de eventos", function()

        test("Teste 1: Tratador NAO trata outro tipo de evento", function()
                --configuracao do evento
                local evt = {
                        class = "ncl",
                        type = "presentation",
                        action = "start"
                }
                system_modified = false
                handler(evt)
                assert_false(system_modified)
        end)
----------------
        test("Teste 2: So trata eventos quando a tecla vermelha (RED) for pressionada", function()
                --configuracao do evento
                local evt = {
                        class = "key",
                        type = "press",
                        key = "RED"
                }
                system_modified = false
                handler(evt)
                assert_true(system_modified)
        end)
----------------
        test("Teste 3: Frase: 'Botao vermelho pressionado.' eh exibida", function()
                --configuracao do evento
                local evt = {
                        class = "key",
                        type = "press",
                        key = "RED"
                }
                handler(evt)
                assert_equal(canvas_values.color,"white")
                assert_equal(canvas_values.font,"vera")
                assert_equal(canvas_values.font_size,30)
                assert_equal(canvas_values.text_x,200)
                assert_equal(canvas_values.text_y,200)
                assert_equal(canvas_values.text_message,"Botao vermelho pressionado.")
        end)
------------------
        test("Teste 4: Tratador posta evento", function()
                --configuracao do evento
                local evt = {
                        class = "key",
                        type = "press",
                        key = "RED"
                }
                handler(evt)
                assert_equal(events.posted.class,"ncl")
                assert_equal(events.posted.type,"presentation")
                assert_equal(events.posted.area,"fim")
                assert_equal(events.posted.action,"start")
        end)
------------------
        test("Teste 5: Tratador de eventos esta registrado", function()
                assert_equal(events.event_registered,handler)
        end)
end)

Até a linha 61 temos as configurações para os testes. O que fiz foi criar algumas tabelas que representam o estado do sistema e as funções que modificam esse estado. Nesse caso, as funções têm o mesmo nome das que estão nas bibliotecas para TV digital. Na linha 65 o arquivo com o tratador de eventos é carregado e da linha 70 até o final os testes são realizados.

Dessa forma, quando o tratador de eventos é chamado (linha 81 por exemplo) o interpretador Lua chamará as funções que foram criadas no início do arquivo. Essas funções modificam o estado das tabelas que representam o sistema, assim podemos verificar se o tratador conseguiu realizar o que desejávamos apenas checando o valor dessas tabelas.

Nenhuma modificação foi realizada no código do tratador de eventos de forma que ele funcionará normalmente quando for executado pelo middleware.

Evite repetição de código

Para testes em um único tratador de eventos que está em um único arquivo, esse exemplo atende nossas necessidades. Contudo, se tivermos vários arquivos separados com vários tratadores de eventos, começamos perceber que copiar um cabeçalho de configuração para cada aquivo pode ser algo, além de chato, que certamente causará problemas para manutenção.

Para solucionar esse problema poderíamos (é, não vamos estender esse tutorial ainda mais) criar um único módulo com as configurações necessárias e importá-lo em cada arquivo de testes. Fica a dica.

Conclusão

Nesse tutorial apresentei uma ferramenta para a realização de testes em código Lua e desenvolvemos uma metodologia para utilização desses testes nos projetos de TV digital. Espero que essa informação possa ser útil nos seus projetos.

Utilize os comentários para enriquecermos ainda mais as informações que foram apresentadas. Portanto, se encontrou algum erro, tem alguma dúvida ou mesmo qualquer comentário, não deixe de se expressar.


Rafael Carvalho é empreendedor, sócio e diretor de desenvolvimento da Peta5, startup que desenvolve soluções para propaganda na TV Digital. Ministra palestras e cursos sobre desenvolvimento de software para TV Digital. Possui experiência na implementação de metodologias ágeis para o gerenciamento de equipes e desenvolvimento de software. Atua principalmente nos temas TV digital, Interatividade, Ginga, NCL e Lua. Tem interesse por TV digital interativa, desenvolvimento de software, gerenciamento de projetos e inovação.

Nenhum comentário:

Postar um comentário

Sua participação no 100 1/2 PALAVRA é essencial. Obrigada!

Mais vistos esta semana

Receba boletim informativo periódico