Sumário
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 tutorial abordando uma solução para a realização de testes unitários em projetos de TV digital.
Neste tutorial apresentarei uma ferramente 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 dele 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | -- 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 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:
1 2 3 4 5 6 7 8 9 | 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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | ------------------------------------------------------------------------ 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”.
Neste 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:
- o tratador só deve tratar eventos de pressionamento de tecla;
- o tratador só está interessado na tecla vermelha;
- quando a tecla vermelha for pressionada a frase “Botao vermelho pressionado.” deve ser escrita na posição 200 x 200 e
- um evento (com class = ‘ncl’, type = ‘presentation’, area = ‘fim’ e action = ‘start’) deve ser postado;
- 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 | ------- -- 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 funções que modificam esse estado. Neste caso as funções tem 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.
Desta 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 destes 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.
Olá, muito bom o conteúdo, entrei na área recentemente, acredito que em breve retornarei aqui pra mais consultas.
Valeu pelo feedback Victor. Desejo muito sucesso na sua nova jornada e espero conseguir continuar ajudando.
Grande abraço.